想象你是一家创业公司的三位联合创始人之一。公司要定团建地点:你说爬山,她说去海边,另一位坚持露营。大家谁也说服不了谁,会议卡住,项目也跟着停摆。分布式系统也面临同样的困境:多台服务器必须对一个决定达成一致——该写哪个值?谁是主节点?哪个请求先执行?这就是分布式共识问题。 Paxos 就像一套开会规则:有人提出议题,大家表态,最终多数人支持的方案通过。但 Basic Paxos 有个限制:开一次会只能决定一件事。真实系统每秒要决定成千上万个操作,总不能每次都重新开会吧? 于是 Multi-Paxos 登场:会议选出一个固定主持人,主持人掌握议程,后续议题不需要每次都重新走表决流程,直接一条条过,效率大幅提升。本文从一次表决开始,带你一步步看懂 Paxos 如何演进成支撑现代分布式存储的 Multi-Paxos。
学习 Multi-Paxos 是读懂 etcd、TiKV 等分布式存储的钥匙。掌握它,你能设计高可用集群的选主与日志复制方案,显著提升系统架构决策力。
可视化演示
展示Paxos两阶段协议(Prepare/Promise→Accept/Ack)、Multi-Paxos Leader选举与日志复制、Quorum容错分析,通过三模式交互演示分布式共识核心机制。
本节配有交互动效(进入视口后自动加载,约 3KB)。
Basic Paxos两阶段协议 阶段一Prepare/Promise获取多数Acceptor承诺 阶段二Accept/Ack完成值写入 Quorum = ⌊N/2⌋+1 两阶段确保安全性但消息开销大
共识问题引入
假设公司三个部门要定团建餐厅。行政说火锅,运营说日料,研发说烧烤。每个人都有自己的偏好,如果没有人让步,会议就僵住了。现实里大家会举手投票,看哪个方案获得多数派支持。分布式系统的共识问题,本质上就是这场“团建投票”——多台机器对一个值达成一致。
真实世界里,三台服务器同时收到不同的写请求,它们需要选出一个确定的值写入副本。网络可能延迟、节点可能宕机,根本没有一个“全局时钟”能保证所有人同时看到同一画面。于是科学家想到:不必让全票通过,只要一个超过半数的集体认可某个值,这个值就是共识。
多数派为什么行得通?因为任意两个多数派集合必有交集。只要两个集合都超过半数节点,它们必然共享至少一个节点。这个节点可以证明哪个值先被选定,从而避免两个不同的值同时成为共识。
这就是多数派原则的数学基础:任何两条不同的共识决议不可能同时获得法定多数,因为多数派必有交集,而交集节点记住了谁先谁后。一场会议,最终永远只能有一个赢家。
- 第一个多数集合
Q_1表示一个候选获得超过半数的节点支持,即|Q_1| > \frac{n}{2}。 - 第二个多数集合
Q_2是另一个可能同时存在的支持集合,同样超过半数。 - 交集必然存在两个多数集合必定重叠,即
Q_1 \cap Q_2 \neq \varnothing。这个交集节点就是防止“双雄并立”的关键。
基础Paxos协议
公司决定给会议立一套正式规则。规则分出三类角色:有人负责发起议题(提案者),有人负责举手投票并记住结果(接受者),有人负责旁听并把结果传达给其他人(学习者)。没有规则,会议就没法高效推进。
Paxos 协议中的三种角色正好对应这个比喻。提案者(Proposer)提出带编号的提案;接受者(Acceptor)对编号做出承诺并接受值;学习者(Learner)从多个接受者处学习最终选定的值。真正的工程实现里一台节点可以同时扮演多个角色,但逻辑上三者必须分开。
提案编号是这套规则中最关键的设计。编号必须单调递增且全局唯一。编号就像一个“优先级数字”:越晚提出的提案编号越大,在表决中越占优先权。这使得两个并发提案不会因为顺序问题互相踩踏,也让接受者能明确判断“我现在该听谁的”。
编号规则让会议永远有清晰先后:任何人发起新提案都必须使用比当前更大的编号,就像开会时“请按议题顺序发言”一样,提案者不需要争夺话筒,只需要自报更大的编号。
- 已有编号
n是接受者目前见过的最大的提案编号,代表当前会议的最高优先级。 - 新提案编号
n'是新到达的提案所携带的编号。 - 严格递增
n' > n保证后发的提案拥有更大的编号,从而获得更高优先级。这是 Paxos 对“后来者居上”的形式化表达。
第一阶段准备
拿到话筒的提案者没有直接宣布最终方案,而是先做一次摸底调查:“在场各位,你们之前有没有已经定过的结论?如果我现在提出一个更高编号的方案,你们愿意优先考虑吗?”这就是 Prepare 阶段的工作。
提案者广播 Prepare(n) 给所有接受者。接受者检查这个编号 n 是否大于自己承诺过的最大编号 promised_id。如果大于,接受者就更新承诺,并回复一个 Promise 消息,同时把自己已接受过的旧提案(编号和值)带上;如果编号不够大,则直接拒绝或忽略。
这个阶段的意义在于“锁定状态”:接受者承诺不再响应任何编号更小的 Prepare 请求。这样一来,后到的提案者能够确定,在他摸底之后不会再有人携带旧编号来搅局,他拿到了一个干净的协商窗口。
摸底调查是 Paxos 的灵魂:它让新提案者知道团队之前是否有既定结论,同时用编号锁住所有接受者的承诺。有了这一步,后面的“最终表决”才不会推翻别人已经达成的协议。
- 输入请求
\text{Prepare}(n)表示一个携带编号n的准备请求到达接受者。 - 检查编号
p_{max}是接受者已经承诺的编号上限;只有当n > p_{max}时才承诺新请求。 - 返回旧值
\text{Promise}(n_a, v_a)中,n_a和v_a是该接受者已经接受过的提案编号与值,必须返回给提案者。 - 拒绝分支
\text{Reject}表示编号不满足条件时拒绝响应,保护已承诺的约定不被破坏。
第二阶段接受
摸底完成后,提案者整理出最终方案。如果他发现之前有人接受过旧值,就必须沿用旧值,而不是另起炉灶;如果所有人都没有接受过任何值,他才能自由提出自己的方案。这就是接受(Accept)阶段。
提案者把编号 n 和值 v 打包成 Accept 请求广播给接受者。接受者发现编号 n 仍不小于自己的承诺编号,就接受这个值,并返回 Accepted 反馈。当提案者收到多数派的 Accepted 时,共识达成,值 v 被正式选定。
“限制性选择”是这个阶段最反直觉也最精妙的设计。它的目的只有一个:防止两个不同的提案者各自提出不同值,导致系统分裂。因为多数派必然相交,提案者 A 的多数派答案一定会被提案者 B 的多数派看到,B 必须延续 A 的结论。
限制性选择让共识有了“先来后到”的惯性:一旦某个值被多数接受,后续一切新提案都会自然而然地延续它。就像会议已经定了火锅,后来的议题安排就不该再推翻火锅店,而是围绕火锅店规划。
- 查询旧提案
\exists (n_p, v_p) \text{ has been accepted}判断是否有任何接受者已经接受过某个旧提案。 - 延续旧值如果存在,新提案必须选择
v_{prev},即沿用已经存在的结果。 - 自由提出如果没有任何旧值,提案者才能选择自己的
v_{new}。
Multi-Paxos演进
公司每天要开十几次会,如果每次会议都重新走一遍“举手确认流程”,效率低得可怕。于是团队推选了一位固定主持人,把议事规则一次性确认好,后续每个议题只要挨个过即可,流程开销大幅下降。
Basic Paxos 每次只能对一个值达成共识,真实系统却需要连续处理成千上万个写请求。Multi-Paxos 的核心思路是:先选举一个稳定的 Leader(主持人),让它在一次 Prepare 中获得一个足够大的编号;之后所有日志条目的共识都复用这个编号,只执行 Accept 阶段。
这个演进看似简单,实际上改变了 Paxos 的效用:Basic Paxos 是“开一次会,决定一件事”;Multi-Paxos 是“选一次主持人,连续过整个议程”。Leader 还负责分配日志槽位、决定提交顺序,让分布式系统真正具备连续处理请求的能力。
主持人机制的本质,是用一次高质量的准备换取了后续所有提案的速度。每一个新日志条目不需要再重新询问“大家愿意吗”,只需要直接说“按这个来”,因为 Leader 的权威已经在选主时确认过了。
- 全称量词
\forall i \in \mathbb{N}表示对所有的自然数提案序号i成立,也就是后续每一个日志条目。 - 第 i 个提案编号
n_i表示第i条日志对应的 Paxos 实例所使用的提案编号。 - 复用 Leader 编号
n_{leader}是 Leader 在一次 Prepare 中确定的编号;所有后续n_i都等于它,从而免去重复 Prepare。
日志复制机制
有了主持人,会议还需要一份清晰的纪要。主持人把每个议题标上序号:第 1 条、第 2 条……所有部门都按这份纪要的顺序执行工作。谁也不能说自己先执行第 3 条,因为纪要的顺序就是所有人的共识顺序。
Multi-Paxos 中的“纪要”就是日志。每个共识实例对应一个日志槽位(slot),槽位里放着一个值。多个节点通过 Multi-Paxos 对同一个槽位达成一致,就得到了相同的有序日志。后续每个节点把日志按顺序应用到自己的状态机上,就保证了副本数据完全一致。
槽位编号必须严格单调递增,因为日志的顺序就是提交顺序。数据库、配置中心等系统的操作语义都依赖这个顺序:先加订单再扣库存,和先扣库存再加订单,结果完全不同。Multi-Paxos 通过槽位把并发操作串行化,从而保证状态机的确定性。
日志槽位就是会议纪要的页码。所有部门都从第 1 页读到第 N 页,执行的顺序完全相同。这样即使某个节点中途宕机重启,只要日志还在,它就能从上次的位置继续,回到与其他节点一致的状态。
- 当前槽位
slot_i表示当前已分配的日志位置。 - 下一个槽位
slot_{i+1}是紧接着的下一条日志所在位置。 - 连续性
slot_{i+1} = slot_i + 1保证日志没有空洞,所有节点按相同顺序看到操作序列。
优化与对比
复盘整个流程:过去每个议题都要从 Prepare 开始,两个往返才能决定一个值;现在主持人选定时消耗一次 Prepare,后续 N 个议题每个只需要一个 Accept 往返。吞吐量大幅提升,这正是 Multi-Paxos 在工程中被广泛采用的原因。
与 Raft 对比,Raft 把 Multi-Paxos 的思路进一步“工程简化”:强约束的 Leader、任期编号、心跳续约,以及日志匹配机制。Raft 的 Leader 更接近“强主持人”模型——所有写入必须经过 Leader,Leader 定期心跳维持权威,因此实现起来比原始 Paxos 直观得多。
工程实践上,etcd、TiKV、CockroachDB 等系统都实现了类 Raft 协议;而 Google 的 Chubby、Spanner 则采用了更贴近 Multi-Paxos 的工程化方案。理解 Paxos 与 Multi-Paxos 的演进,能让你看懂这些系统底层的一致性内核设计。
一条公式概括了 Multi-Paxos 的全部收益:准备费用摊薄到整个日志序列上。正如会议主持人前期花一点时间定规则,后面所有议题都顺着规则高效推进,这正是分布式系统最需要的工程智慧。
- 总耗时
T_{total}表示让 N 条日志达成共识的总时间。 - 一次性准备
T_{prepare}是 Leader 选主时执行的一次 Prepare 耗时,全局只发生一次。 - N 次接受
N \times T_{accept}是后续 N 条提案各自的 Accept 耗时,线性累加。
AI 应用场景
分布式存储的一致性复制引擎
阶段:分布式一致性引擎设计
解决的问题:传统主从复制依赖单点 Master 决定写入顺序,Master 宕机时整个系统停写,或出现双主写冲突。Multi-Paxos 让多个副本通过日志槽位同步决定顺序,任何多数派存活就能继续服务。
具体案例:TiKV 的每个 Raft 组在写操作到达 Leader 后,将操作写入日志,通过心跳复制到多数节点,多数确认后提交并应用到状态机。这个流程正是 Multi-Paxos 思想的工程化体现。
配置中心的分布式协调
阶段:分布式协调与配置管理
解决的问题:微服务配置集中管理时,更新必须让所有节点拿到相同版本,否则不同节点行为不一致。Multi-Paxos 的全局有序日志提供了天然的版本顺序保证。
具体案例:etcd 作为 Kubernetes 的存储后端,每个配置键的变更都被包装成一个提案进入日志,各节点按槽位顺序应用。一次配置发布就是一次共识过程。
Python 演示
class PaxosNode: """模拟一个接受者:记录承诺编号与每个日志槽位的已接受值""" def __init__(self, name): self.name = name self.promised_id = 0 self.accepted = {} def prepare(self, nid): if nid > self.promised_id: self.promised_id = nid return "promise" return "reject" def accept(self, slot, nid, val): if nid >= self.promised_id: self.promised_id = nid self.accepted[slot] = (nid, val) return "ok" return "reject" def run_multi_paxos(nodes, leader_id, slot, value): chosen = value for node in nodes: if slot in node.accepted: chosen = node.accepted[slot][1] break acks = 0 for node in nodes: if node.accept(slot, leader_id, chosen) == "ok": acks += 1 if acks < 2: return None return chosen def demo(): nodes = [PaxosNode("n%d" % i) for i in range(3)] promises = sum(1 for n in nodes if n.prepare(500) == "promise") if promises < 2: print("Leader 选举失败") return print("Leader 编号 500 已确定,后续槽位只走 Accept") for slot, val in enumerate(["创建订单表", "写入订单ID=1", "更新库存"]): result = run_multi_paxos(nodes, 500, slot, val) print("槽位%d -> %s" % (slot, result)) if __name__ == "__main__": demo()
if nid > self.promised_id:只有更大的编号才值得承诺,防止旧请求干扰。self.accepted[slot] = (nid, val)记录某个日志槽位已接受的值,供后续提案参考。if slot in node.accepted:如果槽位上已有旧值,新提案必须沿用,这是限制性选择。node.accept(slot, leader_id, chosen)直接跳过 Prepare,用同一个 Leader 编号接受新值。sum(1 for n in nodes if n.prepare(500) == "promise")一次性选主,为后续所有槽位预热。if acks < 2:接受者未达到多数派(3 节点中至少 2 个)则本次提案失败。
常见误区
误区Paxos 是让所有节点都同意同一个值,所以每个节点都必须回复,少一个节点同意就不安全。
正解Paxos 只要获得多数派确认即可。因为任意两个多数派必有交集,即使一台节点宕机,其余多数派也能完成决议。就像三人团建餐,只要有两人同意火锅,第三人离线也不影响决定生效。
为何常见:现实对话中的“全体一致”思维惯性太强,容易把多数派规则误读成全票通过。
误区Basic Paxos 已经能处理很多值,只要多跑几次 Paxos 循环,系统就能连续写入。
正解Basic Paxos 每次只对一个“值”达成共识。连续处理多条请求需要为每个值创建独立实例,并把结果组织成有序日志——这正是 Multi-Paxos 引入 Leader 和日志槽位的原因。
为何常见:教程常省略“一个值”这个定语,读者误以为 Basic Paxos 天然自带日志能力。
误区编号大的提案有优先权,所以提案编号可以随便加,加到超大也没关系。
正解编号必须从系统初始状态开始增长,同时保持唯一且大于大多数节点已承诺的编号。实际工程使用“轮次号加节点 ID”或 64 位单调序列来避免碰撞和溢出。
为何常见:只看到“大编号优先”的表面规则,忽略了工程上编号的边界条件。
误区Multi-Paxos 省去了 Prepare,所以后续提案完全不需要 Prepare,任何时候都能直接 Accept。
正解准确说是把 Prepare 移到了选主阶段。新 Leader 先做一次 Prepare 获得编号承诺,随后在 Leader 稳定期内复用该编号。Leader 失效或编号过期时仍需重新 Prepare。
为何常见:把“省略”理解成“删除”,忘了它本质上是把一次准备提前并重复利用。
误区Raft 有 Leader 机制,Multi-Paxos 没有,因此 Raft 更先进、更强。
正解Paxos 本身不依赖 Leader;Multi-Paxos 为了效率才引入 Leader。Raft 把 Leader、任期和日志匹配设计得很强制,从而工程实现更简单,但两者在一致性保证上没有强弱之分。
为何常见:把“更易实现”当作“理论上更强”,混淆了工程可落地性和算法理论能力。
面试题
Q1. 在 Basic Paxos 中,Acceptor 最核心的职责是什么?
- A.主动发起提案并推动多数派决议
- B.对收到的编号做出承诺和接受回应← 正确答案
- C.记录日志并从旧日志中恢复状态
- D.选出新的 Leader 并维持心跳
答案:B。Acceptor 是“举手投票的人”,它只负责对编号做出承诺并接受值,不主动发起提案。A 是 Proposer 的职责,C 是状态机恢复逻辑,D 是 Multi-Paxos 中 Leader 选举相关的工程机制,都不是 Acceptor 的核心职责。
Q2. 3 台 Acceptors 的集群中,最多能容忍几台宕机仍能完成共识?
- A.0 台,必须全部存活才能共识
- B.1 台,因为 2 台就能构成多数派← 正确答案
- C.2 台,因为 3 台总能形成多数派
- D.必须至少 4 台才能容忍宕机
答案:B。3 台节点中多数派最少需要 2 台,所以最多允许 1 台宕机。这与“多数派=超过半数”的原则一致,也是容灾设计里常见的选择。
Q3. Acceptor 在 Prepare 阶段返回 Promise 时,为什么要带上自己已接受过的旧提案?
- A.让 Proposer 统计本轮投票总数
- B.让新提案沿用旧值避免双值并存← 正确答案
- C.让 Acceptor 自己学习最终决议结果
- D.让日志复制时保持命令顺序一致
答案:B。Promise 中携带旧提案(编号和值)是为了让新提案者知道之前是否有已接受的值。如果存在,新提案必须沿用旧值,否则两个多数派可能接受不同值,导致共识破坏。
Q4. 在 Prepare 阶段,Acceptor 需要遵守哪些规则?
- A.编号大于已承诺值的 Prepare 予以承诺← 正确答案
- B.编号小于已承诺值的 Prepare 直接拒绝← 正确答案
- C.已接受的值要全部丢弃重新开始
- D.承诺后回应自己已接受的提案信息← 正确答案
答案:ABD。A 和 D 是承诺的核心行为:只对更大的编号承诺并返回已接受信息。B 是保护机制,防止旧编号干扰。C 完全错误——已接受的值是限制性选择的基础,绝不能丢弃。
Q5. Multi-Paxos 跳过后续 Prepare 的基本前提是什么?
- A.集群所有节点同意不再发起新提案
- B.多数派选中一个长期稳定的 Leader← 正确答案
- C.网络延迟为零且消息绝对不丢失
- D.每个提案都使用不同的独立编号
答案:B。Multi-Paxos 之所以能跳过 Prepare,是因为 Leader 在选主时已经获得了所有接受者的编号承诺。只要 Leader 保持稳定,后续提案就能复用同一个编号直接 Accept。网络延迟归零和节点不提议都是不现实的假设。
Q6. Multi-Paxos 相比 Basic Paxos 的改进包括哪些?
- A.Leader 复用编号避免反复 Prepare← 正确答案
- B.将提案组织成有序的日志序列← 正确答案
- C.冲突后可无限重试不设上限
- D.明显减少单条命令网络往返次数← 正确答案
答案:ABD。Multi-Paxos 的核心改进是选主后复用编号、用日志槽位串联多个值,并把每条命令的往返从两次降到一次。C 错误,实际工程中重试有预算和上限,否则系统会陷入活锁。
Q7. 在 Multi-Paxos 中,如果一个日志槽位同时出现两个不同值的竞争,最终结果取决于什么?
- A.v2 必将取代 v1,因为并发冲突
- B.多数派接受后才写进该槽位← 正确答案
- C.该槽位一直空缺,系统死锁
- D.v1 和 v2 合并成一个新值
答案:B。槽位最终写入哪个值,只取决于哪个提案先被多数派接受。另一个值会被拒绝或成为未提交的孤儿日志,最终被覆盖或者被写入下一轮协商中。槽位不会死锁,两个值也不会合并。
Q8. Basic Paxos 的 Safety(安全性)属性最准确的说法是?
- A.任意两个多数派不会选出不同决议值← 正确答案
- B.每个提案编号必须递增且不能重用
- C.一旦某节点接受一个值就永不可变
- D.多数派与少数派决定能自动合并
答案:A。Safety 的本质是“唯一性”:任意两个多数派不可能选出不同的值。B 是算法实现的必要规则而非安全属性本身。C 错误,因为接受者可以在不同编号下改变自己接受的值。D 更是违背了多数派原则。
Q9. Basic Paxos 容易发生冲突,Multi-Paxos 用哪些手段缓解?
- A.选举长期 Leader 减少并发提名← 正确答案
- B.租约机制确保同时只有一个 Leader← 正确答案
- C.只在选主阶段做一次 Prepare← 正确答案
- D.所有节点每次提案都互相广播
答案:ABC。Multi-Paxos 通过选主和租约大幅减少并发冲突,并把 Prepare 收敛到选主阶段。D 是错误操作,会让每个提案都变成全广播,网络开销和冲突概率不降反升。
Q10. 极端情况下 Basic Paxos 可能出现活锁,Multi-Paxos 通常如何避免?
- A.增加 Acceptors 数量扩大多数派
- B.随机退避和 Leader 租约帮助稳定← 正确答案
- C.取消 Prepare 接受任意编号
- D.提案编号每次只递增一个
答案:B。活锁源于多个提案者持续用更大的编号抢跑,导致彼此的 Prepare 互相打断。随机退避让提案者在冲突后随机等待再重试,Leader 租约保证一段时间内只有一个提案者,从根本上消除并发抢占。