村里要办一年一度的年夜饭,全村几十户人家得定在同一天同一家饭店。消息靠传话,有人出门在外接不到电话,有人隔天才看到通知。负责张罗的小张提议村口火锅店,隔壁老李坚持山顶农家乐。两边的支持者各自拉票,最终结果却可能变成——两拨人各订各的,年夜饭不欢而散。 分布式系统每天都在上演同样的剧本:一台台服务器就像一个个村民,网络延迟就是传话的驿站,服务器宕机就是有人缺席。怎样才能保证:只要还有超过一半的村民能联系上,年夜饭就不会黄,而且绝不会出现两桌都订满的闹剧?1975 年,计算机科学家 Leslie Lamport 为这个问题写了一篇旷世论文,用古希腊议会的故事给出了答案——这就是 Paxos 协议。 本文不堆砌论文术语,而是陪你把答案一步步推导出来:从问题根源、角色分工,到编号与承诺、两阶段流程,再到正确性证明与工程特性。你会发现,Paxos 的每个设计,都是被现实逼出来的必然选择。
学懂 Paxos 推导过程,就掌握了分布式共识的底层思维。数据库内核、分布式存储、云原生基础设施的面试与设计中,它都是绕不开的基石。
可视化演示
展示系统处理流水线、实时性能指标曲线及多方案对比分析,包含数据流转、吞吐延迟监控与方案评估。
本节配有交互动效(进入视口后自动加载,约 3KB)。
系统可视化 处理流程:输入→处理→模型→输出 性能监控:实时指标 P=L×T
问题根源
同一场年夜饭,大家消息不对称:张三收到了小张的通知,李四只看到老李的传单,王五干脆没信号。结果不是两桌同时开席,就是全村饿肚子。分布式系统面临的正是这个困局:每个节点对“当前该做什么”都有自己的局部视图,而系统整体又必须给大家一个统一答案。如果这个答案还能被随便推翻,那比没有答案更糟——刚把火锅店的定金付了,转头又被告知改去农家乐。
把村民换成服务器,把饭店换成需要一致提交的数据值(Value),把传话换成网络消息,问题就清晰了:一群节点在没有中央调度者、存在消息延迟和随机故障的环境里,必须就同一个值达成一致,并且一旦选定,这个值就不能被后续操作改写。这不是“尽量同步一下”的软需求,而是一条硬性安全规范,在 Paxos 里被称为一致性(Agreement)。
正式一点:Paxos 要保证的是,任意时刻都不可能有两个不同的值被系统确定为最终结果。这个目标可以写成一条不变量:
这个不变量就是整座大厦的地基。接下来要推导的每一个规则——角色、编号、两阶段——都在为守卫这一个公式服务。过年的一桌饭,看似简单,但放进“有人迟到、有人缺席”的世界里,就逼着我们发明一套精密的政治程序。
- 值集合\(V\) 是所有可能被选定的值的集合,在年夜饭场景里就是“火锅店、农家乐、自助餐……”这些候选地点。
- 否定存在量词\(\neg\exists\) 读作“不存在”,它从根上否定了“有两个不同值同时当选”这种情况。
- 合取条件三个条件用 \(\wedge\) 连在一起,必须同时成立才会违反安全。任何一个为假,破坏都不发生。
角色与多数派
年夜饭总不能全村几千人一起坐在谈判桌上。有人负责出主意,比如小张提出“去火锅店”;有人负责记录和表态,比如几个热心肠的村委会成员;大多数人只关心最后通知我去哪吃。于是 Paxos 把节点分成三个角色:Proposer(提案者,出主意的人)、Acceptor(接受者,负责记录和投票的人)、Learner(学习者,等待最终结果的人)。
这里有个关键的推导转折:如果要求“所有 Acceptor 都同意”才能定案,那么只要有一个 Acceptor 宕机,年夜饭就永远定不下来。所以 Paxos 把“全体一致”降级为“多数派一致”——只要超过半数的 Acceptor 点头,结果就是定局。这个“超过半数”的集合叫 Quorum(法定人数)。
为什么“多数”就够了?因为这带来一个漂亮的数学性质:任意两个多数派必然有至少一个公共成员。就像三户人家中任意两家一起吃饭,总有一户会同时认识两拨人。这个性质写下来是:
多数派交集就是 Paxos 安全的秘密武器。它保证:无论历史如何分岔,总有一个“知情人”站在两条路交汇处,能把上一轮的决议带给下一轮。年夜饭的秩序,就靠这个公共村民来维护。
- 多数派定义一个集合 \(Q\) 是多数派,当且仅当它的成员数严格大于总数的一半。
- 任意两个多数派\(\forall Q_1, Q_2\) 表示“无论你选出哪两个多数派”——不是特批某一对,而是所有可能都要安全。
- 交集非空\(Q_1 \cap Q_2 \neq \emptyset\) 意味着两个小组里至少有一个共同的 Acceptor,它是沟通两轮的桥梁。
编号与承诺规则
小张的方案是 5 号,老李的方案是 6 号。如果两人同时拉票,村民们可能上午答应小张、下午又答应老李,最后谁说了都不算。关键推导来了:要给每个提案一个全局全序的编号 \(n\)(编号越大代表越“新”),并且要求 Acceptor 做出一个刻骨铭心的承诺——一旦答应支持编号 \(n\) 的方案,就绝不再接受任何编号小于 \(n\) 的方案。
这个承诺在 Paxos 里叫 Promise(承诺)。为什么必须这样做?因为编号 5 和 6 之间可能存在竞争窗口:老李的 6 号提案正在路上,如果 Acceptor 还接受小张的 5 号,两个不同的值就可能都被多数派接受,安全地基瞬间崩塌。承诺的本质是“选边站”,用编号的序关系强行消除并发窗口。
还有一个更深的推导:Acceptor 光记住“我承诺过 6 号”还不够,还必须记住“我是否已经在某个编号上接受了某个值”。否则当未来某位 Proposer 来摸底时,这位 Acceptor 无法把自己见证过的历史告诉对方。承诺规则的形式化表达为:
有了编号和承诺,村里就不会有人两面讨好。但承诺只是“防守”,真正要确定一个值,还需要主动出击的流程——这就是下一节的两阶段推导。
- 承诺命题\(\text{Promise}(a, n)\) 表示 Acceptor \(a\) 接受了编号 \(n\) 的 Prepare 请求,并做出“不再支持旧编号”的保证。
- 全序比较\(\forall n' < n\) 覆盖了所有比当前编号小的提案,这正是“只认新不认旧”的数学表达。
- 拒绝行为\(\text{Reject}(a, n')\) 表示对旧编号后续的一切请求,Acceptor 都会明确拒绝或忽略。
两阶段流程推导
既然决定了要投票,又不能乱投,自然导出了两阶段流程。第一阶段叫 Prepare(准备),可以理解为“摸底”:小张拿着 5 号方案挨家挨户问“你们最多支持到几号?如果已经有人定过老地点,告诉我”。每个 Acceptor 的回答是 Promise:好的,5 号以内我不再答应别人;顺便告诉你,我曾经接受过 3 号方案的“农家乐”。
第二阶段叫 Accept(接受),是“正式定案”:小张收集到多数派的承诺后,如果摸底中发现有人报告了已接受的值,就必须采用其中编号最高的那个值;如果没有人报告过任何旧值,他才能提自己的“火锅店”。然后把这个最终值连同 5 号编号发给 Acceptor,让它们记录并确认。
为什么必须“沿用旧值”?这是整个推导中最精妙的一步。因为那个旧值很可能已经在一个较早的轮次里被某个多数派接受了,只是小张当时不知道。如果他不沿用旧值而是坚持自己的值,两个值都可能形成多数派,违反 Agreement。值选取规则写成分段函数:
两阶段流程就此闭环:先摸底收集承诺,再带着旧值或新值正式投票。村民小张发现:只要有人记录过“农家乐”,他就必须改口提议农家乐,哪怕他更爱吃火锅。这不是妥协,而是数学上的必然——那桌旧饭可能已经订好,他不能掀桌。
- 已接受值的提取从所有 Promise 响应里挑出“带着已接受值”的记录,并找出编号最大的那一条。
- 条件分支\(\text{if } \exists\) 判断响应集合中是否真有已接受的旧值,而不是空集合。
- 兜底赋值\(\text{otherwise}\) 处理“没有任何旧值”的情况,此时 Proposer 拥有自由,可以提交自己的值。
正确性证明
现在到了最激动人心的一步:证明这套规则真的不会出现“两桌同时开席”。回顾多数派交集定理:任意两个多数派必然共享至少一个 Acceptor。这个共同村民就是证明的钥匙。
先定义“选定”。一个值 \(v\) 被选定(Chosen),意味着存在一个多数派集合 \(Q\),其中每个 Acceptor 都已经接受过 \((n, v)\)。形式化定义:
接下来用反证法证明安全性不变量。假设第一轮在编号 \(n_1\) 选定了值 \(v_1\),而后来第二轮编号 \(n_2 > n_1\) 却接受了不同的值 \(w \neq v_1\)。由于第二轮也凑到了多数派,它与第一轮构成多数派必有公共 Acceptor \(a\)。在第二轮 Prepare 时,\(a\) 必须如实告诉新 Proposer:“我曾在 \(n_1\) 号方案中接受了 \(v_1\)。”于是按上一节的取值规则,新 Proposer 只能提出 \(v_1\),不可能提出 \(w\),矛盾!
这个反证可以归纳到任意编号更大的后续轮次,因此得到核心不变量:
这个证明的本质是:多数派交集传播记忆。公共村民把过去的决议带向未来,任何试图颠覆历史的提案都会被承诺和取值规则挡回去。年夜饭一旦定了火锅店,后续再多人来拉票,最终结果也只会是火锅店。
- 大小关系\(n_1 < n_2\) 限定“先选定的”和“后发生的”之间的顺序,编号是全序的。
- 前提条件\(\text{Chosen}(n_1, v_1)\) 是归纳的起点,表示编号 \(n_1\) 的值已板上钉钉。
- 结论\(\text{value}(n_2) = v_1\) 推出后到者只能追随先行者,否则会产生矛盾。
算法特性
最后,我们来审视这套协议能撑起多大的风雨。年夜饭的组织者问:最多能容忍几个人失联还能把饭定下来?答案是“小于一半”。假设共有 \(N\) 个 Acceptor,多数派至少需要 \(\lfloor N/2 \rfloor + 1\) 个节点,因此最多可以容忍 \(\lfloor (N-1)/2 \rfloor\) 个节点同时失效:
Paxos 还有一个反直觉的优雅之处:它没有固定的主节点。任何节点都可以在任意时刻扮演 Proposer 发起一轮提案,只要编号够新、能凑齐多数派。这种去中心化让系统在领导人选举上也天然容错。但也因此带来工程上的复杂性:多轮竞争可能活锁(大家互相打断,谁也定不下来)。于是工程界演化出 Multi-Paxos 和 Raft 等变体:前者用长期 Leader 稳定提案通道,后者把“选主+日志复制”整体重构成更易理解的协议。Raft 不是 Paxos 的对立面,而是 Paxos 思想最著名的工程化特化。
年夜饭的故事到此收尾:过半村民在线,饭局就能定;新人提出新编号,也必须沿着旧记忆走;没有村长,谁都能当牵头人。这套由数学逼出来的协议,至今仍是谷歌 Spanner、etcd、ZooKeeper 等系统的心脏——只因为现实中每一场“年夜饭”,都输不起。
- 多数派规模向下取整加一,保证严格过半,不出现“恰好一半”的歧义。
- 容错上限可用节点数减去多数派规模,就是可以容忍的故障数。
- 具体代入以常见的 5 节点集群为例,3 个 Acceptor 存活就足以维持共识。
AI 应用场景
分布式数据库元数据管理
阶段:分布式存储引擎中的数据一致性保障
解决的问题:传统主从复制模式中,主节点一旦宕机,从节点各自为政,元数据可能分叉。Paxos 让多个副本组成多数派,自动选出新主并保证元数据不产生双主分裂,像村民失联后依然能靠多数派重新定局。
具体案例:开源分布式数据库 TiKV 使用 Raft(Paxos 的工程变体)管理 Region 的元数据和选举。某 Region 的 Leader 宕机后,副本通过共识协议在数百毫秒内选出新 Leader,业务无感知。
配置中心与分布式协调服务
阶段:微服务基础设施的服务发现与选主
解决的问题:服务注册、分布式锁和集群选主都需要“所有客户端看到同一个答案”。用 Paxos 家族协议(ZAB、Raft)实现协调服务,避免脑裂导致两套配置同时生效,就像年夜饭不会同时订两家饭店。
具体案例:Apache ZooKeeper 基于 ZAB 协议提供分布式锁。Kafka 集群的 Controller 选举由 ZooKeeper 完成,多个 Broker 竞争同一把锁,只有一个能成为 Controller。
全球多活数据库
阶段:跨地域数据同步的共识层
解决的问题:跨国业务要求多个数据中心同时可写、读到的数据却完全一致。Paxos 被嵌入存储引擎,让每个跨区副本就像不同村的村民,通过两阶段流程在全球尺度上达成一致。
具体案例:Google Spanner 在每一个 Tablet 副本组内运行 Paxos,配合 TrueTime 实现外部一致性。某个数据中心整体故障时,其他两个数据中心组成的多数派继续提供服务。
Python 演示
class Acceptor: def __init__(self, name): self.name = name self.promised_id = -1 self.accepted_id = -1 self.accepted_value = None def recv_prepare(self, n): if n > self.promised_id: self.promised_id = n return True, self.accepted_id, self.accepted_value, self.name return False, -1, None, self.name def recv_accept(self, n, v): if n >= self.promised_id: self.promised_id = n self.accepted_id = n self.accepted_value = v return True return False class Proposer: def __init__(self, name, acceptors): self.name = name self.acceptors = acceptors def propose(self, n, v): promises = [] for a in self.acceptors: ok, max_aid, max_val, aname = a.recv_prepare(n) if ok: promises.append((max_aid, max_val, aname)) quorum = len(self.acceptors) // 2 + 1 if len(promises) < quorum: print(self.name + ": prepare 未获多数派") return False promises.sort(reverse=True) if promises[0][0] != -1: v = promises[0][1] count = 0 for a in self.acceptors: if a.recv_accept(n, v): count += 1 if count >= quorum: print(self.name + ": 选定值为 " + str(v)) return True print(self.name + ": accept 未获多数派") return False if __name__ == "__main__": acceptors = [Acceptor("A1"), Acceptor("A2"), Acceptor("A3")] p1 = Proposer("P1", acceptors) p2 = Proposer("P2", acceptors) p1.propose(1, "火锅店") p2.propose(2, "农家乐")
class Acceptor:每个 Acceptor 保存最大承诺编号和已接受的值,对应村民的记忆。self.promised_id = -1初始承诺编号为 -1,表示还没有承诺过任何提案。def recv_prepare(self, n):处理 Prepare 请求,只有编号更大才回复 Promise,并捎带自己记住的旧值。(True, self.accepted_id, self.accepted_value, self.name)返回四元组,包括承诺结果、已知最高编号的旧值、值本身和节点名。if n >= self.promised_id:Accept 阶段的条件:只有编号不小于已承诺编号才接受,防止旧方案插队。promises.sort(reverse=True)按编号降序排序,最高编号的已接受值排在最前面。if promises[0][0] != -1:如果响应里有旧值,就把本轮提案值改成旧值,体现“沿用旧值”规则。quorum = len(self.acceptors) // 2 + 1计算多数派大小,3 个节点时是 2。p2.propose(2, "农家乐")高编号提案发起时,会因为看到火锅店而放弃自己的农家乐提议。
常见误区
误区跑完一轮 Paxos 后,每个节点就像收到群发短信一样,都知道了年夜饭定在火锅店。毕竟协议跑完了,大家不该都知道吗?
正解Basic Paxos 只保证“选定值不会被改写”,这是安全性;至于谁知道了结果、什么时候知道,属于学习(Learn)环节,需要额外的机制。如果负责通知的 Learner 失联,部分节点可能一直不知道结果,系统仍然安全,只是活性暂未满足。
为何常见:把“协议结束”等同于“广播到全部节点”,混淆了决策与传播两个阶段。
误区既然是编号大的赢,那我直接用每台服务器自己的时间戳当提案编号不就行了吗?又快又天然递增。
正解提案编号要求全局全序且两两唯一。服务器时钟存在偏移和回拨,两台机器可能生成相同甚至乱序的时间戳,导致承诺关系错乱。工程上通常用“轮次号 + 节点 ID”组合,或逻辑时钟保证唯一性。
为何常见:直觉上“越新的方案越优先”,忽略了分布式里没有可信全局时钟这一前提。
误区搞 3 个副本就是为了坏一台还有备份,多数派的作用就是容错,跟一致性没什么关系。
正解多数派的第一价值不是备份,而是任意两个多数派必有交集——这是安全性的根基。如果只要求“写两个副本”而非“写多数派”,两个提案可能各写一半,两个值同时生效,年夜饭直接开成两桌。
为何常见:只从高可用角度理解副本,忽视了“交集”这一数学性质对一致性的决定性作用。
误区Raft 强调选举 Leader 和日志复制,名字、流程、论文风格都不一样,应该和 Paxos 没半点关系。
正解Raft 是 Paxos 思想的工程化特化。它的 Leader 选举相当于固定 Proposer,任期号(term)就是提案编号,日志复制对应两阶段提交。核心不变量与 Paxos 同源,只是做了“强领导者 + 日志连续”的简化,以换取可理解性。
为何常见:只看到协议表面的消息流程差异,没看到它们共享“多数派 + 编号 + 承诺”的底层逻辑。
面试题
Q1. 在 Paxos 算法中,负责提出提案并使用编号推动流程的角色是?
- A.Proposer(提案者)← 正确答案
- B.Acceptor(接受者)
- C.Learner(学习者)
- D.Coordinator(协调者)
答案:A。Proposer 是出主意的人,负责生成编号、发起两阶段流程。Acceptor 负责投票和记录,Learner 只学习最终结果,协调者不是 Paxos 的标准角色。就像小张提议年夜饭去哪吃,村民记录和投票。
Q2. 若 Acceptor 总数为 5,多数派至少需要几个节点?
- A.2 个
- B.3 个← 正确答案
- C.4 个
- D.5 个
答案:B。多数派要求超过半数,5 的一半是 2.5,取整加一得到 3。这是 Quorum 的基本计算,也是容错 2 台节点的基础。
Q3. 关于 Learner 获知最终值的方式,正确的是?
- A.向多数派 Acceptor 查询已接受的值← 正确答案
- B.等待 Proposer 广播 Accepted 消息← 正确答案
- C.必须自己重新跑一轮 Paxos
- D.通过其他 Learner 转述结果← 正确答案
答案:ABD。Learner 可以通过主动查询多数派、被动接收广播或从其他 Learner 处间接得知结果,这些都是工程上常见的学习方式。它不需要自己重新跑一轮提案,那只会增加无谓的冲突。
Q4. Acceptor 发出 Promise(n) 意味着什么?
- A.接受所有编号为 n 的提案
- B.不再接受编号小于 n 的提案← 正确答案
- C.立即为 n 选定一个值
- D.拒绝所有比 n 更大的提案
答案:B。Promise 是一种“承诺不再支持旧编号”的保证,而不是接受某个具体值。它像村民答应 5 号方案后,对 4 号、3 号一概不理会,但不等于立刻同意火锅店。
Q5. Prepare 阶段收到多数派 Promise,且其中有人报告已接受 (n=3, v=农家乐),本轮编号为 5,Proposer 应提出什么值?
- A.立即放弃本轮提案
- B.采用农家乐作为提案值← 正确答案
- C.坚持自己的初始值
- D.重新发起 Prepare 阶段
答案:B。按值选取规则,只要响应中存在已接受值,就必须采用编号最高的那个。因为农家乐可能在旧轮次中已被选定,新提案必须沿用它来保证安全性。
Q6. 两个多数派集合的交集至少有几个 Acceptor?
- A.可以完全没有交集
- B.至少有 1 个公共节点← 正确答案
- C.至少有半数节点
- D.一定包含全部节点
答案:B。两个超过半数的集合,其补集都不到一半,不可能互斥。所以交集至少有一个节点。这是 Paxos 安全证明的地基,就像两拨过半的村民中总有同一张熟面孔。
Q7. 多数派在 Paxos 中承担的作用包括?
- A.保证两次抉择有共同见证者← 正确答案
- B.让系统容忍少数节点故障← 正确答案
- C.减少大多数通信开销
- D.保证提案编号全局唯一
答案:AB。多数派交集提供共同见证者,从而保证安全性;同时它允许最多不到半数的节点宕机,提供容错。通信开销和编号唯一性与多数派本身没有直接关系。
Q8. 若一个 Acceptor 从不记录已接受的值,只记录最大承诺编号,会导致?
- A.两个不同值可能被先后选定← 正确答案
- B.提案编号无法相互比较
- C.系统的活性会彻底丧失
- D.Learner 永远无法得知结果
答案:A。不记录已接受值,Proposer 在 Prepare 阶段就无从得知旧值,可能用新值覆盖旧值,导致两个值先后被不同多数派选定。记忆功能是取值规则的前提,删掉它等于拆掉安全地基。
Q9. 证明 Paxos 安全性时,最关键的一步依赖是?
- A.编号越大承诺越强
- B.多数派交集传递旧值← 正确答案
- C.Learner 的补偿机制
- D.网络必须完全可靠
答案:B。反证法的核心是:新旧两个多数派必有一名公共 Acceptor,它会向新提案者传递旧值,迫使新提案采用旧值,从而不可能出现两个不同值。承诺规则只是辅助,真正承上启下的是交集传值。
Q10. 以下工程实践中,与 Paxos 家族直接相关的是?
- A.Spanner 跨数据中心复制← 正确答案
- B.ZooKeeper 的 ZAB 协议← 正确答案
- C.Redis 单机 RDB 持久化
- D.etcd 使用的 Raft 共识← 正确答案
答案:ABD。Spanner 直接使用 Paxos,ZAB 是 Paxos 思想在 ZooKeeper 中的实现,Raft 是 Paxos 的工程化变体。Redis 单机持久化不涉及分布式共识,与本主题无关。