一致性问题的由来

随着分布式系统机器的增多,故障发生的概率也在增加,在一个分布式系统中,硬件或软件都会出现未知的故障或者不正常运行。分布式系统区别于单机系统的一个特性就是它可以容许部分失败,这便是之前分布式系统概念里面提到的容错和故障处理。

故障处理

在一个分布式系统中,硬件或软件都会出现未知的故障或者不正常运行,因此故障处理是贯穿整个系统的难题。容错(设计容错机制如重传)、故障恢复(数据恢复或“回滚”保证一致性)、冗余(多条路由或者备份等技术)都是故障处理技术。

分布式系统区别于单机系统的一个特性就是它可以容许部分失败。当分布式一个系统中的一个组件发生故障时就可能产生部分失效,这个故障也许会影响到其它组件的正确操作,但同时也有可能完全不影响其它组件。而在非分布式系统中的故障通常会影响到所有的组件,甚至很容易就使整个系统奔溃。

分布式系统设计的一个重要目标就是:它可以从部分失效中自动恢复,而不会严重的影响整体性能。特别是,当故障发生时,分布式系统应该在进行恢复的同时继续以可接受的方式继续操作,也就是它应该容许错误,在发生错误的时某种程度上仍可以继续操作。

容错

容错与被称为“可靠的系统(dependable system)”精密相关,它包含了分布式系统中很多有用的需求:

  • 可用性(availability):指在任意给定的时刻系统都可以正确的操作,可根据用户的行为来执行它的功能。高可用的系统在任何给定的时刻都能及时地工作。
  • 可靠性(reliability):指系统可以无故障的持续运行,不同于可用性,可靠性是根据时间间隔而不是任何时刻来进行定义的。高度可靠的系统可以在一个相对较长的时间内持续工作而不被中断。这很微妙,这是一个重要的不同。如果系统在每小时中崩溃1ms,那么它的可用性就超过999%,但是它还是高度不可靠的。与之类似,如果一个系统从来不崩溃,但是在每年8月中停机两个星期,那么它就是高可靠的,但是它的可用性只有96%。这两种属性并不同。
  • 安全性(safety):指系统偶然出现故障的情况下能正确操作而不会造成任何系统灾难。比如那些核电站和把人送入太空的控制系统,就必须提供高度的安全性。这样的系统即使只是出现非常短暂的瞬时故障,结果都是灾难性的。实践表明,要建立一个安全的系统是非常困难的。
  • 可维护性(maintainability):指发生故障的系统被恢复的困难程度。一般高度可维护的系统可能具有高度的可用性,特别是在可以探测故障并自动恢复时。

通常我们要求可靠的系统提供高度的安全性,一个高可用的系统应该具备高可维护性。

失败&错误

一个系统不能兑现它的承诺时我们就认为它是失败(fail)的,比如一个为用户提供大量服务的分布式系统,如果其中的一个或多个不能被提供时,系统就失败了。而错误(error)是系统的一个状态,它可能会导致失败。容错性要求我们正确区分系统的失败和错误,造成错误的原因被称为故障(fault)。通常分布式系统要求故障的发生不能导致系统的失败。故障通常被分为暂时的、间歇的和持久的。

  • 暂时故障(transient fault):只发生一次,然后就消失了,即使重复操作也不会发生;比如一只鸟飞过通信场偶然导致的无信号等。
  • 间隙故障(intermittent fault):就是一会发生一会消失问题,它们很难被诊断,如连接器接触不良导致的故障。
  • 持久故障(permanent fault):那些故障组件被修复之前持续存在的故障,如芯片燃烧,软件错误等。

故障的模式又分为好几种,如奔溃性故障、遗留性故障、定时故障、响应故障灯。

冗余/复制

如果系统是容错的,那么它能做的做好的事情就是对其它进程隐藏故障的发生。这里的关键技术就是使用冗余(redunsancy)来掩盖故障。物理冗余是提供容错性的著名技术,它通过添加额外的装备、机器后进程使系统作为一个整体来容忍部分组件的失效或故障称为可能。冗余就是要我们把之前的一台机器克隆出几台,这就像孙猴子用猴毛变出来的一群孙悟空。我们用几台一样的机器来对外提供服务,其中的一台出现故障的时候,我们就把服务切换到另一台机器上去,这样我们就实现了用冗余来掩盖故障。

复制是实现冗余的一个重要手段,复制的目的:

  • 可靠性
  • 性能:如就近访问,读写分离等

