Paxos Made Simple
朴素 paxos 算法介绍
前言 #
《Paxos Made Simple》原文的推导思路比较跳跃,初读会比较难以理解,每个词都能看懂可还是不知所云,我觉得这种阅读的重点应该放在作者的行文动机上,如果能够随时思考“为什么要引入这个命题或者设计上”,读起来就顺畅了,还会留下一些启发式的思考
因此本文的写作目的还是帮助理解原文,有关算法细节我会略写
之前的工作中,我感觉到读论文得到的思考,没能成体系的记录下来,边啃原文边用 obsidian 记笔记的方式还是效果不好
所以我开始用 zotero 做文献注解,并且有意识地略读,既能保留原文的脉络,又不需要另起炉灶写很多废话,分析这篇 Paxos Made Simple 就是第一次尝试的副产物了
一 | Paxos 要解决什么问题? #
计算机本质是一个状态机。分布式系统中,我们通过网络对操作(Command)序列达成共识,并在本地重放(Redo)操作序列,来完成状态同步
这里的 Command 序列是追加写入的,一旦完成写入就不会再修改
Paxos 只描述一轮共识的过程,一旦完成共识,就不会再改变,这有什么作用呢?
针对特定的一条 Command,我们建立一个 Paxos 实例完成共识,这就完成一轮写入,更进一步,我们对每一条 Command 都维护一个 Paxos 实例,这就完成了整个操作序列的同步,工程中使用的就是这种 Multi-Paxos 实现
本文描述的就是单轮共识的 Paxos 算法
二 | Paxos 算法流程 #
原文中的 提案 (Proposal),可以简单理解为上面的 command
我们分类出三种角色:
- Proposer:发起 proposal
- Acceptor:作为 proposal 共识的决策者
- Learner:被动学习已经达成共识的 proposal
每条 proposal 应该分配一个独有的序列号(下文用 Pid 代替)
设置 Pid 的目的是确定 proposal 间的全序关系,作为 Acceptor 处理 proposal 冲突的标准
Paxos 共识流程分三个阶段:
Phase-1 Prepare
- Proposer 向 Acceptor 集群广播
prepare req(pid=n) - Acceptor 收到请求后,承诺不再接受
pid < n的 message - Acceptor 将最近接受的一条 proposal 打包成
prepare ack,发送回 Proposer - Proposer 收到大多数 Acceptor 的
ack后,如果它们回复的 proposal 都为空,那么新 proposal 的值(command 内容)可以任意决定,否则,新 proposal 的值必须与 Acceptor 回复的最新的 proposal 值相同
可以认为 Prepare 阶段,就是提前为
pid=n的 proposal 占位,相当于加锁,但如果有更大的 Pid,还是会让出座位
Phase-2 Accept
- Proposer 根据 Phase-1 确定的值构造 proposal,广播到 Acceptor 集群
- Acceptor 会记录先前承诺的 Pid,决定是否接受该 proposal
- Proposer 收到大多数 Acceptor 的
accept ack回复后,认为完成共识,否则,proposal 会用更大的 Pid 重复 Phase-1
Phase-3 Learn
- Acceptor 接受一条 proposal 后(注意,不是达成共识),向所有 Learner 广播该信息,由 Learner 自行维护 Acceptor 集群的状态
- 如果 Learner 发现有一条 proposal 被大多数 Acceptor 接受过,那么就确定共识已经完成,将 proposal(Command) 应用到本地机器上的状态机,至此结束一轮共识
以上是朴素 paxos 的流程
三 | 为什么 Paxos 是正确的? #
原文从数学假设入手,一步步反推构造出 Paxos,从而保证了算法正确性。我不觉得这是原本的探索过程,本文应该是出于 “made simple” 的目的,才选择这种行文手法
与其思考“为什么 Paxos 是这样的”,我选择以 “为什么 Paxos 是正确的” 为切入点展开阅读,跳过本节不影响对 Paxos 本身的学习
本节是对 2.2 节思路的注释,原文提到的命题不做翻译
首先明确 Paxos 的目的:
- 适用于 异步、非拜占庭 模型
- 针对单轮共识,不涉及修改
- 由 Proposer 提出 command
- $(R1)$ 达成共识的 command 必须是唯一的
- $(R2)$ 一旦达成共识,command 的值不会改变
- $(R3)$ 达成共识的 command 会传播到整个集群中
为满足 $(R1)$,我们采用多数原则(或称 quorum、法定人数),正确性显然
设 $C_1,C_2$ 是两个由 Acceptor 中多数成员组成的集合,则二者必有共同成员 $x$
假设 $C_1,C_2$ 都达成共识,共识结果分别为 $v_1,v_2$ ,则 $v_1 = v_x = v_2$
故 不可能 分别对 两个不同的值 完成共识,即 共识值唯一,$(R1)$ 成立
Phase-2 中提到过,一个完成共识的 command 会通过 prepare ack 传递回 Proposer,并替代 proposal 内容,由此完成传播, $(R3)$ 成立
而 $(R2)$, 等价于原文中的 $P2$
$P2$. If a proposal with value v is chosen, then every higher-numbered proposal that is chosen has value v .
原文提出 P2 后的行文思路:
- 要满足共识条件,必须满足 P2
- P2 的充分条件是什么?
- 逐步推理出 P2 的充分条件 P2c,并证明
- 设计一个满足 P2c 的规则
- 这样我们就得到了 Paxos 算法
提出 $P2$ 后,我们考虑 ${P2}^a$,只对 Acceptor 做限制
${P2}^a$ . If a proposal with value v is chosen, then every higher-numbered proposal accepted by any acceptor has value v .
但这样一来,如果有新的 Acceptor 临时加入集群,无法获悉之前共识的上下文,或者说不知道其他人已经达成共识(缺失对运行环境的认知),就会错误接受另外的 command,这就违背了 ${P2^a}$
故我们进一步限制,提出 ${P2}^b$
${P2}^b$ If a proposal with value v is chosen, then every higher-numbered proposal issued by any proposer has value v .
易知 ${P2}^b$ 是 $P2$ 的充分条件,下面我们寻找 ${P2}^b$ 成立的充分条件
怎么寻找呢?我们采用 数学归纳法:
假设 $m < n$ ,$Pid = m$ 时原命题成立
设 $C$ 为达成共识的大多数 Acceptor 集合,则该假设的具体表述如下: Every acceptor in C has accepted a proposal with number in m . . (n − 1), and every proposal with number in m . . (n − 1) accepted by any acceptor has value v .
试寻找 $Pid = n$ 时同样成立的充分条件 ${P2}^c$
只要能找到 ${P2}^c$ ,我们就可以利用归纳法证明 ${P2}^b$ 成立,进而满足 $P2$
通过构造,我们找到了 ${P2}^c$
${P2}^c$. For any v and n, if a proposal with value v and number n is issued, then there is a set S consisting of a majority of acceptors such that either (a) no acceptor in S has accepted any proposal numbered less than n, or (b) v is the value of the highest-numbered proposal among all proposals numbered less than n accepted by the acceptors in S .
P2c 是归纳证明成立的前提,是作者先射箭后画靶,凭空构造出来的,只要我们能保证 P2c 成立,归纳证明就成立,P2b、P2就成立,共识算法就成立
这里的 S 实际就是先前提到的 majority 集群 C,(A) 对应初始所有 acceptor 为空的状态,(B) 对应归纳假设的条件,proposal 从 acceptor 获取 value,强制达成共识
这里借 P2c 说明 paxos 的运行机制
要发出一个 proposal,它的 value 来源只能有两个:
- 要么 acceptor 为空,自由取值
- 要么 acceptor 已经达成 majority 共识,强制给 proposal 赋值为共识结果,以同步到剩余的 acceptor
因为两个 majority 必有交集,故两个条件不可能同时成立,这就保证了 value 来源的一致性
那么我们只需要构造满足 ${P2}^c$ 的规则即可,这需要 Proposer 发出 proposal 时,就持有 Acceptor 中最大的 Pid 信息,从而判断是否应该发出该信息
然而,Proposer 方只能得知发出 proposal 之前,Acceptor 方的信息,如果发出后,有人抢先到达 Acceptor,这就导致信息不一致了,Proposer 是不可能 “预测未来” 的
既然不能避免插队问题,那么 Proposer 可以事先要求 Acceptor 做出 “不接受 Pid 更小的 proposal 插队“ 的承诺,这样就强行保证了信息一致
为什么这里会提到“预测未来”的想法呢?
因为 (A) “发送 proposal” 与 (B) “接受 proposal” 是两个不同的事件,存在逻辑时间差
而 value 是在 (A) 事件确定的,但在 (AB) 之间,可能有其他 proposal 到达,破坏了 (A) 成立的前提,造成混乱
所以我们才会引入一个 (C) “接收 prepare ack” 阶段, 通过 (C) 使 acceptor 做出 promise,从而保证 (AB) 间的因果关系
这里的 ”事先要求“,或者说 promise 机制,就是 Phase-1 的 Prepare 规则了
${P1}^a$ . An acceptor can accept a proposal numbered n iff it has not responded to a prepare request having a number greater than n.
加入这一条 promise 限制,就完成了整个 Prepare 机制过程,进而满足 ${P2}^c$ ,满足归纳证明 ${P2}^b$ 的过程,满足 $P2$ ,满足 $(R2)$,达到 Paxos 的设计目的
至此,完成 Paxos 的正确性推导
四 | Paxos 的优化 #
以上仅是理论上的朴素 Paxos 算法,要实现它,还需要进行性能优化
论文提到的优化:
-
指定 主 Proposer 与 主 Learner,简化通信复杂度,但会引入选主问题
-
Multi-Paxos 对指令序列进行共识,每条 command 对应一个 paxos 实例,在不同机器上的 状态机 进行重放
-
如果某条 command 共识失败了,那就用 no-op 填补空缺(注意 paxos 层只保证执行,至于其中的因果关系,由应用层进行保证)
-
我们设置一个 command 滑动窗口,这样就可以并发执行多个 paxos 实例
-
我们对 Phase-1 进行优化,一条 prepare 请求可以携带一个实例区间(而不是只针对单个 Paxos 实例)
这样 leader 运行期间只需要进行一次 Phase-1,不需要多次获取 promise 。这样此后的每一个 paxos 实例,都只需要执行一轮 Phase2,减少了通信量
-
而集群成员的变化,我们也使用状态机维护,但这应该是阻塞式的,如果处在变更阶段,那么就必须等到集群成员稳定之后再进行下面的共识
为了防止阻塞语义,我们结合滑动窗口对 confchange command 进行延迟应用,当且仅当这条 command (paxos 实例)离开滑动窗口时,才认为集群成员稳定,变更生效
在此之前,即使完成了共识,但只要没有离开滑动窗口,变更就不生效