如前所述,分布式系统中很多事情都有可能出错。解决出错最简单粗暴的方法是让整个系统宕机,并给出出错原因。但在实际生产中,这种方式多不可接受,此时我们就需要找到容错(tolerating faults)的方法。即,即使系统构件出现了一些问题,我们能保证系统仍然正常运行。
本章我们将会讨论一些用于构建具有容错性分布式系统的算法和协议(alogrithm and protocol)。在设计算法和协议时,我们假设第八章提到的分布式系统中的问题都会存在:
构建一个容错系统最好的方法是:找到一些基本抽象,可以对上提供某些承诺,应用层可以依赖这些承诺来构建系统,而不必关心底层细节。在第七章中,通过使用事务,应用层可以假设不会发生宕机(原子性,意思是不会因为宕机出现让事务停留在半成功的状态),没有其他应用并发访问数据库数据(隔离性),且存储系统非常可靠(持久性)。事务模型会隐藏节点宕机、竞态条件(race conditions)、硬盘故障等底层细节,即使这些问题出现了,应用层也不必关心。
在提供最终一致性语义的数据库里,如果你问不同副本同一个问题(比如说查询某条数据),则很可能得到不同的回答(响应),这就很让人迷惑了。如果多副本数据库在行为上能够表现的像只有一个副本,应用层编程将会简单很多。这样在任意时刻,每个客户端所看到的数据视图都是一样的,而不用去担心引入多副本带来的副本滞后(replication lag)等问题。
这就是线性一致性(linearizability)的基本思想,他还有很多其他称呼:原子一致性(atomic consistency)、强一致性(strong consistency)、即时一致性(immediate consistency),或者外部一致性(external consistency)。线性一致性的精确定义很精妙,本节余下部分会进行详细探讨。但其基本思想是,一个系统对外表现的像所有数据只有一个副本,作用于数据上的操作都可以原子地完成。有了这个保证,不管系统中实际上有多少副本,应用层都不用关心。这种抽象,或者说保证,类似于编程中的接口。 线性一致性是一种数据新鲜度保证(recency guarantee)。为了理解这个说法,让我们看一个非线性一致性系统的例子:
上图显示了一个非线性一致性的体育网站。Alice 和 Bob 在一间屋子里,分别通过手机来查看 2014 年国际足联世界杯的总决赛的结果。在最终比分出来后,Alice 刷新了网页,并且看到了发布的赢家信息,并且将该结果告诉了 Bob。Bob 有点难以置信,重新刷了一下网页,但是他的请求被打到了一个滞后的数据库副本上,该副本显示比赛仍在进行。
如果 Alice 和 Bob 同时(也就是并发)刷新网页,可能还不会对出现不同结果有太多惊讶,毕竟他们也不知道谁的查询请求先到(因为并发)。但,上述例子中,Bob 是在 Alice 告知他结果后刷新的网页,因此他才会期待至少能看到和 Alice 一样新的结果。该例子中 Bob 的请求返回了一个过期的结果,这便是违反了线性一致性。
线性一致性背后的思想很简单:让系统表现得好像只有一个数据副本。但精确地将其拆解开,还需要花很多心思。下面我们来看更多的例子,以更好得理解线性一致性。 下图显示了三个客户端并发访问提供线性一致性的数据库的同一个键。在分布式系统论文中,x 被称为“寄存器”(register)。在实践中,x 可以是一个键值存储中的键值对、关系型数据中的一行或者文档数据中的一个文档。
为了简单起见,上图只显示了客户端角度数据读写视图,而略去了数据库的内部数据视角。每个时间条代表一个客户端的请求,起点代表客户端发出请求时刻、终点代表客户端收到响应时刻。由于网络延迟的不确定性,客户端不能够确切的知道数据库会在什么时刻处理其请求,而只能知道这个时间点一定在请求发出和收到回复之间,即时间条中的某个时刻。
在本例中,寄存器支持两种类型的操作:
read_(x) ⇒ v
: 客户端请求读寄存器 x 的值,数据库会返回值 vwrite(x, v) ⇒ r
: 客户端请求将寄存器 x 的值设置为 v,数据返回结果 r(可能是成功或者失败)在上图中,x 初始值为 0,客户端 C 发出一个写请求将其置为 1。在此期间,客户端 A 和 B 不断地向数据库请求 x 的最新值。试问 A 和 B 的每个读请求都会读到什么值?