【理论篇】浅析分布式中的 CAP、BASE、2PC、3PC、Paxos、Raft、ZAB
序
在常见的分布式系统中,总会发生诸如机器宕机或网络异常(包括消息的延迟、丢失、重复、乱序,还有网络分区)等情况。基于此,产生了适应各种场景的一致性算法,解决的问题就是如何在一个可能发生上述异常的分布式系统中,快速且正确地在集群内部对某个数据的值达成一致, 并且保证不论发生以上任何异常,都不会破坏整个系统的一致性。
由于涉及理论较多,本文借鉴了好多博主的文章,反复认真研读,在此特别感谢。
CAP
cap base理论借鉴了https://my.oschina.net/foodon/blog/372703。
2000年7月,加州大学伯克利分校的Eric Brewer教授在ACM PODC会议上提出CAP猜想。2年后,麻省理工学院的Seth Gilbert和Nancy Lynch从理论上证明了CAP。之后,CAP理论正式成为分布式计算领域的公认定理。
CAP理论
一个分布式系统最多只能同时满足一致性(Consistency)、可用性(Availability)和 分区容错性(Partition tolerance)这三项中的两项。
概念
一致性(Consistency)
一致性指“all nodes see the same data at the same time”,即更新操作成功并返回客户端完成后, 所有节点在同一时间的数据完全一致。
可用性(Availability)
可用性指“Reads and writes always succeed”,即服务一直可用,而且是正常响应时间。
分区容错性(Partition tolerance)
分区容错性指 “the system continues to operate despite arbitrary message loss or failure of part of the system”,即分布式系统在遇到某节点或网络分区故障的时候,仍然能够对外 提供满足一致性和可用性的服务。
CAP权衡
选择 | 说明 |
---|---|
CA | 放弃分区容错性,加强一致性和可用性,其实就是传统的单机数据库的选择 |
AP | 放弃一致性(这里说的一致性是强一致性),追求分区容错性和可用性,这是很多分布式系统设计时的选择,例如很多NoSQL系统就是如此 |
CP | 放弃可用性,追求一致性和分区容错性,基本不会选择,网络问题会直接让整个系统不可用 |
通过CAP理论,我们知道无法同时满足一致性、可用性和分区容错性这三个特性,那要舍弃哪个呢?
对于多数大型互联网应用的场景,主机众多、部署分散,而且现在的集群规模越来越大,所以节点故障、网络故障是常态,而且要保证服务可用性达到N个9, 即保证P和A,舍弃C(退而求其次保证最终一致性)。虽然某些地方会影响客户体验,但没达到造成用户流程的严重程度。
对于涉及到钱财这样不能有一丝让步的场景,C必须保证。网络发生故障宁可停止服务,这是保证CA,舍弃P。还有一种是保证CP,舍弃A。例如网络故障是只读不写。
孰优孰略,没有定论,只能根据场景定夺,适合的才是最好的。
BASE理论
eBay的架构师Dan Pritchett源于对大规模分布式系统的实践总结,在ACM上发表文章提出BASE理论,BASE理论是对CAP理论的延伸,核心思想是即使无法 做到强一致性(Strong Consistency,CAP的一致性就是强一致性),但应用可以采用适合的方式达到最终一致性(Eventual Consitency)。
BASE是指基本可用(Basically Available)、软状态( Soft State)、最终一致性( Eventual Consistency)。
概念
基本可用(Basically Available)
基本可用是指分布式系统在出现故障的时候,允许损失部分可用性,即保证核心可用。
电商大促时,为了应对访问量激增,部分用户可能会被引导到降级页面,服务层也可能只提供降级服务。这就是损失部分可用性的体现。
软状态( Soft State)
软状态是指允许系统存在中间状态,而该中间状态不会影响系统整体可用性。分布式存储中一般一份数据至少会有三个副本,允许不同节点间副本同步的延时 就是软状态的体现。mysql replication的异步复制也是一种体现。
最终一致性( Eventual Consistency)
最终一致性是指系统中的所有数据副本经过一定时间后,最终能够达到一致的状态。弱一致性和强一致性相反,最终一致性是弱一致性的一种特殊情况。
BASE与ACID
总的来说,BASE理论面向的是大型高可用可扩展的分布式系统,和传统的事物ACID特性是相反的,「它完全不同于ACID的强一致性模型,而是通过牺牲强一致性 来获得可用性,并允许数据在一段时间内是不一致的,但最终达到一致状态」。但同时,在实际的分布式场景中,不同业务单元和组件对数据一致性的要求是不同 的,因此在具体的分布式系统架构设计过程中,ACID特性和BASE理论往往又会结合在一起。
2PC
二阶段提交又称「2PC(two-phase commit protocol」),2pc是一个非常经典的「强一致、中心化的原子提交协议」。这里所说的中心化是指协议中有两 类节点:一个是中心化「协调者节点(coordinator)「和」N个参与者节点(partcipant)」,事务的提交过程分成了两个阶段来进行处理。
「2pc执行成功,参与者全部同意」
「2pc执行失败,参与者任意一个不同意都会失败」
阶段一:提交事务请求
事务管理器给每个参与者发送Prepare消息,每个数据库参与者在本地执行事务,并写本地的Undo/Redo
日志,此时事务没有提交。(Undo日志是记录修改前的数据,用于数据库回滚,Redo日志是记录修改后的数据,用于提交事务后写入数据文件)
阶段二:事务执行
如果事务管理器收到了参与者的执行失败或者超时消息时,直接给每个参与者发送回滚(Rollback)消息;否则,发送提交(Commit)消息;参与者根据事务管理器的指令执行提交或者回滚操作,并释放事务处理过程中使用的锁资源。
「注意:必须在最后阶段释放锁资源。」
优缺点
「优点」
原理简单,实现方便
「缺点」
-
单点服务: 若协调者突然崩溃则事务流程无法继续进行或者造成状态不一致 -
无法保证一致性: 若协调者第二阶段发送提交请求时崩溃,可能部分参与者受到COMMIT请求提交了事务,而另一部分参与者未受到请求而放弃事 务造成不一致现象。 -
阻塞: 为了保证事务完成提交,各参与者在完成第一阶段事务执行后必须锁定相关资源直到正式提交,影响系统的吞吐量。
参与者在完成阶段一的事务执行后等待协调者的下一个请求,若协调者超时则可以自行放弃事务。这种方案仍然有无法保证一致性的缺点,但并不会出现某些资料所述一直锁定资源,无法继续的情况。
3pc
3PC,全称 “three phase commit”,是 2PC 的改进版,将 2PC 的 “提交事务请求Prepare” 过程一分为二(CanCommit、PreCommit),共形成了由 CanCommit、PreCommit和doCommit三个阶段组成的事务处理协议。
基本流程
执行步骤
CanCommit
-
「协调者进行事务询问」 协调者向所有的参与者发送一个包含事务内容的CanCommit请求,询问是否可以执行事务提交操作,并开始等待 各参与者的响应。 -
「 参与者向协调者反馈事务询问」 参与者在接收到来自协调者的包含了事务内容的CanCommit请求后,正常情况下,如果自身认为可以顺利执行事 务,则反馈Yes响应, 并进入预备状态,否则反馈No响应。
PreCommit
协调者在得到所有参与者的响应之后,「参与者在CanCommit反馈的是Yes,执行事务预提交」:
-
「协调者发送预提交请求」(发出preCommit请求,并进入prepared阶段) -
「参与者进行事务预提交」(参与者接收到preCommit请求后,会执行事务操作,并将Undo和Redo信息记录到事务日志中。) -
「各参与者向协调者反馈事务执行的结果」(若参与者成功执行了事务操作,那么反馈Ack)
协调者在得到所有参与者的响应之后,「参与者在CanCommit反馈的是No,中断事务」:
-
「协调者发送中断请求:」(协调者向所有参与者发出abort请求。) -
「 中断事务」(无论是收到来自协调者的abort请求或者等待协调者请求过程中超时,参与者都会中断事务)
DoCommit
DoCommit阶段完成真正的事务提交或者完成事务回滚。
在第二阶段PreCommit阶段收到ACK确认消息,则完成事务提交:
-
「协调者发送提交DoCommit请求」(协调者将从预提交状态转化为提交状态,并向所有的参与者发送doCommit请求) -
「 参与者进行事务提交」(参与者接收到DoCommit请求后,会正式执行事务提交操作,并在完成提交之后释放整个事务执行过程中占用的事务资源。) -
「各个参与者向协调者反馈事务提交的结果」(若参与者成功完成事务提交,那么反馈Ack响应) -
「 完成事务」(协调者接收到所有参与者反馈的Ack消息后,完成事务。)
在第二阶段PreCommit阶段超时中断没有收到ACK确认消息,则完成事务中断:
-
「 协调者发送中断请求」(协调者向所有的参与者节点发送abort请求) -
「参与者进行事务回滚」(根据记录的Undo信息来执行事务回滚,并在完成回滚之后释放整个事务执行期间占用的资源) -
「各参与者向协调者反馈事务回滚的结果」(参与者在完成事务回滚后,向协调者发送Ack消息。) -
「中断事务」(协调者接收到所有参与者反馈的Ack消息后,中断事务。)
注意:在DoCommit阶段可能出现「协调者宕机」、「协调者与参与者出现网络故障」;「导致参与者接收不到协调者的DoCommit请求或Abort请求, 参与者会在请求超时后,继续进行事务提交」。
优缺点
「降低了阻塞」
-
参与者返回 CanCommit 请求的响应后,等待第二阶段指令,若等待超时,则自动 abort,降低了阻塞; -
参与者返回 PreCommit 请求的响应后,等待第三阶段指令,若等待超时,则自动 commit 事务,也降低了阻塞;
「解决单点故障问题」
-
参与者返回 CanCommit 请求的响应后,等待第二阶段指令,若协调者宕机,等待超时后自动 abort,; -
参与者返回 PreCommit 请求的响应后,等待第三阶段指令,若协调者宕机,等待超时后自动 commit 事务;
「数据不一致问题仍然是存在的」
比如第三阶段协调者发出了 abort 请求,然后有些参与者没有收到 abort,那么就会自动 commit,造成数据不一致。
paxos
「Paxos算法」是莱斯利·兰伯特(英语:Leslie Lamport,LaTeX中的“La”)于1990年提出的一种基于消息传递且具有高度容 错特性的共识(consensus)算法。需要注意的是,Paxos常被误称为“一致性算法”。但是“一致性(consistency)”和“共识 (consensus)”并不是同一个概念。Paxos是一个共识(consensus)算法。
算法流程
Paxos算法解决的问题是分布式共识性问题,即一个分布式系统中的各个进程如何就某个值(决议)通过共识达成一致。
Paxos算法运行在允许宕机故障的异步系统中,不要求可靠的消息传递,可容忍消息丢失、延迟、乱序以及重复。它利用大多数 (Majority) 机制保证了2F+1的容错能力,即2F+1个节点的系统最多允许F个节点同时出现故障。
一个或多个提议进程 (Proposer) 可以发起提案 (Proposal),Paxos算法使所有提案中的某一个提案,在所有进程中达成一致。系统中的多数派同时认可该提案,即达成了一致。最多只针对一个确定的提案达成一致。
Paxos将系统中的角色分为提议者 (Proposer),决策者 (Acceptor),和最终决策学习者 (Learner):
-
「Proposer」: 提出提案 (Proposal)。Proposal信息包括提案编号 (Proposal ID) 和提议的值 (Value)。 -
「Acceptor」:参与决策,回应Proposers的提案。收到Proposal后可以接受提案,若Proposal获得多数Acceptors的接受,则称该Proposal被批准。 -
「Learner」:不参与决策,从Proposers/Acceptors学习最新达成一致的提案(Value)。
在具体的实现中,一个进程可能 同时充当多种角色 。比如一个进程可能 既是Proposer又是Acceptor又是Learner 。Proposer负责提出提案, Acceptor负责对提案作出裁决(accept与否),learner负责学习提案结果。
还有一个很重要的概念叫「提案」(Proposal)。最终要达成一致的value就在提案里。只要Proposer发的提案被Acceptor接受( 半数以上的Acceptor同意才行),Proposer就认为该提案里的value被「选定」了。Acceptor告诉Learner哪个value被选定, Learner就认为那个value被「选定」。只要Acceptor接受了某个提案,Acceptor就任务该提案里的value被「选定」了。
「为了避免单点故障,会有一个Acceptor集合,Proposer向Acceptor集合发送提案,Acceptor集合中的每个成员都有可能同意该提案且每个Acceptor 只能批准一个提案,只有当一半以上的成员同意了一个提案,就认为该提案被选定了。」
「拜占庭问题」
拜占庭将军问题
:是指 拜占庭帝国军队的将军们必须全体一致的决定是否攻击某一支敌军。问题是这些将军在地理上是分隔开来的, 只能依靠通讯员进行传递命令,但是通讯员中存在叛徒,「它们可以篡改消息」,叛徒可以欺骗某些将军采取进攻行动;促成一个不是所有将军都同意的决定, 如当将军们不希望进攻时促成进攻行动;或者迷惑某些将军,使他们无法做出决定。
Paxos算法的前提假设是不存在拜占庭将军问题,即:****信道是安全的(信道可靠),发出的信号不会被篡改,因为Paxos算法是基于消息传递的****。
从理论上来说,在分布式计算领域,试图在异步系统和不可靠信道上来达到一致性状态是不可能的。因此在对一致性的研究过程中,都往往假设信道是可靠的, 而事实上,大多数系统都是部署在一个局域网中,因此消息被篡改的情况很罕见;另一方面,由于硬件和网络原因而造成的消息不完整问题,只需要一套简单 的校验算法即可。因此,在实际工程中,可以假设所有的消息都是完整的,也就是没有被篡。
协议过程
Paxos在原作者的《Paxos Made Simple》中内容是比较精简的:
Phase 1
(a) A proposer selects a proposal number n and sends a prepare request with number n to a majority of acceptors.
(b) If an acceptor receives a prepare request with number n greater than that of any prepare request to which it has already responded, then it responds to the request with a promise not to accept any more proposals numbered less than n and with the highest-numbered pro-posal (if any) that it has accepted.
Phase 2
(a) If the proposer receives a response to its prepare requests (numbered n) from a majority of acceptors, then it sends an accept request to each of those acceptors for a proposal numbered n with a value v , where v is the value of the highest-numbered proposal among the responses, or is any value if the responses reported no proposals.
(b) If an acceptor receives an accept request for a proposal numbered n, it accepts the proposal unless it has already responded to a prepare request having a number greater than n.
阶段 1.
(a) 一个 proposer 选择一个提议编号 *n*,然后将提议编号 *n* 放入 *prepare* request 中并发送给 acceptors 中的多数派。
(b) 如果一个 accptor 接受了一个 *prepare* request,其中的数字 *n* 大于这个 acceptor 已经回复过的所有 *prepare* request,那么他会回复这个 request,同时承诺不接受比 n 小的提议编号,而且会返回它已经接受过的提议中编号最大的那个提议(如果有的话)。
阶段 2
(a) 如果这个 proposer 收到了来自多数 acceptors 对 *prepare* requests (编号 n) 的回复,那么他会对这些 acceptors 分别发送一个 *accept* request,其中包含提议编号 *n* 和一个值 *v*。*v* 是响应中的提议中编号最大的那个提议的值,如果响应中没有提议,那 *v* 可以是任何值。
(b) 如果一个 acceptor 收到了一个带有提议编号 n 的 accept request,除非它已经回应过一个有着比 n 更大的编号的 prepare request ,它会接受这个提议。
举例
此例子 来源 https://ocavue.com/paxos.html#%E8%8A%82%E7%82%B9%E6%95%85%E9%9A%9C%E7%9A%84%E4%BE%8B%E5%AD%90,相对通俗易懂
想象一个用来卖票的分布式系统,就像 12306 那样。这个系统一共有五台机器,分布在不同的地点。为了讲解的方便,我们设定「这个系统只卖一张票」。五台机器有各自的数据库,用来储存买票者的名字。如果机器 A 认为把票卖给了我,而机器 B 认为把票卖给了你,那就糟糕了。于是我们要分布式系统的一致性,具体到这个例子就是保证不同机器中买票者的名字是同一个。
此时机器 D 收到了来自 Alice 的买票请求。于是 D 首先进入 Prepare 阶段:,
D 向其他 4 台机器发送了一条 「提议」(这里用***蓝色的箭头***表示提议):
其中 P-1D
表示:
-
这是一个提议(P for Prepare) -
提议的 ID 是 1D
每个提议都需要一个递增的全局唯一 ID,最简单的方法就是当前时间加上当前机器的名字。这个 ID 会贯穿整个 Paxos 流程。值得注意的是,在提议阶段,D 并没有把购买者的名字 Alice 告诉其他机器。
其他机器收到这个提议后,他们发现之前并没有收到过提议,于是同意了这份提议。具体来说是做了下面几件事:
-
将 P-1D
记录下来 -
承诺以后不再接受 ID < "1D"
的提议向 D 回复 OK,这里用***红色的箭头***表示对提议的回复
D 收到其他机器的回复后,发现加上自己的同意,发现已经有超过半数的机器同意将票卖给 Alice(事实上,所有五台机器都同意这点)。即然多数派已经同意了这份提议,那么 D 就认为认为这个提议已经被通过了,于是进入了 Commit 阶段。
在 Commit 阶段,D 向所有机器发出了一个「决议」(这里用***绿色的箭头***表示表示决议):
其中 A-1D-Alice
表示
-
这是一个决议(A for Accept) -
决议的 ID 是 1D
-
决议内容:将票卖给 Alice
其他四台机器到了这个决议后,就会把决议的内容记录下来,并返回给 D。D 最后把买票成功的消息发给 Alice,用图表示就是这样,这里用***紫色的箭头***表示对决议的回复:
此时,所有五台机器都认为票卖给了 Alice,一致性得到了保证。
上面的情况是所有机器和网络都能正常运行的理想情况,可是现实中总是不理想的,而分布式系统的最大价值之一就是能应对部分节点的故障,所以下面我们来模拟一下节点故障的情况。
# 节点故障的例子
我们假设有两台机器 B 和 E 发生了故障,那么重复一下上面的步骤,看看会发生什么。
第一步,D 接受到了 Alice 的请求,于是向其他机器「发送提议」:
第二步,除了发生故障的 B 和 E,其他机器都「回复了提议」:
第三步,D 认为提议得到了多数派(A、C、D)的认可,于是向大家「发送决议」
第四步,除了故障机外,其他机器都「回复了决议」。最后 D 向 Alice 发送购票成功的消息。
到目前为止,一切都 OK。由于只有少数的机器发生了故障,依然有一个多数派(3 台机器 > 5 台机器 / 2)存在,所以系统的运行没有受到影响。但是,如果我们这时候修好了 B 和 D,就会发现一个问题:B 和 D 就像刚苏醒的植物人,他们还以为这张票没卖出去呢!Paxos 算法如何解决这种情况呢?让我们继续这个例子:
↑ B 和 E 恢复工作了,但是他们此时没有
P-1D
和C-1D-Alice
的信息
假如这时候 Bob 来买票了,他向机器 B 发出了买票请求,于是 B 向其他机器发送了一个提议。
对于这个提议,E 的回复是 OK,但是 A、C 和 D 的回复是 "1B" < "1D", Fail
。因为在之前,A、C、D 已经承诺过了,他们不会接受 id < "1D"
的提议请求。
于是 B 只能放弃 1B
提议,而是紧接着又提出一个 ID 为 2B
提议,并将提议发送给其他机器:
然后机器其他机器都同意了这个提议,不过 A、C 和 D 在同意提议的同时,还返回了这样一条信息:「我已经把票卖给 Alice 了,你不可能通知我把票卖给其他人」:
B 收到了包括自己在内的五分针对 2B
提议的同意,所以 B 可以进入下一步,也就是发表决议了。但是知道了票已经被 Alice 拿走后, 由于卖掉的票是不能再拿回来的,所以 B 被迫修改决议的内容,也就是发表一个将票卖给 Alice 的决议,毕竟:
其他机器收到了这个决议后,也把这个决议写在自己的数据库中,并且将结果返回给 B。B 再通知 Bob 票已经被卖掉了:
在这个例子中,我们的分布式系统非常好地处理了节点故障的情况,最后达到的效果如下:
-
同一张票没有被卖给两个人。虽然 Bob 请求的机器 B 一开始并不知道票已经被 Alice 拿走了,但是最终 Bob 还是知道了。这就是最终一致性。 -
最终所有机器上都储存了相同的信息,也就是 P-2B
和C-2B-Alice
。
# 更加现实的情况
同时处理多个 paxos 实例:
在现实中,一个卖票系统不可能像上面的例子中只卖一张票。我们只要把每一张票当成一个独立的 paxos 算法流程,比如说在每个请求和响应中添加上票的唯一标示, 就能从逻辑上同时处理多张票的售卖。
票的转台
我们做的另一个假设是一张票只会被卖出一次,但是在现实中由于可以退票,一张票可以在退票后卖个另一位顾客。在 paxos 中,我们需要引入状态机的概念:简单来说就有一个 状态 经过一个 操作 后变成了另一个状态。
一张票被卖掉之后,它的状态由 可买 变成了 不可买,退票后其状态又重新变成了 可买。
火车票的售卖 和 银行账号的余额 都可以表示为这样的逻辑:
只要知道初始状态和所有的操作,根据状态机的逻辑就能算出当前的状态。我们可以为每一个状态制定一个 Paxos 算法实例,所有机器上使用 Paxos 算法同步了状态 1, 2, 3, .... 这样就能在所有机器上都保存相同的记录。
Paxos的死锁情况
“活锁”的根本原因在于两个proposer交替提案,避免“活锁”的方式为,如果一个proposer通过accpter返回的消息知道此时有更高编号的提案被提出时, 该proposer静默一段时间,而不是马上提出更高的方案,静默期长短为一个提案从提出到被接受的大概时间长度即可,静默期过后,proposer重新提案。系统中之所以要有主proposer的原因在于,如果每次数据更改都用paxos,那实在是太慢了,还是通过主节点下发请求这样来的快,因为省去了不必要的paxos时间。所以选择主proposer用paxos算法,因为选主的频率要比更改数据频率低太多。但是主proposer挂了咋整,整个集群就一直处于不可用状态,所以一般都用租约的 方式,如果proposer挂了,则租约会过期,其它proposer就可以再重新选主,如果不挂,则主proposer自己续租。
raft
Raft是由Stanford提出的一种更易理解的一致性算法,意在取代目前广为使用但难以理解的Paxos算法。目前,在各种主流语言中都有了一些开源实现。
原理
在Raft中,每个结点会处于下面三种状态中的一种:
-
「follower」:所有结点都以follower的状态开始。如果没收到leader消息则会变成candidate状态
-
「candidate」:会向其他节点“拉选票”,如果得到大部分的票则成为leader。这个过程就叫做
Leader选举(Leader Election)
-
「leader」: 所有对系统的修改都会先经过leader,每个修改都会写一条日志(log entry),leader收到修改请求后的过程如下,
-
复制日志到所有follower结点(replicate entry) -
大部分节点响应时才提交日志 -
通知所有follower节点日志已提交 -
所有follower也提交日志 -
现在整个系统处于一致的状态 这个过程叫做 日志复制(Log Replication)
。
Leader Election
当follower在选举超时时间(election timeout)内未收到leader的心跳消息(append entries),则变成candidate状态。「为了避免选举冲突,这个超时时间是一个150~300ms之间的随机数」。
成为candidate的结点发起新的选举期(election term)去“拉选票”:
-
重置自己的计时器 -
投自己一票 -
发送 「Request Vote消息」
如果接收结点在新term内没有投过票那它就会投给此candidate,并重置它自己的选举超时时间。candidate拉到大部分选票就会成为leader, 并定时发送心跳——「Append Entries消息」,去重置各个follower的计时器。当前Term会继续直到某个follower接收不到心跳并成为candidate。
如果不巧两个结点同时成为candidate都去“拉票”怎么办?
「这时会发生Splite Vote情况」。两个结点可能都拉到了同样多的选票,难分胜负, 选举失败,本term没有leader。之后又有计时器超时的follower会变成candidate,将term加一并开始新一轮的投票。
Log Replication
当发生改变时,leader会复制日志给follower结点,这也是通过Append Entries心跳消息完成的。前面已经列举了Log Replication的过程,这里就不重复了。
「脑裂问题:」 指在一个高可用(HA)系统中,当联系着的两个节点断开联系时,本来为一个整体的系统,分裂为两个独立节点,这时两个节点开始争抢共享资源, 结果会导致系统混乱,数据损坏。
Raft能够正确地处理网络分区(“脑裂”)问题。假设A~E五个结点,B是leader。如果发生“脑裂”,A、B成为一个子分区,C、D、E成为一个子分区。此时C、D、E会发生选举,选出C作为新term的leader。这样我们在两个子分区内就有了不同term的两个leader。这时如果有客户端写A时, 因为B无法复制日志到大部分follower所以日志处于uncommitted未提交状态。而同时另一个客户端对C的写操作却能够正确完成,因为C是新的leader, 它只知道D和E。
当网络通信恢复,B能够发送心跳给C、D、E了,却发现“改朝换代”了,「因为C的term值更大,所以B自动降格为follower。然后A和B都回滚未提交的日志,并从新leader那里复制最新的日志。」
举例论证
ZAB协议
ZAB(Zookeeper Atomic Broadcast) 协议是为分布式协调服务ZooKeeper专门设计的一种支持崩溃恢复的一致性协议。基于该协议,ZooKeeper 实现了一种 主从模式的系统架构来保持集群中各个副本之间的数据一致性。
ZAB协议只允许有一个主进程(即leader)接收客户端事务请求并进行处理。当leader收到事务请求后,将请求事务转化成事务proposal, 由于leader会为每个follower创建一个队列,将该事务proposal放入响应队列,保证事务的顺序性。之后会在队列中顺序向其它节点广播该提案, follower收到后会将其以事务的形式写入到本地日志中,并且向leader发送Ack信息确认,当有一半以上的follower返回Ack信息时, leader会提交该提案并且向其它节点发送commit信息。
相关知识参考了 https://segmentfault.com/a/1190000037550497 文章。
概念
三种角色
「Leader」 :负责整个Zookeeper 集群工作机制中的核心,主要工作有以下两个:
-
事务请求的唯一调度和处理者,保证集群事务处理的顺序性 -
集群内部各服务器的调度者
「Follower」 :它是 Leader 的追随者,其主要工作如下:
-
处理客户端的非实物请求,转发事务请求给 Leader 服务器 -
参与事务请求 Proposal 的投票 -
参与 Leader 选举投票
「Observer」 :是 zookeeper 自 3.3.0 开始引入的一个角色,它不参与事务请求 Proposal 的投票,也不参与 Leader 选举投票,只提供非事务的服务(查询),通常在不影响集群事务处理能力的前提下提升集群的非事务处理能力。
三种状态
在 ZAB 协议中定义:通过自身的状态来区分自己的角色的,在运行期间各个进程可能出现以下三种状态之一:
-
「LOOKING」:处在这个状态时,会进入 Leader 选举状态 -
「FOLLOWER」:Follower 服务器和 Leader 服务器保持同步时的状态 -
「LEADING」:Leader 服务器作为主进程领导者的状态
在组成 ZAB 协议的所有进程启动的时候,初始化状态都是 LOOKING 状态,此时进程组中不存在 Leader,选举之后才有,在进行选举成功后,就进入消息广播模式
(后文介绍),此时 Zookeeper 集群中的角色状态就不再是 LOOKING 状态。
ZXID
zookeeper 消息有严格的因果关系,因此必须将每一个事务请求按照先后顺序来进行排序与处理。那 Zookeeper 是如何保持请求处理的顺序的呢?其中非常关键的点就是 ZXID。
那 ZXID 究竟是怎么发挥作用的呢?
Leader 服务器在接收到事务请求后,会为每个事务请求生成对应的 Proposal 来进行广播,并且在广播事务 Proposal 之前,Leader 服务器会首先为这个事务 Proposal 分配一个全局单调递增的唯一 ID ,我们称之为事务 ID(即 ZXID)。
ZXID 的设计也很有特点,是一个全局有序的 64 位的数字,可以分为两个部分:
-
高 32 位是:epoch(纪元),代表着周期,每当选举产生一个新的 Leader 服务器时就会取出其本地日志中最大事务的 ZXID ,解析出 epoch(纪元)值操作加 1作为新的 epoch ,并将低 32 位置零。 -
低 32 位是:counter(计数器),它是一个简单的单调递增的计数器,针对客户端的每个事务请求都会进行加 1 操作;
这里低 32 位 counter(计数器)单调递增还好理解,高 32 位 epoch(纪元)每次选举加 1 也许有些同学就有疑问了,为什么 epoch(纪元)每次选需要举加 1 ,它在整个 ZAB 协议中有什么作用?
我们知道每当选举产生一个新的 Leader 服务器时生成一个新的 epoch(纪元)值,而在前文我们知道,服务运行过程中触发选举 Leader 的条件是:「Leader 服务器的出现网络中断、奔溃退出、重启等异常情况,或者当集群中半数的服务器与该 Leader 服务器无法通信时」。
这说明整个 Zookeeper 集群此时处于一个异常的情况下,而在发生异常前,消息广播进行到哪一步骤我们根本不知道,集群中的其他 Follower 节点从这种崩溃恢复状态重新选举出 Leader 后,如果老 Leader 又恢复了连接进入集群。此时老 Leader 的 epoch 肯定会小于新 Leader 的 epoch,这时就将老 Leader 变成 Follower,对新的 Leader 进行数据同步。即便这时老 Leader 对其他的 Follower 节点发送了请求,Follower 节点也会比较 ZXID 的值,因为高 32 位加 1 了, Follower 的 epoch(纪元)大于老 Leader 的 epoch(纪元),所以 Follower 会忽略这个请求。
俩种模式
ZAB 协议的包括两种模式:「崩溃恢复」、「消息广播」。
在进入「奔溃恢复」模式时 Zookeeper 集群会进行 Leader 选举,一般有两种情况会发生选举:
-
当服务器启动时期会进行 Leader 选举。 -
当服务器运行期 Leader 服务器的出现网络中断、奔溃退出、重启等异常情况,或者当集群中半数的服务器与该 Leader 服务器无法通信时,进入崩溃恢复模式,开始 Leader 选举。
选举出 Leader 服务器后,会进入「消息广播模式」,开始接收处理客户端的请求。
消息广播模式
-
Leader 服务器接收到请求后在进行广播事务 Proposal 之前会为这个事务分配一个 ZXID,再进行广播。 -
Leader 服务器会为每个 Follower 服务器都各自分配一个单独的队列,然后将需要广播的事务 Proposal 依次放入这些队列中去,并根据 FIFO 策略进行消息的发送。 -
每个Follower 服务器在接收到后都会将其以事务日志的形式写入到本地磁盘中,并且在成功写入后返回 Leader 服务器一个 ACK 响应。 -
当有超过半数的服务器 ACK 响应后,Leader 就会广播一个 Commit 消息给所有的 Follower 服务器,Follower 接收到后就完成对事务的提交操作。
整个过程类似一个二阶段提交的过程,但却有所不同,ZAB 协议简化了二阶段提交模型,在超过半数的 Follower 服务器已经反馈 ACK 之后就开始提交事务 Prososal 了,无需等待所有服务器响应。
崩溃恢复模式
上文中提到过崩溃恢复是在网络中断、奔溃退出、重启等异常情况下触发,流程如下
小结
在高并发分布式场景下,为了保证数据的一致性,诞生了各种各样的理论,都有着各自的应用场景,并且是个逐步发展,百家齐放的过程。cap,base理论反映了当下分布式存在的各种场景,2pc,3pc,paxos,raft,zab都是常见的一致性协议,期望本文让你有个初步的认识,要想深入理解还要靠 自己多多实践验证。
本文是一篇理论总结篇,后续文章会实例举证哪些组件使用了这些理论,敬请期待。
往期推荐
欢迎点赞和转发,感谢阅读!!!