英文原文:Eventually Consistent, Revisited
在世界范围构建可靠的分布式系统往往要求在一致性和可用性之间进行权衡。上个月,亚马逊公司的CTO Werner Vogels发表了一篇文章,描述在大型分布式系统中容忍最终数据一致性的方法。
正如InfoQ之前的一篇新闻里所讨论的:
系统架构师角色关键的一方面就是衡量相互冲突的需求、决定解决方案,常常要牺牲一个方面来换取另一个方面。
亚马逊公司的CTO Werner Vogels发布的新帖子讨论了这些基本需求如何应用于基础设施服务,为构建Internet范围的计算平台提供资源。
鉴于这些系统分布在世界范围内,我们处处利用复制技术来保证稳定的性能和高可用性。尽管复制技术使我们达到部分目的,但它的实现并不是完全透明的;在许多情况下,在服务内部使用复制技术都会给服务的客户带来后果。后果之一体现为对数据一致性的限制,特别是在底层分布式系统提供了一种最终一致的数据复制模型的时候。在亚马逊设计这些大型系统时,我们凭借一套有关大规模数据复制的指导原则和抽象,把注意力集中于高可用性和数据一致性之间的权衡选择。
按照Werner所说,考虑一致性有两种方式:一种是从开发者/客户端的角度——他们如何观察数据更新。第二种是从服务器的角度——更新如何流经整个系统,系统对更新有何保证。
在定义客户端一致性模型时应该考虑的内容如下:
- 存储系统……它在本质上是大规模且高度分布的系统,其创建目的是为了保证耐用性和可用性。
- 进程A。对存储系统进行读写。
- 进程B和C。这两个进程完全独立于进程A,也读写存储系统……客户端一致性必须处理一个观察者(在此即进程A、B或C)如何以及何时看到存储系统中的一个数据对象被更新。
- 强一致性。在更新完成后,(A、B或C进行的)任何后续访问都将返回更新过的值。
- 弱一致性。系统不保证后续访问将返回更新过的值,在那之前要先满足若干条件。从更新到保证任一观察者看到更新值的时刻之间的这段时间被称为不一致窗口。
- 最终一致性。这是弱一致性的一种特殊形式;存储系统保证如果对象没有新的更新,最终所有访问都将返回最后更新的值。如果没有发生故障,不一致窗口的最大值可以根据下列因素确定:比如通信延迟、系统负载、复制方案涉及的副本数量。
客户端一致性模型的变体有:
- 因果一致性。如果进程A通知进程B它已更新了一个数据项,那么进程B的后续访问将返回更新后的值,且一次写入将保证取代前一次写入。与进程A无因果关系的进程C的访问遵守一般的最终一致性规则。
- “读己之所写”一致性。这是一个重要的模型。当进程A自己更新一个数据项之后,它总是访问到更新过的值,绝不会看到旧值。这是因果一致性模型的一个特例。
- 会话一致性。这是上一个模型的实用版本,它把访问存储系统的进程放到会话的上下文中。只要会话还存在,系统就保证“读己之所写”一致性……系统的保证也不会延续到新的会话。
- 单调读一致性。如果进程已经看到过数据对象的某个值,那么任何后续访问都不会返回在那个值之前的值。
- 单调写一致性。系统保证来自同一个进程的写操作顺序执行。要是系统不能保证这种程度的一致性,就非常难以编程了。
服务器端的一致性水平取决于如何在数据副本之间传播更新(这是改善吞吐量、提供可伸缩性的典型方式)。只有部分数据副本参与更新操作,且/或作为读操作的一部分与其它副本进行联系时,就会出现弱/最终一致性。发生这种情况的两种常见场景分别是为读伸缩而做大量复制的情况和有复杂数据访问的情况。在大多数这样的系统中,更新以一种“懒”方式传播到副本集内的其它节点上。所有副本都完成更新前的这段时间就是不一致窗口,读取尚未接收到更新的节点是整个系统的薄弱环节。服务器提供的一致性水平可以通过客户端/服务器通信的特定实现来改进,或者由客户端自己来完成:
“读己之所写”一致性、会话一致性和单调一致性是否可以达成,取决于客户端对为其执行分布式协议的服务器的“粘度”。如果每次都是同一台服务器,那么就比较容易保证“读己之所写”一致性和单调一致性。这样做会使管理负载平衡以及容错变得稍困难一些,但这是一种简单的方案…… 客户端有时会实现“读己之所写”一致性和单调读一致性。通过给写入添加版本,对那些版本早于最后版本的值来说,客户端会丢弃这些值的读出。
每个客户端应用对服务器造成的不一致性都有自己的耐受力,但在任何情况下,客户端应用都应该知道服务器应用提供的一致性水平。有很多改善最终一致性模型的实用方法,比如会话级别的一致性和单调读一致性,它们都为开发人员提供了更好的工具。