想象你是一家创业公司的三位联合创始人之一。公司要定团建地点:你说爬山,她说去海边,另一位坚持露营。大家谁也说服不了谁,会议卡住,项目也跟着停摆。分布式系统也面临同样的困境:多台服务器必须对一个决定达成一致——该写哪个值?谁是主节点?哪个请求先执行?这就是分布式共识问题。 Paxos 就像一套开会规则:有人提出议题,大家表态,最终多数人支持的方案通过。但 Basic Paxos 有个限制:开一次会只能决定一件事。真实系统每秒要决定成千上万个操作,总不能每次都重新开会吧? 于是 Multi-Paxos 登场:会议选出一个固定主持人,主持人掌握议程,后续议题不需要每次都重新走表决流程,直接一条条过,效率大幅提升。本文从一次表决开始,带你一步步看懂 Paxos 如何演进成支撑现代分布式存储的 Multi-Paxos。

岗位关联

学习 Multi-Paxos 是读懂 etcd、TiKV 等分布式存储的钥匙。掌握它,你能设计高可用集群的选主与日志复制方案,显著提升系统架构决策力。

目录
  1. 可视化演示
  2. 共识问题引入
  3. 基础Paxos协议
  4. 第一阶段准备
  5. 第二阶段接受
  6. Multi-Paxos演进
  7. 日志复制机制
  8. 优化与对比
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

展示Paxos两阶段协议(Prepare/Promise→Accept/Ack)、Multi-Paxos Leader选举与日志复制、Quorum容错分析,通过三模式交互演示分布式共识核心机制。

本节配有交互动效(进入视口后自动加载,约 3KB)。

Basic Paxos两阶段协议 阶段一Prepare/Promise获取多数Acceptor承诺 阶段二Accept/Ack完成值写入 Quorum = ⌊N/2⌋+1 两阶段确保安全性但消息开销大

共识问题引入

假设公司三个部门要定团建餐厅。行政说火锅,运营说日料,研发说烧烤。每个人都有自己的偏好,如果没有人让步,会议就僵住了。现实里大家会举手投票,看哪个方案获得多数派支持。分布式系统的共识问题,本质上就是这场“团建投票”——多台机器对一个值达成一致。

真实世界里,三台服务器同时收到不同的写请求,它们需要选出一个确定的值写入副本。网络可能延迟、节点可能宕机,根本没有一个“全局时钟”能保证所有人同时看到同一画面。于是科学家想到:不必让全票通过,只要一个超过半数的集体认可某个值,这个值就是共识。

多数派为什么行得通?因为任意两个多数派集合必有交集。只要两个集合都超过半数节点,它们必然共享至少一个节点。这个节点可以证明哪个值先被选定,从而避免两个不同的值同时成为共识。

这就是多数派原则的数学基础:任何两条不同的共识决议不可能同时获得法定多数,因为多数派必有交集,而交集节点记住了谁先谁后。一场会议,最终永远只能有一个赢家。

∣Q1∣>n2且∣Q2∣>n2⟹Q1∩Q2≠∅\displaystyle |Q_1| > \frac{n}{2} \quad \text{且} \quad |Q_2| > \frac{n}{2} \quad \Longrightarrow \quad Q_1 \cap Q_2 \neq \varnothing
  1. 第一个多数集合Q_1 表示一个候选获得超过半数的节点支持,即 |Q_1| > \frac{n}{2}。
  2. 第二个多数集合Q_2 是另一个可能同时存在的支持集合,同样超过半数。
  3. 交集必然存在两个多数集合必定重叠,即 Q_1 \cap Q_2 \neq \varnothing。这个交集节点就是防止“双雄并立”的关键。

基础Paxos协议

公司决定给会议立一套正式规则。规则分出三类角色:有人负责发起议题(提案者),有人负责举手投票并记住结果(接受者),有人负责旁听并把结果传达给其他人(学习者)。没有规则,会议就没法高效推进。

