本文为《Raft 实战系列理论篇》的番外篇,其讲述的“线性一致性”,也是 Raft 协议在工程实践中所必须考虑的问题。很多读者对“线性一致性”存在误解,为帮助大家更好的理解先详细介绍下其概念。
系列快速链接:
在前面5篇文章中,我们分别介绍了 Raft 基本概念、Raft 选主机制、Raft 基于日志复制实现状态机机制、Raft 选主及状态机维护的安全性、Raft 集群变更防脑裂 & 解决数据膨胀,系统学习 Raft 建议从头阅读。
------------
1. 什么是线性一致性?
在《Raft 基本概念》中有讲:在分布式系统中,为解决单点问题提升系统可用性,通常使用多副本机制做容错,但同时会带来另一个技术问题,即:如何保证多副本之间的数据一致性。
什么是一致性?一般说来一致性有很多种模式,不同的模式均是用来评判一个并发系统模型正确与否的不同程度的标准。而今天要讨论的是线性一致性(Linearizability),其实强一致性(Strong Consistency)模型,就是线性一致性模型, CAP 理论中的 C 指的就是它。
《Raft 基本概念》中我们也简要描述过何为线性一致性:
所谓的强一致性并不是指集群中所有节点在任一时刻的状态必须完全一致,而是指一个目标,即让一个分布式系统看起来只有一个数据副本,并且读写操作都是原子的,这样应用层就可以忽略系统底层多个数据副本间的同步问题。也就是说,我们可以将一个强一致性(线性一致性)分布式系统当成一个整体,一旦某个客户端成功的执行了写操作,那么所有客户端都一定能读出刚刚写入的值。即使发生网络分区故障,或者少部分节点发生异常,整个集群依然能够像单机一样提供服务。
“像单机一样提供服务” 是从感官上描述了一个线性一致性系统应该具备的特性,那么如何判断一个系统是否切实具备线性一致性呢?简单来说,就是不能读到旧版本(stale)数据,具体可分为两种情况:
并发:对于调用时间存在重叠的请求,生效顺序可以任意确定。
偏序:对于调用时间存在先后关系的请求,后一个请求不能违背前一个请求确定的结果。
根据这两条规则可以判定一个系统是否具备线性一致性。我们先来看《Designing Data-Intensive Application》(本文所有例图均来自本书,作者 Martin Kleppmann)中非线性一致性系统的一个例子。
上图中,裁判先将比赛结果写入主库,Alice 和 Bob 看到的数据分别从 Follower 1 库及 Follower 2 库读取。由于主从同步延迟的原因,Follower 2 的同步延迟高于 Follower 1,最终导致 Bob 在听到了 Alice 的惊呼后刷新页面看到的仍然是比赛在进行中这样“不一致”的结果。
线性一致性的基本理念虽然很简单,只要求分布式系统看起来只有一个数据副本,但实际应用中需要关注众多的关键点,下面继续介绍几个例子。
上图从客户端的外部视角展示了多个用户同时请求读写一个系统的场景(每条矩形都是用户发起的一个请求,左端是请求发起的时刻,右端是收到响应的时刻)。由于网络延迟和系统处理时间并不固定,所以柱形长度并不相同。
x
最初的值为0
,Client C 在某个时间段将x
写为1
。Client A 第一个读操作位于 Client C 的写操作之前,因此必须读到原始值
0
。Client A 最后一个读操作位于 Client C 的写操作之后,如果系统是线性一致的,那么必须读到新值
1
。其它与写操作重叠的所有读操作,既可能返回
0
,也可能返回1
,因为我们并不清楚写操作在哪个时间段内哪个精确的点生效,这种情况下读写是并发的。
仅仅是这样的话,仍然不能说这个系统满足线性一致。假设 Client B 的第一次读取返回了 1
,如果 Client A 的第二次读取返回了 0
,那么这种场景并不破坏上述规则,但这个系统仍不满足线性一致,因为客户端在写操作执行期间看到 x
的值在新旧之间来回翻转,这并不符合我们期望的“看起来只有一个数据副本”的要求。
所以我们需要额外添加一个约束,如下图所示。
在任何一个客户端的读取返回新值后,所有客户端的后续读取也必须返回新值,这样系统便满足线性一致了。
我们最后来看一个更复杂的例子,继续细化这个时序图。
4
)。此外这张图里还有一些值得指出的细节点,可以解开很多我们在使用线性一致系统时容易产生的误解:
Client B 的首个读请求在 Client D 的首个写请求和 Client A 的首个写请求之前发起,但最终读到的却是最后由 Client A 写成功之后的结果。 Client A 尚未收到首个写请求成功的响应时,Client B 就读到了 Client A 写入的值。
至此,我们 《Raft 实战系列原理篇》就只剩下如何实现线性一致性与性能优化这个话题,这个会在下文继续深入介绍。