最终一致性 – 修订版

http://b.oldhu.com 

最终一致性 – 修订版

中文翻译 by hhhtj@javaeye

原文:http://www.allthingsdistributed.com/2008/12/eventually_consistent.html

我在大约一年前写了本文章的第一版,但我对其一直不太满意,因为当时写得有点匆忙。这个专题非常重要,需要特别对待。ACM Queue为了出版我的那篇文章,要我对它进行一下修订。我借此机会进行了改进,形成这个新的版本。

最终一致性 - 平衡一致性与可用性,构建一个全球级别的可靠分布式系统

Amazon的云计算的基础是一些基础架构服务,比如:Amazon S3(简单存储服务), SimpleDB和EC2(可伸缩计算云)。这些服务是建造互联网级计算平台和应用的资源。对于这些服务来说,对它们的要求很严格,需要在安全性、可伸缩性、可用性、性能和性价比等方面达到很高的标准,并需要在面向全球的数百万用户时保持其高标准。

这些服务实际上都是运行在全球的大型分布式系统。这种规模带来了额外的挑战,因为当一个系统要处理万亿级(trillion, 10 12)的请求的时候,平时很少遇到的意外情况就成为了常态,需要在设计和架构系统的时候一并考虑。由于这些系统要在全球范围内提供服务,我们大量地使用复制技术来提供一致的性能和高可用性。复制技术可以帮助我们接近目标,但它并不能以一种完美、透明的方式帮我们实现目标。在很多情况下,这些服务的客户端必须要面对使用复制技术带来的一些后果。

这些后果之一就是服务可提供的数据一致性,特别是当底层的分布式系统是使用最终一致性的模型进行数据复制时。我们在Amazon设计这些大型系统时,建立了关于规模数据复制的一系列的指导准则和抽象模型,关注在高可用性和数据一致性之间进行取舍。在本文中,我将介绍一些相关的背景,这些背景影响了我们的实施手段。本文在2007年12月时有过一个较早的版本,并在读者的帮助下进行了修订。

历史

在理想的世界里,应该只有一种一致性模型:当一个写操作发生时,所有的观察者都能看到这个更新。

理想第一次遇到困难是在70年代后期的数据库系统中。当时关于这个专题最好的文档是Bruce Lindsay et al的“Notes on Distributed Databases“。其中指出了数据库复制的基本原则,论述了实现一致性的一系列技术。其中很多项技术都试图达到分布透明性,也就是说,对于用户来说,整个系统看起来象一个,而不是多个。这个時期的很多系统都采用了这种方案:宁可让整个系统不可用,也不破坏透明性。

在90年代中期,随着更大的Internet系统的出现,这个方法被修正。从那时候,人们开始认识到,对这些系统来说,可用性才是最重要的。但他们还在犹豫,应该牺牲什么来换取可用性?Eric Brewer(UC Berkeley的system processor)是当时Inktomi的头,在2000年PODC(Principles of Distributed Computing)大会的Keynote中提出了不同的取舍方案。他提出了CAP原理,指出共享数据系统的三个属性:数据一致性、系统可用性、对网络分割的容忍性,任何时刻只能同时满足两个。2002年Seth Gilbert和Nancy Lynch的论文对此给出了更正式的确认。

所以,一个不容忍网络分割的系统可以实现数据的一致性和可用性,这通常是用事务来实现的:客户端和存储系统必须在同一个环境中,他们在特定的情况下作为一个整体失败,只有这样,客户端才看不到网络分割。但一个重要的事实是,在大型的分布式系统中,网络分割是难免的,因此这时数据的一致性和可用性就无法同时满足。这意味着我们必须做出选择:放松一致性的要求,在网络分割的情况下允许系统保持可用性;或者要求严格的一致性,并导致有时候系统不可用。

这两种选择都要求客户端应用的开发者注意系统可以提供什么。如果系统强调一致性,开发人员就必须处理系统不可用的情况。比如一个写操作,如果由于系统不可用导致了写失败,那开发人员必须处理这些无法写入的数据;如果系统强调可用性,它会一直允许写入,但在某种情况下可能无法读取到最新的数据。这样开发人员就必须决定客户端是否需要一直访问到绝对最新的数据。有很多应用可以允许稍微旧一点的数据,它们很适合这种强调可用性的模型。

在事务性系统的一致性原则中,我们定义了ACID(原子性、一致性、隔离性、持久性),这是另一种不同层面的一致性承诺。在ACID中,一致性是指当事务完成时数据库要处于一种一致的状态。比如当从一个帐户向另一个帐户转帐时,两个帐户的总额不能改变。在基于ACID的系统中,这种一致性通常是开发者的责任,他们要编写事务相关的代码,数据库则对其进行辅助。

