http://b.oldhu.com
最终一致性 – 修订版
最终一致性 - 平衡一致性与可用性,构建一个全球级别的可靠分布式系统
Amazon的云计算的基础是一些基础架构服务,比如:Amazon S3(简单存储服务), SimpleDB和EC2(可伸缩计算云)。这些服务是建造互联网级计算平台和应用的资源。对于这些服务来说,对它们的要求很严格,需要在安全性、可伸缩性、可用性、性能和性价比等方面达到很高的标准,并需要在面向全球的数百万用户时保持其高标准。这些后果之一就是服务可提供的数据一致性,特别是当底层的分布式系统是使用最终一致性的模型进行数据复制时。我们在Amazon设计这些大型系统时,建立了关于规模数据复制的一系列的指导准则和抽象模型,关注在高可用性和数据一致性之间进行取舍。在本文中,我将介绍一些相关的背景,这些背景影响了我们的实施手段。本文在2007年12月时有过一个较早的版本,并在读者的帮助下进行了修订。
历史
所以,一个不容忍网络分割的系统可以实现数据的一致性和可用性,这通常是用事务来实现的:客户端和存储系统必须在同一个环境中,他们在特定的情况下作为一个整体失败,只有这样,客户端才看不到网络分割。但一个重要的事实是,在大型的分布式系统中,网络分割是难免的,因此这时数据的一致性和可用性就无法同时满足。这意味着我们必须做出选择:放松一致性的要求,在网络分割的情况下允许系统保持可用性;或者要求严格的一致性,并导致有时候系统不可用。
在事务性系统的一致性原则中,我们定义了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)。这时系统将确保将同一个进程的写操作顺序化。这是一个很基本的要求,没有这种保证的系统是基本没法用的。
以上的几种模型是可以组合的,比如可以同时有读一致性和会话一致性。从实用的角度出发,读一致性和读写一致性在最终一致性系统中是最需要的,但并不是必须的。有这两个特性的系统会让开发人员编写程序更简单,同时允许存储系统放松一致性并提供高可用性。
服务器端一致性
- N = 存储数据复本的结点的数量
- W = 在写操作完成之前,必须要确认的数据复本的数量
- R = 在读操作的时候,需要联系的结点的数量
如果W + R > N,那么写操作结点集合和读操作结点集合总是互相覆盖的,这样可以确保强一致性。在实现了同步复制的主-备架构的RDBMS中,N=2,W=2,R=1。无论客户端从哪个副本里读取数据,永远可以得到一致的结果;在异步复制并允许从备份系统读取的情况下,N=2,W=1,R=1,这时候W + R = N,一致性无法得到保证。
如何设置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个原因:在高并发的情况下改善读和写的性能;应对网络分割的情况,这时候多数仲裁模型会导致系统部分不可用。
不一致是否可接受要看客户端应用,在所有的情况下,开发人员都必须知道服务器端提供的一致性模型,并在开发应用时加以考虑。对于最终一致性模型有很多改进,比如会话一致性、读一致性,这都为开发人员提供了更好的工具。很多时候应用可以毫无问题地应对存储系统提供的最终一致性保证。一个流行的例子是在网站上,我们可以有所谓的用户一致性,这时不一致窗口要小于用户点击下一个页面的时间,这样写操作只要在下次读之前完成即可。
这篇文章的目的是为了让大家知道一个需要在全球运行的系统的复杂性。这样的系统需要仔细地调整,以确保可以提供应用所需的 持久性、可用性和性能。系统设计人员可以使用的工具之一是一致窗口的大小,在这个窗口内,系统的客户端可以体验到大规模系统背后的工程实现。