etcd 是线性一致性读,而 zk 却是顺序一致性读,再加上各种共识、强弱一致的名词,看的时候总会混淆,这篇文档就列举下分布式系统中的那些"一致性名词",引用了很多其他的文章,不过会多出一些例子来帮助理解。
在谈到一致性这个词时,你会想到CAP理论的 consistency,或者 ACID 中的 consistency,或者 cache一致性协议的 coherence,还是 Raft/Paxos 中的 consensus?
一致性这个词在不同的领域具有不同的含义,毕竟这个中文词在英文中对应了不同的术语,consistency,coherence,consensus三个单词统一翻译为”一致性”。因此在谈一致性之前,有必要对这几个概念做一个区分,否则很容易让人迷惑
Coherence 只出现在Cache Coherence 一词中,称为”缓存一致性”,研究多核场景,即怎么保证多个核上的CPU 缓存数据是一致的,一般是单机维度的,不算分布式领域,可以参考这篇文章
consensus准确的翻译是共识,即多个提议者达成共识的过程,例如Paxos,Raft 就是共识算法,paxos 是一种共识理论,分布式系统是他的场景,一致性是他的目标。
一些常见的误解:使用了 Raft或者 paxos 的系统都是线性一致的(Linearizability 即强一致),其实不然,共识算法只能提供基础,要实现线性一致还需要在算法之上做出更多的努力。
因为分布式系统引入了多个节点,节点规模越大,宕机、网络时延、网络分区就会成为常态,任何一个问题都可能导致节点之间的数据不一致,因此Paxos 和 Raft 准确来讲是用来解决一致性问题的共识算法,用于分布式场景,而非”缓存一致性“这种单机场景。所以很多文章也就简称”Paxos是分布式系统中的一致性算法“,
一致性(Consistency)的含义比共识(consensus)要宽泛,一致性指的是多个副本对外呈现的状态。包括顺序一致性、线性一致性、最终一致性等。而共识特指达成一致的过程,但注意,共识并不意味着实现了一致性,一些情况下他是做不到的。
这里提一下Paxos,Paxos 其实是一类协议,Paxos 中包含 Basic Paxos、Multi-Paxos、Cheap Paxos 和其他的变种。Raft 就是 Multi-Paxos 的一个变种,Raft 通过简化 Multi-Paxos 的模型,实现了一种更容易让人理解和工程实现的共识算法,
Paxos是第一个被证明完备的共识算法,能够让分布式网络中的节点在出现错误时仍然保持一致,当然前提是没有恶意节点,也就是拜占庭将军问题。在传统的分布式系统领域是不需要担心这种问题的,因为不论是分布式数据库、消息队列、分布式存储,你的机器都不会故意发送错误信息,最常见的问题反而是节点失去响应,所以它们在这种前提下,Paxos是足够用的。
consensus共识在实现机制上属于复制状态机(Replicated State Machine)的范畴,复制状态机是一种很有效的容错技术,基于复制日志来实现,每个 Server 存储着一份包含命令序列的日志文件,状态机会按顺序执行这些命令。因为日志中的命令和顺序都相同,因此所有节点会得到相同的数据。
因此保证系统一致性就简化为保证操作日志的一致,这种复制日志的方式被大量运用,如 GSF、HDFS、ZooKeeper和 etcd 都是这种机制。
共识算法还有一个很重要的领域,就是比较火的区块链,比如工作量证明(POW)、权益证明(POS)和委托权益证明(DPOS)、置信度证明(PoB)等等,都是共识算法,这篇文章就列出来了 30 种
大家熟知的zk、etcd这种之所以叫“传统分布式”,就是相对于区块链这种”新型分布式系统“而言的,都是多节点共同工作,只是区块链有几点特殊:
介绍完了Coherence和consensus共识,我们来看consistency一致性,也就是我们平时说的最多的 CAP、Base、ACID之类。
最简单的,客户端C1将系统中的一个值K由V1更新为V2,客户端C2/C3/C4..需要立即读取到K的最新值
一致性要求的是一致,并不是正确,如果所有节点一致给出一个”错误“的答案,那也叫一致性
对于不同的场景,用户角度对于一致性的要求是不一样的,例如:
上面是夸张了的用户情况,在实际业务中,一致性也是分等级的,如强一致性和弱一致性,怎么使用要看具体情况和系统的容忍度。
强一致性和弱一致性只是一种统称,按照从强到弱,可以划分为
强一致性包括线性一致性和顺序一致性,其他的如最终一致都是弱一致性。
关于强和弱的定义,可以参考剑桥大学的slide
Strong consistency
– ensures that only consistent state can be seen.
* All replicas return the same value when queried for the attribute of an object * All replicas return the same value when queried for the attribute of an object. This may be achieved at a cost – high latency.
Weak consistency
– for when the “fast access” requirement dominates.
* update some replica, e.g. the closest or some designated replica
* the updated replica sends up date messages to all other replicas.
* different replicas can return different values for the queried attribute of the object the value should be returned, or “not known”, with a timestamp
* in the long term all updates must propagate to all replicas …….
强一致性集群中,对任何一个节点发起请求都会得到相同的回复,但将产生相对高的延迟。而弱一致性具有更低的响应延迟,但可能会回复过期的数据,最终一致性即是经过一段时间后终会到达一致的弱一致性。
如买最后一张车票,两个售票处分别通过某种方式确认过这张票的存在。这时,两家售票处几乎同时分别来了一个乘客要买这张票,从各自“观察”看来,自己一方的乘客都是先到的,这种情况下,怎么能达成对结果的共识呢?看起来很容易,卖给物理时间上率先提交请求的乘客即可。
然而,对于两个来自不同位置的请求来说,要判断在时间上的“先后”关系并不是那么容易。两个车站的时钟时刻可能是不一致的,时钟计时可能不精确的,根据相对论的观点,不同空间位置的时间是不一致的。因此追求绝对时间戳的方案是不可行的,能做的是要对事件的发生进行排序。
这也是解决分布式系统领域很多问题的核心秘诀:把不同时空发生的多个事件进行全局唯一排序,而且这个顺序还得是大家都认可的,排了序,一个一个处理就行了,和单机没有任何区别(不考虑突然故障情况,只考虑共识机制)
如果存在可靠的物理时钟,实现排序往往更为简单。高精度的石英钟的漂移率为 10的-7 次方,最准确的原子震荡时钟的漂移率为 10的-13 次方。Google 曾在其分布式数据库 Spanner 中采用基于原子时钟和 GPS 的“TrueTime”方案,能够将不同数据中心的时间偏差控制在 10ms 置信区间。在不考虑成本的前提下,这种方案简单、有效。然而,计算机系统的时钟误差要大得多,这就造成分布式系统达成一致顺序十分具有挑战,或者说基本不可能。
要实现绝对理想的严格一致性(Strict Consistency)代价很大。除非系统不发生任何故障,而且所有节点之间的通信无需任何时间,此时整个系统其实就等价于一台机器了。因此根据实际需求的不用,人们可能选择不同强度的一致性。
虽然强度上 线性一致性 > 顺序一致性,但因为顺序一致性出现的时间比较早(1979年),线性是在顺序的基础上的加强(1990 年)。因此先介绍下 顺序一致性
顺序一致性也算强一致性的一种,他的原理比较晦涩,论文看这里
举例说明1:下面的图满足了顺序一致,但不满足线性一致。
image
从图上看,进程P1,P2的一致性并没有冲突。因为从这两个进程的角度来看,顺序应该是这样的:
Write(y,2), Read(x,0), Write(x,4), Read(y,2)
这个顺序对于两个进程内部的读写顺序都是合理的,只是这个顺序与全局时钟下看到的顺序并不一样。在全局时钟的观点来看,P2进程对变量X的读操作在P1进程对变量X的写操作之后,然而P2读出来的却是旧的数据0
举例说明 2:
假设我们有个分布式 KV 系统,以下是四个进程 对其的操作顺序和结果:
--表示持续的时间,因为一次写入或者读取,客户端从发起到响应是有时间的,发起早的客户端,不一定拿到数据就早,有可能因为网络延迟反而会更晚。
情况 1:
A: --W(x,1)----------------------
B: --W(x,2)----------------------
C: -R(x,1)- --R(x,2)-
D: -R(x,1)- --R(x,2)--
情况 2:
A: --W(x,1)----------------------
B: --W(x,2)----------------------
C: -R(x,2)- --R(x,1)-
D: -R(x,2)- --R(x,1)--
上面情况 1 和 2 都是满足顺序一致性的,C 和 D 拿的顺序都是 1-2,或 2-1,只要CD 的顺序一致,就是满足顺序一致性。只是从全局看来,情况 1 更真实,情况 2 就显得”错误“了,因为情况2 是这样的顺序
B W(x,2) -> A W(x,1) -> C R(x,2) -> D R(x,2) -> C R(x,1) -> D R(x,1)
不过一致性不保证正确性,所以这仍然是一个顺序一致。再加一种情况 3:
情况 3:
A: --W(x,1)----------------------
B: --W(x,2)----------------------
C: -R(x,2)- --R(x,1)-
D: -R(x,1)- --R(x,2)--
情况 3 就不属于顺序一致了,因为C 和 D 两个进程的读取顺序不同了。
回到情况 2,C 和 D 拿数据发起的时间是不同的,且有重叠,有可能 C 拿到 1 的时候,D 已经拿到了 2,这就导致了不同的客户端在相同的时间获取了不一样的数据,但其实这种模式在现实中的用的挺广泛的:
如:你在Twitter上写了2条推文,你的操作会耗费一定的时间渗透进一层层的缓存系统,不同的朋友将在不同的时间看到你的信息,但每个朋友都会以相同顺序看到了你的2条推文,不会是乱序。只是一个朋友已经看到了第二条,一个朋友才刚看到第一条,不过没关系,他总会看到两条,顺序没错就行,无伤大雅。
但有些时候,顺序一致是不满足要求的,举例说明 3:
从时间轴上可以看到,B0 发生在 A0 之前,读取到的 x 值为0。B2 发生在 A0 之后,读取到的 x 值为1。而读操作 B1,C0,C1 与写操作 A0 在时间轴上有重叠,因此他们可能读取到旧的值0,也可能读取到新的值1。注意,C1 发生在 B1 之后(二者在时间轴上没有重叠),但是 B1 看到 x 的新值,C1 反而看到的是旧值。即对用户来说,x 的值发生了回跳。
即要求任何一次读都能读到最新数据,和全局时钟一致。对比例1,既满足顺序一致又满足线性一一致应该是这样的:
每个读操作都读到了该变量的最新写的结果,同时两个进程看到的操作顺序与全局时钟的顺序一样,都是Write(y,2), Read(x,4), Write(x,4), Read(y,2)
ZooKeeper
一种说法是 ZooKeeper 是最终一致性,因为由于多副本、以及保证大多数成功的 Zab 协议,当一个客户端进程写入一个新值,另外一个客户端进程不能保证马上就能读到这个值,但是能保证最终能读取到这个值。
另外一种说法是 ZooKeeper 的 Zab 协议类似于 Paxos 协议,提供了强一致性。
但这两种说法都不准确,ZooKeeper 文档中明确写明它的一致性是 Sequential consistency即顺序一致。
ZooKeeper中针对同一个Follower A提交的写请求request1、request2,某些Follower虽然可能不能在请求提交成功后立即看到(也就是强一致性),但经过自身与Leader之间的同步后,这些Follower在看到这两个请求时,一定是先看到request1,然后再看到request2,两个请求之间不会乱序,即顺序一致性
其实,实现上ZooKeeper 的一致性更复杂一些,ZooKeeper 的读操作是 sequential consistency 的,ZooKeeper 的写操作是 linearizability 的,关于这个说法,ZooKeeper 的官方文档中没有写出来,但是在社区的邮件组有详细的讨论。ZooKeeper 的论文《Modular Composition of Coordination Services》 中也有提到这个观点。
总结一下,可以这么理解 ZooKeeper:从整体(read 操作 +write 操作)上来说是 sequential consistency,写操作实现了 Linearizability。
线性一致性又被称为强一致性、严格一致性、原子一致性。是程序能实现的最高的一致性模型,也是分布式系统用户最期望的一致性。CAP 中的 C 一般就指它
顺序一致性中进程只关心大家认同的顺序一样就行,不需要与全局时钟一致,线性就更严格,从这种偏序(partial order)要达到全序(total order)
要求是:
1.任何一次读都能读到某个数据的最近一次写的数据。
2.系统中的所有进程,看到的操作顺序,都与全局时钟下的顺序一致。
以上面的例 3 继续讨论:
B1 看到 x 的新值,C1 反而看到的是旧值。即对用户来说,x 的值发生了回跳。
在线性一致的系统中,如果 B1 看到的 x 值为1,则 C1 看到的值也一定为1。任何操作在该系统生效的时刻都对应时间轴上的一个点。如果我们把这些时刻连接起来,如下图中紫线所示,则这条线会一直沿时间轴向前,不会反向回跳。所以任何操作都需要互相比较决定,谁发生在前,谁发生在后。例如 B1 发生在 A0 前,C1 发生在 A0 后。而在前面顺序一致性模型中,我们无法比较诸如 B1 和 A0 的先后关系。
image
线性一致性的理论在软件有哪些体现呢?
etcd 与 raft
上面提到ZooKeeper的写是线性一致性,读是顺序一致性。而 etcd读写都做了线性一致,即 etcd 是标准的强一致性保证。
etcd是基于raft来实现的,raft是共识算法,虽然共识和一致性的关系很微妙,经常一起讨论,但共识算法只是提供基础,要实现线性一致还需要在算法之上做出更多的努力如库封装,代码实现等。如raft中对于一致性读给出了两种方案,来保证处理这次读请求的一定是 Leader:
基于 raft 的软件有很多,如 etcd、tidb、SOFAJRaft等,这些软件在实现一致读时都是基于这两种方式。
关于 etcd 的选主架构这里不做描述,可以看这篇文章,这里对ReadIndex和Lease Read做下解释,即etcd 中线性一致性读的具体实现
由于在 Raft 算法中,写操作成功仅仅意味着日志达成了一致(已经落盘),而并不能确保当前状态机也已经 apply 了日志。状态机 apply 日志的行为在大多数 Raft 算法的实现中都是异步的,所以此时读取状态机并不能准确反应数据的状态,很可能会读到过期数据。
基于以上这个原因,要想实现线性一致性读,一个较为简单通用的策略就是:每次读操作的时候记录此时集群的 commited index,当状态机的 apply index 大于或等于 commited index 时才读取数据并返回。由于此时状态机已经把读请求发起时的已提交日志进行了 apply 动作,所以此时状态机的状态就可以反应读请求发起时的状态,符合线性一致性读的要求。这便是 ReadIndex 算法。
那如何准确获取集群的 commited index ?如果获取到的 committed index 不准确,那么以不准确的 committed index 为基准的 ReadIndex 算法将可能拿到过期数据。
为了确保 committed index 的准确,我们需要:
leader 会发起一次广播请求,如果还能收到大多数节点的应答,则说明此时 leader 还是 leader。这点非常关键,如果没有这个环节,leader 有可能因网络分区等原因已不再是 leader,如果读请求依然由过期的 leader 处理,那么就将有可能读到过去的数据。
这样,我们从 leader 获取到的 commited index 就作为此次读请求的 ReadIndex。
以网络分区为例:
image
如上图所示:
这样一来,Raft 算法就可以保障 CAP 中的 C 和 P,但无法保障 A:网络分区时并不是所有节点都可响应请求,少数节点的分区将无法进行服务,从而不符合 Availability。因此,Raft 算法是 CP 类型的一致性算法。
Raft保证读请求Linearizability的方法:
1.Leader把每次读请求作为一条日志记录,以日志复制的形式提交,并应用到状态机后,读取状态机中的数据返回。(一次RTT、一次磁盘写)
2.使用Leader Lease,保证整个集群只有一个Leader,Leader接收到都请求后,记录下当前的commitIndex为readIndex,当applyIndex大于等于readIndex 后,则可以读取状态机中的数据返回。(0次RTT、0次磁盘写)
3.不使用Leader Lease,而是当Leader通过以下两点来保证整个集群中只有其一个正常工作的Leader:(1)在每个Term开始时,由于新选出的Leader可能不知道上一个Term的commitIndex,所以需要先在当前新的Term提交一条空操作的日志;(2)Leader每次接到读请求后,向多数节点发送心跳确认自己的Leader身份。之后的读流程与Leader Lease的做法相同。(一次RTT、0次磁盘写)
4.从Follower节点读:Follower先向Leader询问readIndex,Leader收到Follower的请求后依然要通过2或3中的方法确认自己Leader的身份,然后返回当前的commitIndex作为readIndex,Follower拿到readIndex后,等待本地的applyIndex大于等于readIndex后,即可读取状态机中的数据返回。(2次或1次RTT、0次磁盘写)
Linearizability 和 Serializability
Serializability是数据库领域的概念,而Linearizability是分布式系统、并发编程领域的东西,在这个分布式SQL时代,自然Linearizability和Serializability会经常一起出现。
Serializable的含义是:对并发事务包含的操作进行调度后的结果和某种把这些事务一个接一个的执行之后的结果一样。最简单的一种调度实现就是真的把所有的事务进行排队,一个个的执行,显然这满足Serializability,问题就是性能。可以看出Serializability是与数据库事务相关的一个概念,一个事务包含多个读,写操作,这些操作由涉及到多个数据对象。
Linearizability: 针对单个操作,单个数据对象而说的。属于CAP中C这个范畴。一个数据被更新后,能够立马被后续的读操作读到。
Strict Serializability: 同时满足Serializability和Linearizability。
举个最简单的例子:两个事务T1,T2,T1先开始,更新数据对象o,T1提交。接着T2开始,读数据对象o,提交。以下两种调度:
因果一致性,属于弱一致性,因为在Causal consistency中,只对有因果关系的事件有顺序要求。
没有因果一致性时会发生如下情形:
所以很多系统采用因果一致性系统来避免这种问题,例如微信的朋友圈就采用了因果一致性,可以参考:https://www.useit.com.cn/thread-10587-1-1.html
image
最终一致性这个词大家听到的次数应该是最多的,也是弱一致性,不过因为大多数场景下用户可以接受,应用也就比较广泛。
理念:不保证在任意时刻任意节点上的同一份数据都是相同的,但是随着时间的迁移,不同节点上的同一份数据总是在向趋同的方向变化。
简单说,就是在一段时间后,节点间的数据会最终达到一致状态。不过最终一致性的要求非常低,除了像gossip这样明确以最终一致性为卖点的协议外,包括redis主备、mongoDB、乃至mysql热备都可以算是最终一致性,甚至如果我记录操作日志,然后在副本故障了100天之后手动在副本上执行日志以达成一致,也算是符合最终一致性的定义。有人说最终一致性就是没有一致性,因为没人可以知道什么时候算是最终。
上边提到的因果一致性可以理解为是最终一致性的变种, 如果进程 A 通知进程 B 它已经更新了一个数据项,那么进程 B 的后续访问将返回更新后的值,并且写操作将被保证取代前一次写入。和进程 A 没有因果关系的 C 的访问将遵循正常的最终一致性规则。
最终一致其实分支很多,以下都是他的变种:
后面要提到的 BASE理论中的 E,就是Eventual consistency最终一致
ACID 是处理事务的原则,一般特指数据库的一致性约束,ACID 一致性完全与数据库规则相关,包括约束,级联,触发器等。在事务开始之前和事务结束以后,都必须遵守这些不变量,保证数据库的完整性不被破坏,因此 ACID 中的 C 表示数据库执行事务前后状态的一致性,防止非法事务导致数据库被破坏。比如银行系统 A 和 B 两个账户的余额总和为 100,那么无论 A, B 之间怎么转换,这个余额和是不变,前后一致的。
这里的C代表的一致性:事务必须遵循数据库的已定义规则和约束,例如约束,级联和触发器。因此,任何写入数据库的数据都必须有效,并且完成的任何事务都会改变数据库的状态。没有事务可以创建无效的数据状态。注意,这与CAP定理中定义的“一致性”是不同的。
ACID 可以翻译为酸,相对应的是碱,也就是 BASE,不过提BASE之前要先说下 CAP,毕竟 BASE是基于 CAP 提出的折中理论
CAP 理论中的 C 也就是我们常说的分布式系统中的一致性,更确切地说,指的是分布式一致性中的一种: 也就是前面讲的线性一致性(Linearizability),也叫做原子一致性(Atomic consistency)。
CAP 理论也是个被滥用的词汇,关于 CAP 的正确定义可参考cap faq。很多时候我们会用 CAP 模型去评估一个分布式系统,但这篇文章会告诉你 CAP 理论的局限性,因为按照 CAP 理论,很多系统包括 MongoDB,ZooKeeper 既不满足一致性(线性一致性),也不满足可用性(任意一个工作中的节点都要可以处理请求),但这并不意味着它们不是优秀的系统,而是 CAP 定理本身的局限性(没有考虑处理延迟,容错等)。
正因为 CAP 中的一致性和可用性是强一致性和高可用,后来又有人基于 CAP 理论 提出了BASE 理论,即基本可用(Basically Available)、软状态(Soft State)、最终一致性(Eventual Consistency)。BASE的核心思想是即使无法做到强一致性,但每个应用都可以根据自身的业务特点,采用适当的方法来使系统达到最终一致性。显然,最终一致性弱于 CAP 中的 线性一致性。很多分布式系统都是基于 BASE 中的”基本可用”和”最终一致性”来实现的,比如 MySQL/PostgreSQL Replication 异步复制。
ACID一致性是有关数据库规则,如果数据表结构定义一个字段值是唯一的,那么一致性系统将解决所有操作中导致这个字段值非唯一性的情况,如果带有一个外键的一行记录被删除,那么其外键相关记录也应该被删除,这就是ACID一致性的意思。
CAP理论的一致性是保证同样一个数据在所有不同服务器上的拷贝都是相同的,这是一种逻辑保证,而不是物理,因为光速限制,在不同服务器上这种复制是需要时间的,集群通过阻止客户端查看不同节点上还未同步的数据维持逻辑视图。
当跨分布式系统提供ACID时,这两个概念会混淆在一起,Google’s Spanner system能够提供分布式系统的ACID,其包含ACID+CAP设计,也就是两阶段提交 2PC+ 多副本同步机制(如 Paxos)
image
ACID 是处理事务的原则,限定了原子性、一致性、隔离性、持久性。ACID、CAP、BASE这些都只是理论,只是在实现时的目标或者折中,ACID 专注于分布式事务,CAP 和 BASE是分布式通用理论。
解决分布式事务时有 2pc、3pc、tcc 等方式,通过增加协调者来进行协商,里面也有最终一致的思想。
而Paxos协议与分布式事务并不是同一层面的东西,Paxos用于解决多个副本之间的一致性问题。比如日志同步,保证各个节点的日志一致性,选主的唯一性。简而言之,2PC用于保证多个数据分片上事务的原子性,Paxos协议用于保证同一个数据分片在多个副本的一致性,所以两者可以是互补的关系,不是替代关系。对于2PC协调者单点问题,可以利用Paxos协议解决,当协调者出问题时,选一个新的协调者继续提供服务。原理上Paxos和 2PC相似,但目的上是不同的。etcd 中也有事务的操作,比如迷你事务
关于 raft 的内容也可以看下MIT-6.824
共识、线性一致性与顺序一致性 - 简书
https://www.jianshu.com/p/59ea658a1313