分布式理论系列(三)一致性算法:从 Paxos 到 Raft

961 阅读12分钟

Paxos 和 Raft 是目前分布式系统领域中两种非常著名的解决一致性问题的共识算法,两者都能解决分布式系统中的一致性问题,但是Paxos的实现与证明非常难以理解,Raft的实现比较简洁并且遵循人的直觉,它的出现就是为了解决 Paxos 难以理解并和难以实现的问题。

一、Paxos 算法

1. 什么是 Paxos 算法

Paxos 算法目的是让整个集群的结点对某个值的变更达成一致。Paxos 算法(强一致性算法)属于多数派——大多数的决定会成个整个集群的统一决定。任何一个点都可以提出要修改某个数据的提案,是否通过这个提案取决于这个集群中是否有超过半数的结点同意(所以 Paxos 算法需要集群中的结点是单数)

Paxos算法在分布式领域具有非常重要的地位,开源分布式锁组件GoogleChubby的作者MikeBurrows说过,这个世界上只有一种一致性算法,那就是Paxos算法,其他的算法都是残次品。

Paxos 有点类似我们之前说的 2PC、3PC,但是解决了他们俩的各种硬伤。该算法在很多大厂都得到了工程实践,比如阿里的 OceanBase 的分布式数据库,底层就是使用 Paxos 算法。再比如 Goole 的 chubby 分布式锁也是用的这个算法。可以改算法在分布式系统中的地位,甚至于,paxos 就是分布式一致性的代名词。

2. Paxos 的节点角色

上述三类角色只是逻辑上的划分,在工作实践中,一个节点可以同时充当这三类角色。

提案者(Proposer)

提案者发起一个提案,提案包含一个提案编号(Proposal ID:提案编号通常是全局唯一且自增的)和提案内容(Value),提案内容可以包含一条命令或者数据变更。

不同的 Proposer 可以提出不同的甚至矛盾的 value,比如某个 Proposer 提议“将变量 X 设置为 1”,另一个 Proposer 提议“将变量 X 设置为 2”,但对同一轮 Paxos 过程,最多只有一个 value 被批准。

接受者(Acceptor)

接受者为提案投票,接受或者拒绝提案。

注意:
在paxos算法中,由于有大多数的概念,Proposer 提出的 value 必须获得超过半数(N/2+1)的 Acceptor 批准后才能通过。所以接受者必须为奇数且大于等于3,否则无法判断投票结果。

学习者(Learner)

学习者既不发起提案,也不投票提案,但是会从接受者那里获取到提案通过的信息。

这里 Leaner 的流程就参考了 Quorum 议会机制,某个 value 需要获得 W=N/2 + 1 的 Acceptor 批准,Learner 需要至少读取 N/2+1 个 Accpetor,最多读取 N 个 Acceptor 的结果后,才能学习到一个通过的 value。

学习者的存在其实是为了减少整个提案投票过程中的参与节点,因为参与节点越多,算法收敛的越慢,集群写性能越差。当然,参与节点越多,也意味着算法可靠性越强,不会因为个别节点的问题影响整个结果。真实的应用场景,参与节点的多少由系统的设计容量决定,有些系统为了读性能,就是需要大量的节点,这个时候,为了防止写性能的大幅下降,就需要学习者的角色平衡。

3. Proposer 与 Acceptor 之间的交互

Paxos 中, Proposer 和 Acceptor 是算法核心角色,Paxos 描述的就是在一个由多个 Proposer 和多个 Acceptor 构成的系统中,如何让多个 Acceptor 针对 Proposer 提出的多种提案达成一致的过程,而 Learner 只是“学习”最终被批准的提案。

Proposer 与 Acceptor 之间的交互主要有 4 类消息通信,如下图:

这 4 类消息对应于 Paxos 算法的两个阶段 4 个过程。

4. Paxos 选举过程

选举过程可以分为两个部分,准备阶段和选举阶段,可以查看下面的时序图:

Phase 1 准备阶段

Proposer 生成全局唯一且递增的 ProposalID,向 Paxos 集群的所有机器发送 Prepare 请求,这里不携带 value,只携带 N 即 ProposalID。

