Paxos 协议是少数在工程实践中证实的强一致性、高可用的去中心化分布式协议。
Paxos 协议的流程较为复杂,但其基本思想却不难理解,类似于人类社会的投票过程。Paxos 协议中,有一组完全对等的参与节点(称为 accpetor),这组节点各自就某一事件做出决议,如果某个决议获得了超过半数节点的同意则生效。Paxos 协议中只要有超过一半的节点正常,就可以工作,能很好对抗宕机、网络分化等异常情况。
Paxos 协议中,有三类节点:
Proposer:提案者。Proposer 可以有多个,Proposer 提出议案(value)。所谓 value,在工程中可以是任何操作,例如“修改某个变量的值为某个值”、“设置当前 primary 为某个节点”等等。Paxos协议中统一将这些操作抽象为 value。不同的 Proposer 可以提出不同的甚至矛盾的 value,例如某个Proposer 提议“将变量 X 设置为 1”,另一个 Proposer 提议“将变量 X 设置为 2”,但对同一轮 Paxos过程,最多只有一个 value 被批准。
Acceptor:批准者。Acceptor 有 N 个,Proposer 提出的 value 必须获得超过半数(N/2+1)的 Acceptor批准后才能通过。Acceptor 之间完全对等独立。
Learner:学习者。Learner 学习被批准的 value。所谓学习就是通过读取各个 Proposer 对 value的选择结果,如果某个 value 被超过半数 Proposer 通过,则 Learner 学习到了这个 value。回忆(2.4 )不难理解,这里类似 Quorum 机制,某个 value 需要获得 W=N/2 + 1 的 Acceptor 批准,从而学习者需要至少读取 N/2+1 个 Accpetor,至多读取 N 个 Acceptor 的结果后,能学习到一个通过的 value。
上述三类角色只是逻辑上的划分,实践中一个节点可以同时充当这三类角色。
Paxos 协议一轮一轮的进行,每轮都有一个编号。每轮 Paxos 协议可能会批准一个 value,也可能无法批准一个 value。如果某一轮 Paxos 协议批准了某个 value,则以后各轮 Paxos 只能批准这个value。++上述各轮协议流程组成了一个 Paxos 协议实例,即一次 Paxos 协议实例只能批准一个 value,这也是 Paxos 协议强一致性的重要体现。++
每轮 Paxos 协议分为阶段,++准备阶段和批准阶段++,在这两个阶段 Proposer 和 Acceptor 有各自的处理流程。
流程 2.8.1:Proposer 的流程
(准备阶段)
1. 向所有的 Acceptor 发送消息“Prepare(b)”; 这里 b 是 Paxos的轮数,每轮递增
2. 如果收到任何一个 Acceptor 发送的消息“Reject(B)”,则对于这个 Proposer 而言本轮 Paxos 失败,将轮数 b 设置为 B+1 后重新步骤 1;
(批准阶段,根据收到的 Acceptor 的消息作出不同选择)
3. 如果接收到的 Acceptor 的“Promise(b, v_i)”消息达到 N/2+1 个(N 为 Acceptor 总数,除法取整,下同);v_i 表示 Acceptor 最近一次在 i 轮批准过 value v。
3.1 如果收到的“Promise(b, v)”消息中,v 都为空,Proposer 选择一个 value v,向所有 Acceptor广播 Accept(b, v);
3.2 否则,在所有收到的“Promise(b, v _ i)”消息中,选择 i 最大的 value v,向所有 Acceptor 广播消息 Accept(b,v);
4. 如果收到 Nack(B),将轮数 b 设置为 B+1 后重新步骤 1;
流程 2.8.2:Accpetor 流程
(准备阶段)
1. 接受某个 Propeser 的消息 Prepare(b)。参数 B 是该 Acceptor 收到的最大 Paxos 轮数编号;V 是 Acceptor 批准的 value,可以为空
1.1 如果 b>B,回复 Promise(b, V_B),设置 B=b;表示保证不再接受编号小于 b 的提案。
1.2 否则,回复 Reject(B)。
(批准阶段)
2. 接收 Accept(b, v),
2.1 如果 b < B, 回复 Nack(B),暗示 proposer 有一个更大编号的提案被这个 Acceptor 接收了。
2.2 否则设置 V=v。表示这个 Acceptor 批准的 Value 是 v。广播 Accepted 消息。
基本例子里有 5 个 Acceptor,1 个 Proposer,不存在任何网络、宕机异常。我们着重考察各个Accpetor 上变量 B 和变量 V 的变化,及 Proposer 上变量 b 的变化。
初始状态
- | Acceptor 1 | Acceptor 2 | Acceptor 3 | Acceptor 4 | Acceptor 5 |
---|---|---|---|---|---|
B | 0 | 0 | 0 | 0 | 0 |
V | NULL | NULL | NULL | NULL | NULL |
- | Proposer 1 |
---|---|
b | 1 |
Proposer 向所有 Accpetor 发送“Prepare(1)”,所有 Acceptor 正确处理,并回复 Promise(1, NULL)
- | Acceptor 1 | Acceptor 2 | Acceptor 3 | Acceptor 4 | Acceptor 5 |
---|---|---|---|---|---|
B | 1 | 1 | 1 | 1 | 1 |
V | NULL | NULL | NULL | NULL | NULL |
- | Proposer 1 |
---|---|
b | 1 |
Proposer 收到 5 个 Promise(1, NULL),满足多余半数的 Promise 的 value 为空,此时发送Accept(1, v1 ),其中 v1 是 Proposer 选择的 Value。
- | Acceptor 1 | Acceptor 2 | Acceptor 3 | Acceptor 4 | Acceptor 5 |
---|---|---|---|---|---|
B | 1 | 1 | 1 | 1 | 1 |
V | v1 | v1 | v1 | v1 | v1 |
此时,v 1 被超过半数的 Acceptor 批准,v1 即是本次 Paxos 协议实例批准的 Value。如果 Learner学习 value,学到的只能是 v1
在同一个Paxos实例中,批准的Value是无法改变的,即使后续Proposer以更高的序号发起Paxos协议也无法改变 value。
例如,某次 Paxos 协议运行后,Acceptor 的状态是:
- | Acceptor 1 | Acceptor 2 | Acceptor 3 | Acceptor 4 | Acceptor 5 |
---|---|---|---|---|---|
B | 3 | 3 | 3 | 2 | 2 |
V | v1 | v1 | v1 | NULL | NULL |
5 个 Acceptor 中,有 3 个已经在第三轮 Paxos 协议批准了 v 1 作为 value。其他两个 Acceptor 的 V为空,这可能是因为 Proposer 与这两个 Acceptor 的网络中断或者这两个 Acceptor 宕机造成的。
此时,即使有新的 Proposer 发起协议,也无法改变结果。假设 Proposer 发送“prepare(4)消 息”,由于 4 大于所有的 Accpetor 的 B 值,所有收到 prepare 消息的 Acceptor 回复 promise 消息。但 前三个 Acceptor 只能回复 promise(4, v 1 _3),后两个 Acceptor 回复 promise(4, NULL)。
- | Acceptor 1 | Acceptor 2 | Acceptor 3 | Acceptor 4 | Acceptor 5 |
---|---|---|---|---|---|
B | 4 | 4 | 4 | 4 | 4 |
V | v1 | v1 | v1 | NULL | NULL |
此时,Proposer 可能收到若干个 Acceptor 发送的 promise 消息,没有收到的 promise 消息可能是网络异常造成的。无论如何,Proposer 要收到至少 3 个 Acceptor 的 promise 消息后才满足协议中大于半数的约束,才能发送accpet消息。这3个promise消息中,至少有1个消息是promise(4, v 1 _3),至多 3 个消息都是 promise(4,v 1 _3)。另一方面,Proposer 始终不可能收到 3 个 promise(4, NULL)消息,最多收到 2 个。综上,按协议流程,Proposer 发送的 accept 消息只能是“accept(4, v 1 )”而不能自由选择 value。
无论这个 accept 消息是否被各个 Acceptor 接收到,都无法改变 v 1 是被批准的 value 这一事实。即从全局看,有且只有 v 1 是满足超过多数 Acceptor 批准的 value。例如,假设 accept(4, v 1 )消息被Acceptor 1、Acceptor2、Acceptor4 收到,那么状态变为:
- | Acceptor 1 | Acceptor 2 | Acceptor 3 | Acceptor 4 | Acceptor 5 |
---|---|---|---|---|---|
B | 4 | 4 | 4 | 4 | 4 |
V | v1 | v1 | v1 | v1 | NULL |
从这个例子我们可以看到一旦一个 value 被批准,此后永远只能批准这个 value。
Paxos 协议的核心就在与“批准的 value 无法改变”,这也是整个协议正确性的基础,为了更好的理解后续对 Paxos 协议的证明。这里再看一种看似可能,实际违反协议的状态,这种状态也是后续反证法证明协议时的一种错误状态。
- | Acceptor 1 | Acceptor 2 | Acceptor 3 | Acceptor 4 | Acceptor 5 |
---|---|---|---|---|---|
B | 1 | 1 | 1 | 1 | 1 |
V | v1 | v1 | v1 | v2 | v2 |
上述状态中,3 个轮次为 1 的 Acceptor 的 value 为 v 1 ,2 个轮次更高的 Acceptor 的 value 为 v 1 。此时被批准的 value 是 v 1 。
假设此时发生新的一轮 b=3 的 Paxos 过程,Proposer 有可能收到 Acceptor 3、4、5 发出的 3 个promise 消息分别为“promise(1, v 1 _1)”,“promise(2, v 2 _2)” “promise(2, v 2 _2)”。按协议,proposer选择 value 编号最大的 promise 消息,即 v 2 _2 的 promise 消息,发送消息“Accept(3, v 2 )”,从而使得最终的批准的 value 成为 v 2 。就使得批准的 value 从 v 1 变成了 v 2 。
上述假设看似正确,其实不可能发生。这是因为本节中给出的初始状态就是不可能出现的。这是因为,要到达成上述状态,发起 prepare(2)消息的 proposer 一定成功的向 Acceptor 4、Acceptor 5发送了 accept(2, v 2 )。但发送 accept(2, v 2 )的前提只能是 proposer 收到了 3 个“promise(2, NULL)”消息。 然而,从状态我们知道,在 b=1 的那轮 Paxos 协议里,已经有 3 个 Acceptor 批准了 v 1 ,这 3 个Acceptor 在 b=2 时发出的消息只能是 promise(2,v 1_ 1),从而造成 proposer 不可能收到 3 个“promise(2,NULL)”,至多只能收到 2 个“promise(2, NULL)”。另外,只要 proposer 收到一个“promise(2,v 1 _1)”,其发送的 accept 消息只能是 accept(2, v 1 )。
从这个例子我们看到 Prepare 流程中的第 3 步是协议中最为关键的一步,它的存在严格约束了“批准的 value 无法改变”这一事实。在后续协议推导中我们将看到这一步是如何被设计出来的。
这里给一个较为复杂的异常状态下Paxos 运行实例。本例子中有 5 个Acceptor和 2 个 Proposer。
Proposer 1 发起第一轮 Paxos 协议,然而由于异常,只有 2 个 Acceptor 收到了 prepare(1)消息。
- | Acceptor 1 | Acceptor 2 | Acceptor 3 | Acceptor 4 | Acceptor 5 |
---|---|---|---|---|---|
B | 1 | 1 | 0 | 0 | 0 |
V | NULL | NULL | NULL | NULL | NULL |
Proposer 1 只收到 2 个 promise 消息,无法发起 accept 消息;此时,Proposer 2 发起第二轮 Paxos协议,由于异常,只有 Acceptor 1、3、4 处理了 prepare 消息,并发送 promise(2, NULL)消息
- | Acceptor 1 | Acceptor 2 | Acceptor 3 | Acceptor 4 | Acceptor 5 |
---|---|---|---|---|---|
B | 2 | 1 | 2 | 2 | 0 |
V | NULL | NULL | NULL | NULL | NULL |
Proposer 2 收到了 Acceptor 1、3、4 的 promise(2, NULL) 消息,满足协议超过半数的要求,选择了 value 为 v 1 ,广播了 accept(2, v 1 )的消息。由于异常,只有 Accptor 3、4 处理了这个消息。
- | Acceptor 1 | Acceptor 2 | Acceptor 3 | Acceptor 4 | Acceptor 5 |
---|---|---|---|---|---|
B | 2 | 1 | 2 | 2 | 0 |
V | NULL | NULL | v1 | v1 | NULL |
Proposer 1 以b=3发起新一轮的Paxos协议,由于异常,只有Acceptor 1、2、3、5处理了prepare(3)消息。
- | Acceptor 1 | Acceptor 2 | Acceptor 3 | Acceptor 4 | Acceptor 5 |
---|---|---|---|---|---|
B | 3 | 3 | 3 | 2 | 3 |
V | NULL | NULL | v1 | v1 | NULL |
由于异常,Proposer 1 只收到 Acceptor1、2、5 的 promise(3, NULL)的消息,符合协议要求,Proposer 1 选择 value 为 v 2 ,广播 accept(3, v 2 )消息。由于异常,这个消息只被 Acceptor 1、2 处理。
- | Acceptor 1 | Acceptor 2 | Acceptor 3 | Acceptor 4 | Acceptor 5 |
---|---|---|---|---|---|
B | 3 | 3 | 3 | 2 | 3 |
V | v2 | v2 | v1 | v1 | NULL |
当目前为止,没有任何value被超过半数的Acceptor批准,所以Paxos协议尚没有批准任何value。然而由于没有 3 个 NULL 的 Acceptor,此时能被批准的 value 只能是 v 1 或者 v 2 其中之一。
此时 Proposer 1 以 b=4 发起新的一轮 Paxos 协议,所有的 Acceptor 都处理了 prepare(4)消息。
- | Acceptor 1 | Acceptor 2 | Acceptor 3 | Acceptor 4 | Acceptor 5 |
---|---|---|---|---|---|
B | 4 | 4 | 4 | 4 | 4 |
V | v2 | v2 | v1 | v1 | NULL |
由于异常,Proposer 1 只收到了 Acceptor3 的 promise(4, v 1 _3)消息、Acceptor4 的 promise(4,v 1 _2)、Acceptor5 的 promise(4, NULL)消息,按协议要求,只能广播 accept(4, v 1 )消息。假设 Acceptor2、3、4 收到了 accept(4, v 1 )消息。由于批准 v 1 的 Acceptor 超过半数,最终批准的 value 为 v 1 。
- | Acceptor 1 | Acceptor 2 | Acceptor 3 | Acceptor 4 | Acceptor 5 |
---|---|---|---|---|---|
B | 4 | 4 | 4 | 4 | 4 |
V | v2 | v1 | v1 | v1 | NULL |
从前面的例子不难看出,Paxos 协议的过程类似于“占坑”,哪个 value 把超过半数的“坑”(Acceptor)占住了,哪个 value 就得到批准了。
这个过程也类似于单机系统并行系统的加锁过程。假如有这么单机系统:系统内有 5 个锁,有多个线程执行,每个线程需要获得 5 个锁中的任意 3 个才能执行后续操作,操作完成后释放占用的锁。我们知道,上述单机系统中一定会发生“死锁”。例如,3 个线程并发,第一个线程获得 2 个锁,第二个线程获得 2 个锁,第三个线程获得 1 个锁。此时任何一个线程都无法获得 3 个锁,也不会主动释放自己占用的锁,从而造成系统死锁。
但在 Paxos 协议过程中,虽然也存在着并发竞争,不会出现上述死锁。这是因为,Paxos 协议引入了轮数的概念,高轮数的 paxos 提案可以抢占低轮数的 paxos 提案。从而避免了死锁的发生。然而这种设计却引入了“活锁”的可能,即 Proposer 相互不断以更高的轮数提出议案,使得每轮 Paxos过程都无法最终完成,从而无法批准任何一个 value。
Proposer 1 以 b=1 提起议案,发送 prepare(1)消息,各 Acceptor 都正确处理,回应 promise(1,NULL)
- | Acceptor 1 | Acceptor 2 | Acceptor 3 | Acceptor 4 | Acceptor 5 |
---|---|---|---|---|---|
B | 1 | 1 | 1 | 1 | 1 |
V | NULL | NULL | NULL | NULL | NULL |
Proposer 2 以 b=2 提起议案,发送 prepare(2)消息,各 Acceptor 都正确处理,回应 promise(2,NULL)
- | Acceptor 1 | Acceptor 2 | Acceptor 3 | Acceptor 4 | Acceptor 5 |
---|---|---|---|---|---|
B | 2 | 2 | 2 | 2 | 2 |
V | NULL | NULL | NULL | NULL | NULL |
Proposer 1 收到 5 个 promise(1, NULL)消息,选择 value 为 v 1 发送 accept(1, v 1 )消息,然而这个消息被所有的 Acceptor 拒绝,收到 5 个 Nack(2)消息。
Proposer 1 以 b=3 提起议案,发送 prepare(3)消息,各 Acceptor 都正确处理,回应 promise(3,NULL)
- | Acceptor 1 | Acceptor 2 | Acceptor 3 | Acceptor 4 | Acceptor 5 |
---|---|---|---|---|---|
B | 3 | 3 | 3 | 3 | 3 |
V | NULL | NULL | NULL | NULL | NULL |
Proposer 2 收到 5 个 promise(2, NULL)消息,选择 value 为 v 2 发送 accept(2, v 2 )消息,然而这个消息被所有的 Acceptor 拒绝,收到 5 个 Nack(3)消息。
上述过程交替进行,则永远无法批准一个 value,从而形成 Paxos 协议活锁。Paxos 协议活锁问题也是这个协议的主要问题。
参考:《分布式系统原理介绍》 - 刘杰