如果我们想A和B的服务是高度可用的,我们就不得不允许A和B读到的数据不一致发生。
用事务的观点来看一下N1和N2的数据持久化问题,a1是写操作,a2是读操作。在一个本地系统中,可以利用数据库中的锁的机制方便地处理,隔离a2中的读操作,直到a1的写成功完成。然而,在分布式的模型中,需要考虑到N1和N2节点,中间的消息同步也要完成才行。除非我们可以控制a2何时发生,我们永远无法保证a2可以读到a1写入的数据。所有加入控制的方法(阻塞,隔离,中央化的管理,等等)会要么影响分区容错性,要么影响A和B的可用性。
我们必须做一些取舍:
1. 放弃Partition Tolerance 。如果你想避免分区问题发生,你就必须要阻止其发生。一种做法是将所有的东西(与事务相关的)都放到一台机器上。或者放在像机柜这类的统一管理的单元上。但100%不出现分区是不可能的,而且放弃分区容错代价昂贵。Haddoop的必然失效假设,Google和Facebook采用自己的硬件网络设备就是一个很好的证明。
2. 放弃Availability 。相对于放弃分区容错性来说,其反面就是放弃可用性。一旦遇到分区事件,受影响的服务需要等待数据一致,因此在等待期间就无法对外提供服务。在多个节点上控制这一点会相当复杂,而且恢复的节点需要处理逻辑,以便平滑地返回服务状态。
3. 放弃Consistency, 或者如同Werner Vogels所提倡的,接受事情会变得“最终一致 (Eventually Consistent)”。许多的不一致性并不比你想的需要更多的工作(意味着持续的一致性或许并不是我们所需要的)。比如网络购书,如果一本库存的书,接到了2个订单,第二个就会成为备份订单。只要告知客户这种情况(请记住这是一种罕见的情况),也许每个人都会高兴的。
4. 引入BASE 。有一种架构的方法称作BASE(Basically Available, Soft-state, Eventually consistent)支持最终一致概念的接受。BASE如其名字所示,是ACID的反面,但如果认为任何架构应该完全基于一种(BASE)或完全基于另一种(ACID),就大错特错了。应该结合自身的使用场景,合理平衡BASE和ACID。
放弃分区容错是不现实的[注2],我们需要在可用性和一致性中做些权衡。我们现在来看第三种和第四种选择。
由于一致性和可用性不可能同时满足,我们只有两个选择:放松一致性,在分区发生时仍然可用;一致性具有优先,意味着某些时候服务不可用,大部分时间仍然可用。不论哪一种选择,客户端开发者需要关注系统的行为,可能都要做些额外的处理。如果系统强调的是一致性,那开发者就需要处理服务不可用的情况,此时的写操作需要重试。如果系统强调的是可用性,所有的写操作都被接受,读者可能读到不正确的数据,开发者需要重新读取。Werner Vogels考虑一致性有两种视角:开发者即客户端,服务器。前者观察数据更新,后者观察数据在系统中更新。
客户端一般有下面的组件构成:
存储系统。它在本质上是大规模且高度分布的系统,其创建目的是为了保证耐用性和可用性。
进程A。对存储系统进行读写。
进程B和C。这两个进程完全独立于进程A,也读写存储系统。
客户端的一致性模型必须处理一个观察者(在此即进程A、B或C)如何以及何时看到存储系统中的一个数据对象被更新。在进程A更新一个数据对象后,我们面对下面3个一致性类型:
弱一致性。系统不保证后续访问将返回更新过的值,在返回更新过的值之前要先满足若干条件。从更新到所有观察者都看到更新值的时间称为不一致窗口(inconsistency window)。
最终一致性(Eventual consistency)。这是弱一致性的一种特殊形式;存储系统保证如果对象没有新的更新,最终所有访问都将返回最后更新的值。如果没有发生故障,不一致窗口的最大值可以根据下列因素确定:比如通信延迟、系统负载、复制涉及的副本数量。
客户端一致性模型有一些重要的变体:
因果一致性(Causal consistency)。如果进程A通知进程B它已更新了一个数据项,那么进程B的后续访问将返回更新后的值,且一次写入将保证取代前一次写入。与进程A无因果关系的进程C的访问遵守一般的最终一致性规则。
“读己之所写”一致性(Read-your-writes consistency)。当进程A自己更新一个数据项之后,它总是访问到更新过的值,绝不会看到旧值。这是因果一致性模型的一个特例。
会话一致性(Session consistency)。这是上一个模型的实用版本,它把访问存储系统的进程放到会话的上下文中。只要会话还存在,系统就保证“读己之所写”一致性。如果由于某种原因,会话被中止了,系统将会新启一个会话。系统保证会话之间是独立不重叠的。
单调读一致性(Monotonic read consistency)。如果进程已经看到过数据对象的某个值,那么任何后续访问都不会返回在那个值之前的值。
单调写一致性(Monotonic write consistency)。系统保证来自同一个进程的写操作顺序执行。要是系统不能保证这种程度的一致性,就非常难以 编程了。
每个客户端应用对服务器造成的不一致性都有自己的耐受力,但在任何情况下,客户端应用都应该知道服务器应用提供的一致性水平。有很多改善最终一致性模型的实用方法,比如会话级别的一致性和单调读一致性,它们都为开发人员提供了更好的工具。
最终一致性并不是分布式系统所独有的。其实现在的关系 数据库的主从数据库同步也是最终一致性的例子。如果采用同步方式,即是事务处理。如果是异步方式,在主数据库上的写会保持到数据库日志中,然后将日志异步地同步到所有的从数据库上。异步方式可能在日志尚未传输完成就已经宕机,从从数据库上读的数据可能是旧值,与主数据库不一致。为了提供可扩展性和高性能,采用的读写分离技术是一种典型的最终一致性,它的不一致窗口是日志传输的时延。
服务器端的一致性水平取决于如何在数据的各个副本之间传播更新(这是改善吞吐量、提供可伸缩性的典型方式)。只有部分数据副本参与更新操作,作为读操作的一部分与其它副本进行联系时,就会出现弱/最终一致性。发生这种情况的两种常见场景分别是为读伸缩而做大量复制的情况和有复杂数据访问的情况。在大多数这样的系统中,更新是以一种“懒”方式传播到的其它节点的副本上。所有副本都完成更新前的这段时间就是不一致窗口,读取尚未接收到更新的节点是整个系统的薄弱环节。
从服务端角度,如何尽快将更新后的数据分布到整个系统,降低达到最终一致性的时间窗口,是提高系统的可用度和用户体验非常重要的方面。在继续分析之前先定义一些变量:
1. N — 数据复制的份数
2. W — 更新数据是需要保证写完成的节点数
3. R — 读取数据的时候需要读取的节点数
如果W+R>N,写的节点和读的节点重叠,则是强一致性。例如对于典型的一主一备同步复制的关系型数据库,N