复制的方式可以分为异步复制和同步复制,二者的主要区别在于主副本接收到用户的写请求操作后该什么时候返回成功给用户:

  • 同步复制要求至少一个备副本返回成功后才可以向用户响应成功
  • 而异步复制只需要主副本本机写入成功后即可立即返回。

强同步复制可以保证主备副本之间的一致性,但当备副本出现故障时则会出现可用性问题;异步复制虽然提升了可用性,却也存在我们之前的一致性问题。这里就可以我们就可以感受到一致性和可用性往往是矛盾的。现在我们进入了两难的境地,一方面我们需要多副本冗余复制技术来缓解我们分布式系统的可用性问题,另一方面,保持副本之间的一致性通常要求全局同步,而全局同步必然带来性能的严重下降,这又似乎有些得不偿失。

故通常我们提分布式系统的一致性就是指的多个副本之间数据(状态)是否保持一致,如上面所言,由于分布式系统共享并发的特性给多个副本之间的一致带来了巨大的挑战,这也正是分布式系统一致性问题的由来。

CAP定理

在理论计算机科学中,CAP定理(CAP theorem),又被称作布鲁尔定理(Brewer’s theorem),它指出对于一个分布式计算系统来说,不可能同时满足以下三点:

  • 一致性( Consistency ):等同于所有节点访问同一份最新的数据副本
  • 可用性( Availability ):对数据更新具备高可用性
  • 网络分区容忍性(Partition tolerance)

以实际效果而言,分区相当于对通信的时限要求。系统如果不能在时限内达成数据一致性,就意味着发生了分区的情况,必须就当前操作在C和A之间做出选择。

在分布式系统中,一般来说 P 是前提,所以基本在C和A里进行权衡。

ACID

提到CAP定理的一致性,我们往往会联想到数据库事务处理的ACID特性:

  • A(Atomicity)原子性:事务是一个不可再拆分的最小单位,要么整个执行,要么整个回滚。
  • C(Consistency)一致性:事务要保证数据库整体数据的完整性和业务的数据的一致性,事务成功提交整体数据修改,事务错误则回滚到数据回到原来的状态。
  • I(Isolation)隔离性:隔离性是说两个事务的执行都是独立隔离开来的,事务之前不会相互影响,多个事务操作一个对象时会以串行等待的方式保证事务相互之间是隔离的。
  • D(Durability)持久性:持久性是指一旦事务成功提交后,事务处理过的数据将会保存到数据库,不能再进行回滚。

CAP vs. ACID

ACID中的C表示事务的执行一定保证数据库中数据的约束不被破坏。这里的C指的是从业务层面定义约束,例如银行转账场景,转入和转出金额要平衡,又或者外键指向的行必须存在,这个C一方面依赖数据库的保证,例如原子性,也依赖于业务特性和业务层代码实现。

而CAP是应用在分布式领域的,表示系统的正确性模型,CAP的C和ACID的C关注的并不是同一件事,ACID的C更多强调的是业务层面的约束,而CAP的C则更加强调分布式系统数据之间要达到的正确(一致)程度,从实现角度来看CAP的C相当于是ACID的I(隔离性),数据库的隔离级别如下:

  • 未提交读:存在脏读、不可重复读、幻读问题
  • 提交读(RC):存在不可重复读和幻读问题
  • 可重复读(RR):依然存在幻读问题(InnoDB通过间隙锁避免了幻读)
  • 可序列化(serializable):效率最低,并发性能差

对应的我们在实现分布式一致性时也会有相应的一些类型。

BASE理论

BASE理论是由eBay架构师提出的,是对CAP中一致性和可用性权衡的结果,其来源于对大规模互联网分布式系统实践的总结,是基于CAP定律逐步演化而来。核心思想:在报AP的前提下,虽然无法做到强一致性(Strong consistency),但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性(Eventual consistency)。

  • BA(Basically Available):基本可用。系统发生故障(fault),但依然可用,只是会出现部分损失,如响应时间的损失:如网页响应时间从0.5s变成3s;如功能上的损失:如电商大促,部分用户被引导到一个降级页面。
  • S(Soft State):软状态。相对于硬状态(多副本之间始终保持一致)而言,软状态允许系统系统中的数据存在中间状态,但不影响系统整体的可用性,即允许系统在多个不同节点的数据副本存在数据延时。
  • E(Eventually Consistent):最终一致性,即一段时间后处于软状态的多个副本最终将会转变成一致的。