一致性 - 客户端与服务器

有两种看待一致性的途径,一种是从开发者/客户端的角度看:他们如何观察数据更新;另一种是从服务器端看:数据更新如何在系统中流转,对于写操作系统能提供哪些承诺。

客户端一致性

客户端有这样一些组件:

 

 

  • 一个存储系统。现在我们先把它看成一个黑盒子。但我们应该知道在底层它是一个大规模、高度分布的系统,可提供持久性和可用性的保证。
  • 进程A。从存储系统中进行读写的进程。
  • 进程B和C。这两个进程与进程A相互独立,也从存储系统中进行读写。它们是真的进程还是一个进程中的几个线程并不重要,重要的是它们之间相互独立,共享信息需要进行通讯

客户端一致性与观察者(在这个例子中是进程ABC)如何、何时看到存储系统中的数据对象更新有关。当进程A在存储系统中更新了一个数据对象后,以下是几种不同类型的一致性:

  • 强一致性。当写操作完成后,任何后续的来自ABC的访问都会得到更新后的值。
  • 弱一致性。系统不保证后续的访问可以返回最新的值。从写操作完成到所有观察者都可以看到最新的值的时间间隔称为不一致窗口
  • 最终一致性。这是弱一致性的一种,存储系统确保如果数据对象没有其它的更新,则所有的访问最终会返回最后更新的值。如果没有错误发生,则不一致窗口的最大值可根据通讯延迟、系统负载、复本个数等参数进行计算。最常见的最终一致性系统是DNS:对一个域名的更新会以一种配置好的模式向外复制,并与缓存策略结合。最终所有的客户端会看到这个更新。

 

最终一致性模型有几个变种值得探讨:

 

 

  • 因果一致性(causal consistency)。如果进程A已经告诉了B它更新了一个数据项,进程B后续的读操作会得到更新后的数据,写操作会覆盖较早的写操作。来自C的访问与A之间没有这个因果关系,适用正常的最终一致性规则。这样我们就称A和B之间存在因果一致性。
  • 读写一致性(read-your-writes consistency)。这是一个重要的模型,是指进程A更新了一个数据项后,自己永远可以访问到最新的值。这是因果一致性的一个特殊类型(A和B相同时)
  • 会话一致性(session consistency)。这是上一个模型的一种实用版本。当一个进程在一个会话的上下文中访问存储系统时,只要会话存在,系统就保证读写一致性。如果会话因为系统出错而终止,则必须建立一个新的会话,系统不保证跨会话的一致性。
  • 读一致性(monotonic read consistency)。如果一个进程已经看到了对象的一个值,则后续的访问永远不会看到以前的值。
  • 写一致性(monotonic write consistency)。这时系统将确保将同一个进程的写操作顺序化。这是一个很基本的要求,没有这种保证的系统是基本没法用的。

以上的几种模型是可以组合的,比如可以同时有读一致性和会话一致性。从实用的角度出发,读一致性和读写一致性在最终一致性系统中是最需要的,但并不是必须的。有这两个特性的系统会让开发人员编写程序更简单,同时允许存储系统放松一致性并提供高可用性。

从以上这些模型变种可以看到,有很多情景是可能发生的。相关的后果是否可接受则与每个应用相关。很多现代的关系型数据库系统(RDBMS)提供主-备方式的可靠性,同时支持同步和异步模式的数据复制技术。在同步模式下,数据复制作为事务的一个环节存在;在异步模式下数据会延迟到达备份系统,通常是通过日志传送(log shipping)的方式进行。在异步模式下,如果主系统在日志到达备份系统之前出错,从切换后的备机读出的数据将是旧的、不一致的数据。另外为了支持更可扩展的读性能,RDBMS开始支持从备份系统中读取数据。这也是提供最终一致性的一个经典模型,其不一致窗口取决于日志传送的周期和速度。

服务器端一致性

在服务器端我们需要更深入地看一下数据更新是如何流经整个系统,这样我们才能了解是什么决定了让使用系统开发人员看到的不同模型。开始之前让我们先定义几个概念:

  • N = 存储数据复本的结点的数量
  • W = 在写操作完成之前,必须要确认的数据复本的数量
  • R = 在读操作的时候,需要联系的结点的数量

如果W + R > N,那么写操作结点集合和读操作结点集合总是互相覆盖的,这样可以确保强一致性。在实现了同步复制的主-备架构的RDBMS中,N=2,W=2,R=1。无论客户端从哪个副本里读取数据,永远可以得到一致的结果;在异步复制并允许从备份系统读取的情况下,N=2,W=1,R=1,这时候W + R = N,一致性无法得到保证。