Paxos 协议中的三种角色正好对应这个比喻。提案者(Proposer)提出带编号的提案;接受者(Acceptor)对编号做出承诺并接受值;学习者(Learner)从多个接受者处学习最终选定的值。真正的工程实现里一台节点可以同时扮演多个角色,但逻辑上三者必须分开。

提案编号是这套规则中最关键的设计。编号必须单调递增且全局唯一。编号就像一个“优先级数字”:越晚提出的提案编号越大,在表决中越占优先权。这使得两个并发提案不会因为顺序问题互相踩踏,也让接受者能明确判断“我现在该听谁的”。

编号规则让会议永远有清晰先后:任何人发起新提案都必须使用比当前更大的编号,就像开会时“请按议题顺序发言”一样,提案者不需要争夺话筒,只需要自报更大的编号。

n′>n\displaystyle n' > n
  1. 已有编号n 是接受者目前见过的最大的提案编号,代表当前会议的最高优先级。
  2. 新提案编号n' 是新到达的提案所携带的编号。
  3. 严格递增n' > n 保证后发的提案拥有更大的编号,从而获得更高优先级。这是 Paxos 对“后来者居上”的形式化表达。

第一阶段准备

拿到话筒的提案者没有直接宣布最终方案,而是先做一次摸底调查:“在场各位,你们之前有没有已经定过的结论?如果我现在提出一个更高编号的方案,你们愿意优先考虑吗?”这就是 Prepare 阶段的工作。

提案者广播 Prepare(n) 给所有接受者。接受者检查这个编号 n 是否大于自己承诺过的最大编号 promised_id。如果大于,接受者就更新承诺,并回复一个 Promise 消息,同时把自己已接受过的旧提案(编号和值)带上;如果编号不够大,则直接拒绝或忽略。

这个阶段的意义在于“锁定状态”:接受者承诺不再响应任何编号更小的 Prepare 请求。这样一来,后到的提案者能够确定,在他摸底之后不会再有人携带旧编号来搅局,他拿到了一个干净的协商窗口。

摸底调查是 Paxos 的灵魂:它让新提案者知道团队之前是否有既定结论,同时用编号锁住所有接受者的承诺。有了这一步,后面的“最终表决”才不会推翻别人已经达成的协议。