正确性

分布式系统之所以会出现数据的不一致性的主要原因基于可靠性和容错性的需要使用了多个数据副本。在使用复制技术(同步/异步)的很多情况下,唯一可行的解决方法是放宽在一致性方面的限制,避免全局同步来获得较好的性能,所付出的代价就是各个副本可能不总是相同的。事实表明,一致性可被放宽的程度主要取决于复制数据的访问和更新模式,同时还取决于这些数据的用途。

因此,一致性关注的是分布式系统不同实体之间数据或状态的一致程度,也可以说正确程度。这里我们引申出对系统正确性(Correctness)的定义:给定一些涉及操作与状态的规则,随着操作的演进,系统将一直遵循这些规则。我们把这样的规则称为一致性模型。

一致性模型

一致性模型(consistency model)实质上是进程和数据存储之间的一个约定,即:如果进程同意遵守某些规则,那么数据存储将正常运行。正常情况下,一个进程在一个数据项上执行读操作时,它期待返回的是该数据在其最后一次写操作之后的结果。

在分布式系统中,在没有全局时钟的情况下,精确的定义哪次是写操作的最后一次操作是相当困难的。作为替代方法,我们需要提供其它的定义,因此产生了一系列的一致性模型。两种分类方式:

  • 以数据为中心的一致性模型
  • 以客户为中心的一致性模型

以数据为中心的一致性模型

这个很容易理解,我们在谈论一致性时往往考虑的是数据本身,多个副本之间数据的读写操作。我们在说一致性的时候其实往往暗含了这么一个概念:任意时刻对系统的任意一个副本发起读请求都可以获取我们之前最后一次写操作写入的数据。

怎么定义“之前最后一次”?

事件

在一致性模型中,我们把某个操作抽象为“事件”,对应到分布式系统中相当于一次“RPC Call”,事件起始于发起调用(Invoke或Request),终止于接收到响应(Response),所以在谈论一致性模型的时候一个事件是包含请求和响应两个阶段的。当事件B的Request在事件A的Response之后发生,我们才能称“事件A在B之前发生(happen before,先发生)”;如果两个事件的请求和响应出现了交叉,我们则不能称之为“之前”,即这样的事件之间是不存在“先发生(happen before)”这个关系的。

事件A、B、D、E和A、C、E是存在“先发生”这个关系的,但事件B、C或事件C、D则是不存在这个关系。

顺序

很明显的,只有存在“先发生(happen before)”关系的一组事件才可以被串行执行,或者我们称之为顺序执行;存在事件交叉的两个事件就是我们常说的并行执行,它们之间的顺序是未知的,不确定的。

回到数据上,只有对数据项串进行串行操作(事件)我们才能确保任意时刻发出的“读操作”都可以获取到之前最后一次“写操作”写入的数据。在单机单进程(线程)里我们可以很方便将一系列事件串行执行,可在分布式(多进程线程)的场景下,“事件并发”则是不可避免的,所以才有了分布式系统的一致性问题。

线性一致性 vs. 顺序一致性

  • 线性一致性(Linearizability Consistency):所有节点上事件发生的的顺序全部一样,且和全局时间上时间发生的顺序一致。
  • 顺序一致性(Sequential Consistency):所有节点上事件发生的的顺序全部一样,但是可能和全局时间上发生的顺序不一致。

在全局事件中我们有下面这样的顺序:A1->A2->A3->B1->B3->C1->C2->C3,但我们唯独不能确认事件B2和B1、B3之间的顺序关系。故有进程进程P1、P2、P3彼此之间满足顺序一致性,但进程P2和P1、P3不满足线性一致性。从这里我们可以指导它们之间的关键区别在于在全局时间上的事件顺序。

因果一致性

因果一致性(causal consistency):所有进程必须以相同的顺序看到具有潜在因果关系的写操作。不同的机器可以以不同的顺序看到并发的写操作。

我们不必对一个进程中的每个操作都施加顺序约束,只有因果相关的操作必须按顺序发生。这些操作之间存在因果关系,每个操作都依赖之前操作的结果。比如微信朋友圈,只有好友发布了一条新的动态之后我们才能进行回复操作。

因果一致性的系统中我们必须对这些存在因果关系的操作进行线性化,实现因果一致性要求跟踪哪些进程看到了哪些写操作,这意味着必须构建和维护一张记录哪些操作与哪些操作有关的关系图。

以客户为中心的一致性模型