Acceptor 收到 Prepare 请求后,判断收到的 ProposalID 是否比之前已响应的所有提案的 N 大,如果是,则:

    1. 在本地持久化 N,可记为 Max_N;
    1. 回复请求,并带上已经 Accept 的提案中 N 最大的 value,如果此时还没有已经 Accept 的提案,则返回 value 为空;
    1. 做出承诺,不会 Accept 任何小于 Max_N 的提案。

如果否,则不回复或者回复 Error。

Phase 2 选举阶段

为了方便描述,我们把 Phase 2 选举阶段继续拆分为 P2a、P2b 和 P2c。

P2a:Proposer 发送 Accept

经过一段时间后,Proposer 收集到一些 Prepare 回复,有下列几种情况:

  • 若回复数量 > 一半的 Acceptor 数量,且所有回复的 value 都为空时,则 Porposer 发出 accept 请求,并带上自己指定的 value。

  • 若回复数量 > 一半的 Acceptor 数量,且有的回复 value 不为空时,则 Porposer 发出 accept 请求,并带上回复中 ProposalID 最大的 value,作为自己的提案内容。

  • 若回复数量 <= 一半的 Acceptor 数量时,则尝试更新生成更大的 ProposalID,再转到准备阶段执行。

P2b:Acceptor 应答 Accept

Accpetor 收到 Accpet 请求 后,判断:

  • 若收到的 N >= Max_N(一般情况下是等于),则回复提交成功,并持久化 N 和 value;

  • 若收到的 N < Max_N,则不回复或者回复提交失败。

P2c: Proposer 统计投票

经过一段时间后,Proposer 会收集到一些 Accept 回复提交成功的情况,比如:

  • 当回复数量 > 一半的 Acceptor 数量时,则表示提交 value 成功,此时可以发一个广播给所有的 Proposer、Learner,通知它们已 commit 的 value;

  • 当回复数量 <= 一半的 Acceptor 数量时,则尝试更新生成更大的 ProposalID,转到准备阶段执行。

  • 当收到一条提交失败的回复时,则尝试更新生成更大的 ProposalID,也会转到准备阶段执行。

5. 图文解释paxos算法的流程

我们假设有两个提案者、三个接受者、三个学习者。如下图:

Phase 1 准备阶段

在第一阶段,两个提案者都可以发起提案,但是由于网络原因,总有先来后到之分。
我们假设提案者1发起了提案号为1的提案,提案内容是把a的值改为3。而提案者2发起了提案号为2的提案,提案内容是把a的值改为5。如下图:

可以看到,提案者1的请求先到了接受者A和B那里,而提案者2的请求先到了接受者C那里。
接受者如果此前没有接受过任何提案,就会接受它收到的第一个提案,因此接受者A和B会接受提案1,而接受者C会接受提案2。
一旦接受,接受者就会记录当次提案的信息(相当于两阶段提交中的事务日志),并返回给提案者自己接受的情况,而且会带上自己上一次接受的情况,如下图:

图中红色是接受者当前记录的提案id和提案内容,注意,此时a的值仍然为初始值1,并没有改变。然后注意返回信息里带着lastId为null,表示接受者之前没有接受过任何提案。
然后就是paxos算法比较核心的几段逻辑,即两个提案者冲突的情况,如下图:

可以看到,由于提案者2的提案Id更大,因此接受者A和B立马倒戈向提案者2,将自己记录的提案换掉。
同时,接受者C由于已经记录了提案者2的提案,提案者1的提案id太小了,接受者C会果断拒绝提案者1。于是三个接受者返回的情况如下图:

至此,两位提案者都收到了三个接受者的反馈,投票结束。

具体投票结果如下:

提案者1意识到投票结果如下:
接受者A和B同意自己的提案,接受者C拒绝了自己的提案,并且接受者A和B此前没有同意过别人的提案。自己的提案已获得半数以上接受者同意,准备发起提交。