这些方案是一些基本的仲裁协议(quorum protocol)。问题在于当系统由于出错无法向W个结点中写入时,写操作将失败,从而使得整个系统不可用。比如N=3、W=3并只有2个结点可用时,写操作一定会失败。

在需要提供高性能和高可用性的分布式存储系统中,数据的副本通常大于2。只关心容错的系统经常使用N=3、W=2、R=2的配置;需要非常高读性能的系统会把数据复制更多份,N可以是几十,甚至几百,同时配置R=1,允许只从一个结点读数据;需要高一致性的系统会设置W=N,当然这会降低写操作的成功率。对于一个关注容错而不太关心一致性的系统,会设置W=1,这样写操作的持久性最小,然后系统会信赖一种推迟复制的技术去更新其它副本。

 

如何设置N,W和R要看常见的情况是怎样,哪个性能环节需要优化。R=1、N=W时为读优化;W=1、R=N时为写优化。当然在后一种情况下,当出错时数据的持久性是无法保证的。如果W < (N + 1) / 2,就会在写结点集合不互相覆盖时出现写冲突。

 

弱一致/最终一致的情况会在W + R <= N的时候出现。这时候有可能读结点集合和写结点集合是互相不覆盖的。如果这是有意为之,而且也不是出错时的情况,那么不把R设置成1是没有任何道理的。这有两个常见的例子:第一个是上面提到的为了扩展读性能而进行复制;第二个例子的数据访问则更复杂 --

 

在简单的key-value模型中,很容易进行版本比较,从而知道哪个是最新写入系统的数据;而在一个返回对象集的系统中,就很难知道哪个对象集是最新的。在大多数W < N的系统中,会有一种推迟复制的机制将数据复制到N中除了W之外的结点,这个时间就是我们之前讨论的不一致窗口。在不一致窗口中,从还没有得到最新数据的结点中读数据只能读到旧的数据。

 

读写一致性、会话一致性、读一致性和写一致性能否实现通常与客户端能否总是能从同一个服务器那里得到服务(stickiness)有关。如果每次都是同一个服务器,那相对来说确保读写一致性、读一致性是容易的。这会使得负责平衡和容错稍困难,但它还是很简单。使用会话来确保客户端与服务器的相关性(sticky, 每次同一个服务器),可使得这个过程变得明确并为客户端提供一个可靠的句柄。

 

有时候是从客户端来实现读写一致性和读一致性。通过向写操作上加入版本信息,客户端可放弃那些在最新版本之前的版本的数据。

 

当系统中一些结点无法连接到另一些结点时,就发生了网络分割。但这时候每个结点集都可以被相应的一组客户端访问到。如果使用经典的多数仲裁方案,那么有W个结点的分区可以继续进行写操作,而另一个分区则无法进行。对读操作同样。网络分割不经常出现,但在数据中心之间,甚至数据中心内容确实会发生。

 

对于一些应用,在网络分割时,任何分区的不可用都是不可接受的,而且客户端必须要能访问可用的分区。这时分区的2边需要新指定一些存储结点用于接受数据,当分区恢复后再进行数据合并。比如Amazon的购物车使用了这样一个永远可写的系统。当网络分割时,用户可以继续向购物车里放东西,即使他原来的购物车放在另外的分区中。当分割恢复时,购物车应用会帮助存储系统一起进行数据合并。

Amazon的Dynamo

Amazon的Dynamo就是这样一个系统,在内部用于Amazon电子商务平台的很多服务,也用于Amazon的Web Services。Dynamo的设计目标之一就是在一个跨越多个数据中心的存储系统中,从一致性、持久生、可用性和性能之间做出平衡。

总结

在大规模分布式系统中,数据的不一致是必须被容忍的,有2个原因:在高并发的情况下改善读和写的性能;应对网络分割的情况,这时候多数仲裁模型会导致系统部分不可用。


不一致是否可接受要看客户端应用,在所有的情况下,开发人员都必须知道服务器端提供的一致性模型,并在开发应用时加以考虑。对于最终一致性模型有很多改进,比如会话一致性、读一致性,这都为开发人员提供了更好的工具。很多时候应用可以毫无问题地应对存储系统提供的最终一致性保证。一个流行的例子是在网站上,我们可以有所谓的用户一致性,这时不一致窗口要小于用户点击下一个页面的时间,这样写操作只要在下次读之前完成即可。


这篇文章的目的是为了让大家知道一个需要在全球运行的系统的复杂性。这样的系统需要仔细地调整,以确保可以提供应用所需的 持久性、可用性和性能。系统设计人员可以使用的工具之一是一致窗口的大小,在这个窗口内,系统的客户端可以体验到大规模系统背后的工程实现。

猜你喜欢

转载自oldhu.iteye.com/blog/700050