以数据为中心的一致性模型从数据存储的角度提供系统级别的一致性。由于性能原因,可能只有当进程使用诸如事务处理和锁一类的同步机制时,系统才能保证顺序一致性。

以客户为中心的一致性模型,它们的约束一般都比我们之前介绍的一致性模型要弱,它们只对事件的发送顺序作了部分假设。以客户为中心的一致性模型一般都是弱一致性模型,它并不保证进程或者线程的访问都会返回最新的更新过的值。

系统在数据写入成功之后,不承诺立即可以读到最新写入的值,也不会具体的承诺多久之后可以读到。本质上,它为单一的客户提供一致性的保证,保证该客户对数据存储的访问的一致性。它并不为不同客户的并发访问提供任何一致性保证。

最终一致性

最终一致性(Eventual Consistency),它是弱一致性的一种特定形式。它不要求系统有同步的更新操作,只要求在很长一段时间内没有更新操作时,那么所有的副本逐渐成为相互完全相同的副本。只要求更新操作被保证传播到所有的副本,如:DNS,Web缓存等。

分类

以客户为中心的一致性模型可以看做是因果一致性的特定形式,这里介绍几种类型:

  • 单调读一致性(Monotonic Read Consistency)
  • 单调写一致性(Monotonic Write Consistency)
  • 写读一致性(Writes-follows-reads Consistency)
  • 读你所写一致性(Read-your-writes Consistency)
  • 会话一致性(Session Consistency)

单调读一致性

也称为“读单增一致性”,它要求如果一个进程读取数据项X的值,那么该进程对X执行的任何后续读操作将总是得到第一次读取的那个值或更新的值。

也就是说,单调读一致性保证,如果一个进程已经在t时刻看到x的值,那么以后它不会看到较老版本的x的值。分布式多副本的场景下这意味着,在第一个读操作之前的所有写操作需要在第二个读操作发起到达前传播给其它所有副本。

上面的图符合单调读一致性,下面不符合。

单调写一致性

也称为“写单增一致性”,读单调读相似,它要求一个进程对数据项X执行的写操作必须在进程对X执行任何后续写操作之前完成。

完成一个写操作意味着不管后续操作的启动位置,执行这个后续操作的副本都能反映出同一进程先前的写操作的结果。也就是说,在数据项X的副本上执行的写操作只有在该副本已经通过任何之前的写操作进行更新之后才能被执行,而这些之前执行的写操作可能发生在X的其它副本上。

上面的图满足单调写一致性,下面不符合。

写读一致性

更新是作为前一个读操作的结果传播的。同一个进程对数据项x执行读操作之后的写操作,保证发生在与x读取值相同或比之更新的值上。

也就是说,进程对数据项x执行的任何后续的写操作都会在x的副本上执行,而改副本是用进程最近读取的值更新的。一个场景是,写读一致性可以保证我们只有在看到别人发表的文章之后才能进行回复(用户X发表文章A,然后用户Y回复了一篇文章B,在B被复制到任何副本之前,A必须已经写入那个副本)。

上面的图满足写读一致性,下面不符合。

读你所写一致性

也可以称之为“读写一致性”,因果一致性的一种特定形式。

一个进程对数据项X执行一次写操作的结果总是会被该进程对X执行的任意后续读操作看见。

也就是说,一个进程总可以读到自己更新的数据,即一个写操作总是在同一个进程执行的后续读操作之前完成,而不管这个后续的读操作发生在什么位置。但它不保证其它进程可以即时读到。比如我们再朋友圈发一条动态,我们总是可以立马看到,但你的好友可能需要过一段时间才能看到。

上面的图符合读你所写一致性,下面不符合。和单调读一致性很相似,只是这里的一致性是通过进程P执行的最后一次写操作确定的,而不是通过进程的最后一次读操作确定的。

会话一致性

读写一致性的一种特定形式,进程在访问存储系统的一个会话内时,保证这些进程之间的读你所写。比如我们常见的聊天室场景。

一致性的强弱关系

再看CAP

CAP的C是什么级别的一致性?

  • 线性一致性

CAP定理里,保AP的情况下我们可实现的最高级别的一致性是?

  • CAP理论只声称我们不能构建一个完全可用的线性化系统。
  • 完全可用:MR、MW、WFR等
  • 部分可用/基本可用:因果一致性,RYW等

工程实践

  • 在实际应用场景中,分区(P)的情况是很少出现的,CAP在大多数时间能够同时满足C和A。

Reference