提案者2意识到投票结果如下:
接受者ABC都同意了自己的提案,其中接受者C此前没有同意过别人的提案,但是接受者AB已经同意了1号提案。注意,接下来非常关键的一点:为了使得系统尽快达成共识,提案者2意识到已经有半数以上接受者同意了提案1,于是自己放弃提案2,提案者2将会发起提交,但是会修改提案内容为提案1的内容。

注意
提案者可能没能收到接受者的反馈很长时间,或者被多数接受者拒绝了,此时提案者应当重新生成新的提案号,并反复尝试。

Phase 2 选举阶段

下面就进入到第二个阶段,提交阶段,该阶段比较简单,提案者按照自己的情况决定发起提交。
提案者1发起的是自己提案1的提交,内容是让a变成3。
提案者2发起的是自己提案2的提交,但是内容也是让a变成3。
由于接受者ABC已经记录提案2是自己接受的最大提案,因此提案者1会被拒绝,提案者2会成功。
如下图:

一旦提案提交,接下来就会由接受者将自己的提交情况发给所有的学习者,如下图:

为简略,只画了一个学习者的情况,学习者收到半数以上接受者的同步请求,就会将提案的变动更新。

6. paxos算法的几个关键点

paxos算法向来以难懂闻名,上述图文过程,估计看懂的人很少,下面再就一些关键点做解释:

  1. 一个最难懂的地方在于为什么明明提案者2胜出了,a的值却没有改成5而是改成了3。
    在paxos算法中,真正胜出的关键不是比谁的id大,而是比谁的请求到的早。虽然提案者1的id小,但是由于其请求最先到达半数以上接受者那里,所以最终a的值还是改成了3。提案者2虽然在投票环节胜出,但是为了能够尽快的达成一致,让所有接受者节点尽快收敛到同一个值上,提案者2还是会放弃自己的值,帮助提案者1去完成提案。注意:算法的关键不是加剧竞争,而是要减少竞争,增加协同和共识。

  2. Acceptor需要接受更大的N,也就是ProposalID有什么意义?
    这种机制可以防止其中一个 Proposer 崩溃宕机产生阻塞问题,允许其他 Proposer 用更大 ProposalID 来抢占临时的访问权。

  3. 如何产生唯一的编号,也就是 ProposalID?
    在《Paxos made simple》论文中提到,唯一编号是让所有的 Proposer 都从不相交的数据集合中进行选择,需要保证在不同Proposer之间不重复,比如系统有 5 个 Proposer,则可为每一个 Proposer 分配一个标识 j(0~4),那么每一个 Proposer 每次提出决议的编号可以为 5*i + j,i 可以用来表示提出议案的次数。

7. paxos算法与两阶段提交的不同之处

  1. 两阶段提交只有一个协调者,而paxos算法允许有多个提案者。即paxos算法不存在单点问题。

  2. paxos算法只需大多数同意即可,而两阶段提交要求所有的节点都同意才行。即paxos算法其实一致性要比两阶段提交弱,有那么一段时间,可能存在数据不一致的情况。但是,由于其他提案者一旦获知某个提案被多数派通过了,那么其他提案者就会尽力帮助这个提案获得整个集群的通过,因此最终,集群中所有的节点,都会通过该提案。

  3. paxos算法实际上也没法保证在提交阶段的可靠性,如果到了提交阶段,网络出现问题,总是有节点没有收到提交请求,实际上提案者也只能不断重试或者放弃提交。如果半数以上接受者回复ack,提案者其实就认为提案已经提交了。但是实际上,可能还是有少数节点,这一期间压根没有收到提交请求,数据还是旧的。数据的不一致性还是会存在的。

8. fast-paxos算法

由于paxos算法在提案者非常多,竞争激烈的时候,可能收敛很慢或者没法收敛,即大家赢得的接受者都一样多,导致只能不停地重试。有人提出了fast-paxos算法,其实就是先选主,选出一个leader来当提案者,这样就不存在竞争了,整个过程就像两阶段提交了。即使中途出现问题,集群选出两个主,也没关系,选出两个主就退化成了paxos算法典型的多提案者情况,只是收敛变慢而已。zookeeper的分布式一致算法其实就是借助此算法的思想实现的。

二、raft算法