Prepare(n)→{Promise(na,va)n>pmaxRejectn≤pmax\displaystyle \text{Prepare}(n) \to \begin{cases} \text{Promise}(n_a, v_a) & n > p_{max} \\ \text{Reject} & n \le p_{max} \end{cases}
  1. 输入请求\text{Prepare}(n) 表示一个携带编号 n 的准备请求到达接受者。
  2. 检查编号p_{max} 是接受者已经承诺的编号上限;只有当 n > p_{max} 时才承诺新请求。
  3. 返回旧值\text{Promise}(n_a, v_a) 中,n_a 和 v_a 是该接受者已经接受过的提案编号与值,必须返回给提案者。
  4. 拒绝分支\text{Reject} 表示编号不满足条件时拒绝响应,保护已承诺的约定不被破坏。

第二阶段接受

摸底完成后,提案者整理出最终方案。如果他发现之前有人接受过旧值,就必须沿用旧值,而不是另起炉灶;如果所有人都没有接受过任何值,他才能自由提出自己的方案。这就是接受(Accept)阶段。

提案者把编号 n 和值 v 打包成 Accept 请求广播给接受者。接受者发现编号 n 仍不小于自己的承诺编号,就接受这个值,并返回 Accepted 反馈。当提案者收到多数派的 Accepted 时,共识达成,值 v 被正式选定。

“限制性选择”是这个阶段最反直觉也最精妙的设计。它的目的只有一个:防止两个不同的提案者各自提出不同值,导致系统分裂。因为多数派必然相交,提案者 A 的多数派答案一定会被提案者 B 的多数派看到,B 必须延续 A 的结论。

限制性选择让共识有了“先来后到”的惯性:一旦某个值被多数接受,后续一切新提案都会自然而然地延续它。就像会议已经定了火锅,后来的议题安排就不该再推翻火锅店,而是围绕火锅店规划。

v={vprevif ∃(np,vp) has been acceptedvnewotherwise\displaystyle v = \begin{cases} v_{prev} & \text{if } \exists (n_p, v_p) \text{ has been accepted} \\ v_{new} & \text{otherwise} \end{cases}
  1. 查询旧提案\exists (n_p, v_p) \text{ has been accepted} 判断是否有任何接受者已经接受过某个旧提案。
  2. 延续旧值如果存在,新提案必须选择 v_{prev},即沿用已经存在的结果。
  3. 自由提出如果没有任何旧值,提案者才能选择自己的 v_{new}。

Multi-Paxos演进

公司每天要开十几次会,如果每次会议都重新走一遍“举手确认流程”,效率低得可怕。于是团队推选了一位固定主持人,把议事规则一次性确认好,后续每个议题只要挨个过即可,流程开销大幅下降。

Basic Paxos 每次只能对一个值达成共识,真实系统却需要连续处理成千上万个写请求。Multi-Paxos 的核心思路是:先选举一个稳定的 Leader(主持人),让它在一次 Prepare 中获得一个足够大的编号;之后所有日志条目的共识都复用这个编号,只执行 Accept 阶段。

这个演进看似简单,实际上改变了 Paxos 的效用:Basic Paxos 是“开一次会,决定一件事”;Multi-Paxos 是“选一次主持人,连续过整个议程”。Leader 还负责分配日志槽位、决定提交顺序,让分布式系统真正具备连续处理请求的能力。

主持人机制的本质,是用一次高质量的准备换取了后续所有提案的速度。每一个新日志条目不需要再重新询问“大家愿意吗”,只需要直接说“按这个来”,因为 Leader 的权威已经在选主时确认过了。

∀i∈N:ni=nleader\displaystyle \forall i \in \mathbb{N}:\quad n_i = n_{leader}
  1. 全称量词\forall i \in \mathbb{N} 表示对所有的自然数提案序号 i 成立,也就是后续每一个日志条目。
  2. 第 i 个提案编号n_i 表示第 i 条日志对应的 Paxos 实例所使用的提案编号。
  3. 复用 Leader 编号n_{leader} 是 Leader 在一次 Prepare 中确定的编号;所有后续 n_i 都等于它,从而免去重复 Prepare。

日志复制机制

有了主持人,会议还需要一份清晰的纪要。主持人把每个议题标上序号:第 1 条、第 2 条……所有部门都按这份纪要的顺序执行工作。谁也不能说自己先执行第 3 条,因为纪要的顺序就是所有人的共识顺序。

Multi-Paxos 中的“纪要”就是日志。每个共识实例对应一个日志槽位(slot),槽位里放着一个值。多个节点通过 Multi-Paxos 对同一个槽位达成一致,就得到了相同的有序日志。后续每个节点把日志按顺序应用到自己的状态机上,就保证了副本数据完全一致。

槽位编号必须严格单调递增,因为日志的顺序就是提交顺序。数据库、配置中心等系统的操作语义都依赖这个顺序:先加订单再扣库存,和先扣库存再加订单,结果完全不同。Multi-Paxos 通过槽位把并发操作串行化,从而保证状态机的确定性。

日志槽位就是会议纪要的页码。所有部门都从第 1 页读到第 N 页,执行的顺序完全相同。这样即使某个节点中途宕机重启,只要日志还在,它就能从上次的位置继续,回到与其他节点一致的状态。

sloti+1=sloti+1\displaystyle slot_{i+1} = slot_i + 1
  1. 当前槽位slot_i 表示当前已分配的日志位置。
  2. 下一个槽位slot_{i+1} 是紧接着的下一条日志所在位置。
  3. 连续性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 的全部收益:准备费用摊薄到整个日志序列上。正如会议主持人前期花一点时间定规则,后面所有议题都顺着规则高效推进,这正是分布式系统最需要的工程智慧。

Ttotal=Tprepare+N×Taccept\displaystyle T_{total} = T_{prepare} + N \times T_{accept}
  1. 总耗时T_{total} 表示让 N 条日志达成共识的总时间。
  2. 一次性准备T_{prepare} 是 Leader 选主时执行的一次 Prepare 耗时,全局只发生一次。
  3. N 次接受N \times T_{accept} 是后续 N 条提案各自的 Accept 耗时,线性累加。

AI 应用场景

分布式存储的一致性复制引擎

阶段:分布式一致性引擎设计

解决的问题:传统主从复制依赖单点 Master 决定写入顺序,Master 宕机时整个系统停写,或出现双主写冲突。Multi-Paxos 让多个副本通过日志槽位同步决定顺序,任何多数派存活就能继续服务。

具体案例:TiKV 的每个 Raft 组在写操作到达 Leader 后,将操作写入日志,通过心跳复制到多数节点,多数确认后提交并应用到状态机。这个流程正是 Multi-Paxos 思想的工程化体现。

配置中心的分布式协调

阶段:分布式协调与配置管理

解决的问题:微服务配置集中管理时,更新必须让所有节点拿到相同版本,否则不同节点行为不一致。Multi-Paxos 的全局有序日志提供了天然的版本顺序保证。

具体案例:etcd 作为 Kubernetes 的存储后端,每个配置键的变更都被包装成一个提案进入日志,各节点按槽位顺序应用。一次配置发布就是一次共识过程。

Python 演示

python50 行
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()
目的:用三节点模型演示 Multi-Paxos 的关键流程——先选 Leader 获得大编号,再对后续日志槽位直接执行 Accept,无需重复 Prepare。
  1. if nid > self.promised_id:只有更大的编号才值得承诺,防止旧请求干扰。
  2. self.accepted[slot] = (nid, val)记录某个日志槽位已接受的值,供后续提案参考。
  3. if slot in node.accepted:如果槽位上已有旧值,新提案必须沿用,这是限制性选择。
  4. node.accept(slot, leader_id, chosen)直接跳过 Prepare,用同一个 Leader 编号接受新值。
  5. sum(1 for n in nodes if n.prepare(500) == "promise")一次性选主,为后续所有槽位预热。
  6. 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 最核心的职责是什么?

单选题 · easy

  • A.主动发起提案并推动多数派决议
  • B.对收到的编号做出承诺和接受回应← 正确答案
  • C.记录日志并从旧日志中恢复状态
  • D.选出新的 Leader 并维持心跳

答案:B。Acceptor 是“举手投票的人”,它只负责对编号做出承诺并接受值,不主动发起提案。A 是 Proposer 的职责,C 是状态机恢复逻辑,D 是 Multi-Paxos 中 Leader 选举相关的工程机制,都不是 Acceptor 的核心职责。

Q2. 3 台 Acceptors 的集群中,最多能容忍几台宕机仍能完成共识?

单选题 · easy

  • A.0 台,必须全部存活才能共识
  • B.1 台,因为 2 台就能构成多数派← 正确答案
  • C.2 台,因为 3 台总能形成多数派
  • D.必须至少 4 台才能容忍宕机

答案:B。3 台节点中多数派最少需要 2 台,所以最多允许 1 台宕机。这与“多数派=超过半数”的原则一致,也是容灾设计里常见的选择。

Q3. Acceptor 在 Prepare 阶段返回 Promise 时,为什么要带上自己已接受过的旧提案?

单选题 · mid

  • A.让 Proposer 统计本轮投票总数
  • B.让新提案沿用旧值避免双值并存← 正确答案
  • C.让 Acceptor 自己学习最终决议结果
  • D.让日志复制时保持命令顺序一致

答案:B。Promise 中携带旧提案(编号和值)是为了让新提案者知道之前是否有已接受的值。如果存在,新提案必须沿用旧值,否则两个多数派可能接受不同值,导致共识破坏。

Q4. 在 Prepare 阶段,Acceptor 需要遵守哪些规则?

多选题 · mid

  • A.编号大于已承诺值的 Prepare 予以承诺← 正确答案
  • B.编号小于已承诺值的 Prepare 直接拒绝← 正确答案
  • C.已接受的值要全部丢弃重新开始
  • D.承诺后回应自己已接受的提案信息← 正确答案

答案:ABD。A 和 D 是承诺的核心行为:只对更大的编号承诺并返回已接受信息。B 是保护机制,防止旧编号干扰。C 完全错误——已接受的值是限制性选择的基础,绝不能丢弃。

Q5. Multi-Paxos 跳过后续 Prepare 的基本前提是什么?

单选题 · mid

  • A.集群所有节点同意不再发起新提案
  • B.多数派选中一个长期稳定的 Leader← 正确答案
  • C.网络延迟为零且消息绝对不丢失
  • D.每个提案都使用不同的独立编号

答案:B。Multi-Paxos 之所以能跳过 Prepare,是因为 Leader 在选主时已经获得了所有接受者的编号承诺。只要 Leader 保持稳定,后续提案就能复用同一个编号直接 Accept。网络延迟归零和节点不提议都是不现实的假设。

Q6. Multi-Paxos 相比 Basic Paxos 的改进包括哪些?

多选题 · mid

  • A.Leader 复用编号避免反复 Prepare← 正确答案
  • B.将提案组织成有序的日志序列← 正确答案
  • C.冲突后可无限重试不设上限
  • D.明显减少单条命令网络往返次数← 正确答案

答案:ABD。Multi-Paxos 的核心改进是选主后复用编号、用日志槽位串联多个值,并把每条命令的往返从两次降到一次。C 错误,实际工程中重试有预算和上限,否则系统会陷入活锁。

Q7. 在 Multi-Paxos 中,如果一个日志槽位同时出现两个不同值的竞争,最终结果取决于什么?

单选题 · hard

  • A.v2 必将取代 v1,因为并发冲突
  • B.多数派接受后才写进该槽位← 正确答案
  • C.该槽位一直空缺,系统死锁
  • D.v1 和 v2 合并成一个新值

答案:B。槽位最终写入哪个值,只取决于哪个提案先被多数派接受。另一个值会被拒绝或成为未提交的孤儿日志,最终被覆盖或者被写入下一轮协商中。槽位不会死锁,两个值也不会合并。

Q8. Basic Paxos 的 Safety(安全性)属性最准确的说法是?

单选题 · hard

  • A.任意两个多数派不会选出不同决议值← 正确答案
  • B.每个提案编号必须递增且不能重用
  • C.一旦某节点接受一个值就永不可变
  • D.多数派与少数派决定能自动合并

答案:A。Safety 的本质是“唯一性”:任意两个多数派不可能选出不同的值。B 是算法实现的必要规则而非安全属性本身。C 错误,因为接受者可以在不同编号下改变自己接受的值。D 更是违背了多数派原则。

Q9. Basic Paxos 容易发生冲突,Multi-Paxos 用哪些手段缓解?

多选题 · hard

  • A.选举长期 Leader 减少并发提名← 正确答案
  • B.租约机制确保同时只有一个 Leader← 正确答案
  • C.只在选主阶段做一次 Prepare← 正确答案
  • D.所有节点每次提案都互相广播

答案:ABC。Multi-Paxos 通过选主和租约大幅减少并发冲突,并把 Prepare 收敛到选主阶段。D 是错误操作,会让每个提案都变成全广播,网络开销和冲突概率不降反升。

Q10. 极端情况下 Basic Paxos 可能出现活锁,Multi-Paxos 通常如何避免?

单选题 · extreme

  • A.增加 Acceptors 数量扩大多数派
  • B.随机退避和 Leader 租约帮助稳定← 正确答案
  • C.取消 Prepare 接受任意编号
  • D.提案编号每次只递增一个

答案:B。活锁源于多个提案者持续用更大的编号抢跑,导致彼此的 Prepare 互相打断。随机退避让提案者在冲突后随机等待再重试,Leader 租约保证一段时间内只有一个提案者,从根本上消除并发抢占。