
如果你在分布式系统圈子里待过一段时间一定听过这个说法Paxos是公认的“最难懂的分布式一致性算法”甚至有人调侃“Paxos论文发表十几年真正能讲清楚的人依然不多”。我第一次读Paxos的时候满脑子都是“法定人数”“轮次编号”“已批准的值”这些术语读了三遍还是云里雾里。直到后来自己动手写了一个简化版的多节点状态机又认真把Raft读了一遍再回头看Paxos才意识到它其实没那么神——核心思想就一句话先选出“谁说了算”再决定“值是什么”。这篇文章就是写给那些像我一样被Paxos各种学术表述劝退过的人。我会用一个“开会选时间”的生活例子把Basic Paxos的两轮投票逻辑拆开揉碎再讲清楚它最常见的变体Multi-Paxos是怎么在真实系统里落地以及学习过程中特别容易踩的几个认知坑。如果你正准备面试、复习分布式理论或者要在工程里实现类似机制这篇可以作为你的第一份“人话版Paxos速通材料”。1. Paxos到底在解决什么问题——先搞懂“共识”这个词1.1 一个值多个节点怎么保持一致先丢一个最简单的场景。假设你维护一个三节点的缓存系统客户端往里面写了一个key三个节点各自保存一份拷贝。正常情况下主节点收到写请求同步给另外两个写完返回成功。有一天其中一个节点宕机了另外两个还能正常服务。这时候又一个写请求来了只有两个节点能响应要不要写如果不写系统直接降级如果写那等宕机的节点恢复之后它手里的数据是旧的和另外两个节点不一致。你要么等它恢复后把新数据推给它要么干脆接受一段时间的不一致。但问题没这么简单——如果恢复过程中又来了一个写请求呢如果两个节点同时处理不同的写请求呢如果网络把三个节点切成了两拨两边同时都在写各自的值呢这些情况单靠“备份手动同步”根本处理不了。你真正需要的是让所有节点对“当前这个key的值是多少”这件事达成一致这个机制就是共识算法。Paxos解决的就是这个问题在可能宕机、可能网络分区、可能消息乱序的分布式环境里让多个节点对一个值达成强一致。它不是某个框架的专利算法而是一种被证明过安全性的协议框架——只要按它的规则走系统就能保证不会出现“两个节点各自认定一个值、且互相不知道”的分裂局面。1.2 没有人为故障兜底共识根本无从谈起有人会问我用数据库自带的主从复制不就完事了吗主库写从库同步主库挂了再选一个新主库。这其实就是一种共识的雏形但主从复制里有一个隐含假设主库永远只有一个。现实中“某个节点以为自己还是主库、另一个节点已经被别人选成了新主库”的情况并不罕见。两个主库同时接受写入数据就分裂了。所以理解Paxos的第一步是先接受几个不可回避的约束节点之间只能靠发消息通信没有共享内存消息可能延迟、可能丢失、可能乱序单个节点可能宕机而且宕机后无法向其他人解释它宕机前看到了什么整个系统随时可能被切成几块互不通信的分区。Paxos要在这样一堆“烂条件”下依然保证安全性。这里的“安全性”具体指什么用大白话说一旦某个值被宣布“已批准”就不会有另一个不同的值再被批准而且只要多数派节点还活着且能通信系统就能继续对新的值达成共识。一个算法能在这种条件下保证安全你才敢把业务数据交给它。工程上有一种常见误解觉得Paxos能“自动修复一切故障”包括同时挂掉一半以上节点还能继续写。这是不可能的任何容错算法都有边界这个边界就是“多数派必须可用”。三节点集群挂两台系统当然不能继续产生新共识但已批准的值依然可以被活着的节点读出来。这已经是最好的结果了。1.3 共识不是“最终一致”而是“已批准的值不可推翻”这里要做一个重要的区分。很多后端同学接触过“最终一致”以为Paxos就是让所有节点最终同步成一个样子。实际上Paxos要解决的是更强的约束当一个值被“批准”chosen之后无论后续发生什么最终活着的节点都会认同这个值没有任何节点会批准另一个不同的值。这就是“已批准的值具有唯一性”。拿生活打个比方一群人在微信群里讨论聚餐地点。小红说“吃火锅”小刚说“吃烧烤”最后大多数人投票选了火锅。此时“火锅”这个值就是“已批准”的。后面哪怕小红反悔想改成烧烤也不能改了——因为群里大多数人已经认了火锅你要推翻它就等于要说服大多数人集体反悔这在Paxos里是做不到的。Paxos保证的就是这种“投票结果一旦产生就不能被推翻”。它不保证每次投票一定成功可能因为活锁等原因达不成共识但一旦成功结果就是唯一的。理解了这个你去看Paxos的两个阶段就能时刻问自己这个阶段做的是什么它为了保证“不推翻已批准的值”做了什么约束这两个问题的答案就是Paxos全部的精髓。2. 用“开会选时间”的例子拆解Basic Paxos的完整流程2.1 Paxos的三个角色和一句话核心先介绍角色。Basic Paxos里通常有三个角色Proposer提议者发起提议提出一个值比如“时间定在周三下午三点”Acceptor接受者对提议进行投票收到提议后可以接受或拒绝Learner学习者只学习已经获批的结果不参与投票。实际工程里一个节点往往同时扮演多个角色比如每个节点既是Proposer又是Acceptor。但这不影响理解先分开看。Paxos的核心机制概括成一句话想要通过一个值必须经过两轮投票。第一轮叫Prepare先跟法定人数打招呼、抢一个编号第二轮叫Accept带上具体的值再次请求法定人数确认。为什么非要两轮如果你只做一轮Accept就会出现A节点接受x、B节点接受y两边人数各占一半谁都没有多数系统卡死更糟糕的是可能在不同时刻出现“x拿到了多数派”和“y也拿到了多数派”的乱局。两轮投票的核心价值在于第一轮让所有Acceptor彼此探底让它们记住“我承诺不再接受编号小于X的提议”后续Accept阶段才能保证只批准一个值。是不是感觉还是有点绕下面把它翻译成人话。2.2 第一式Prepare——“我先占个号”想象一个团队要敲定周会时间。团队成员是Acceptor项目经理小P是Proposer。小P想提议“周三下午三点开会”他做第一件事给所有团队成员群发一条Prepare消息里面带一个编号N。编号是什么你可以把它理解为提议的“优先级凭证”数字越大越新后面的提议越有资格“插队”。但每条Prepare消息里只带编号不带具体值。小P说“我想发起一个提议编号是5。请大家告诉我两件事第一你们之前有没有见过比5更大的编号第二你们此刻还记不记得自己批准过什么值”为什么这两件事很重要因为Acceptor的答复里藏着一个承诺。每个Acceptor收到编号为5的Prepare后如果5大于它见过的所有编号它就会承诺以后我不会再接受编号小于5的提议同时如果它曾经批准过某个值它必须把那个值告诉小P。如果5小于它见过的编号它直接拒绝或忽略。这个“承诺”就是Paxos里最重要的一对约束之一。它保证了一旦某个Proposer拿到了多数派的Prepare应答它就拿到了“锁”。锁的意思是在接下来的Accept阶段里任何编号更小的提议都不可能被通过了因为它们已经被Acceptor的承诺挡在门外。2.3 第二式Accept——“按约定提交值”小P发出编号5的Prepare收回了多数派假设5人团队至少3人的应答。如果这些应答者之中有人说过“我批准过值v”那么小P必须选择v作为自己的提议值如果所有人都说“我没批准过任何值”小P才可以自由选择自己想要的值比如“周三下午三点”。然后小P给这些应答者发送Accept请求里面是“编号5值周三下午三点”。Acceptor收到后如果自己还承诺着“不接受编号小于5的提议”而5正好大于等于它们见过的最大编号就接受这个值不然就拒绝。当多数派都接受了“编号5周三下午三点”这个值就被批准了。这看起来只是两轮消息为什么要这么设计关键是中间那条规则“如果有人批准过值就必须选那个值”。这条规则让Paxos在任何情况下都不会批准出两个不同的值。它的直觉是万一已经有一个值被批准过了那你新发起的提案由于编号更大、条件更充分系统不能违背老结论再去选个新值——除非前面那个值根本没有被真正锁定。Paxos用“多数派编号”把“锁定”这件事做了形式化。所以Paxos里的“值”并不总是由Proposer自由决定的它更像是“在遵循旧结论的前提下填补未被决定的值”。如果你理解了这一点后面看Multi-Paxos时就不会奇怪“为什么每个日志槽位的值往往来自上一个已经批准的值”2.4 完整模拟三个节点的投票过程为了具体一点我模拟一个三节点集群。节点A、B、C都是Acceptor同时A和B也都可以当Proposer。场景一只有一个提议者。A发出Prepare(1)发往A、B、C。由于A自己也是Acceptor它先答应自己B、C也答应各自返回“我没有批准过值”。A发起Accept(1, “x”)发往A、B、C。三人都接受x被批准。这是最简单的路径。场景二两个提议者先后发起。A发出Prepare(1)B和C都应答了但A的Accept还没发出。B发出Prepare(2)C也应答了。C记住“不会接受编号小于2的提议”。B发Accept(2, “y”)C接受。此时B有两个接受B自己C。A的Accept终于到了。A发给B的Accept(1, “x”)会被B拒绝因为B已经接受过编号2发给C的Accept(1, “x”)也会被C拒绝因为C承诺过不接收编号小于2的提议。只有A自己还接受。于是x没有得到多数派失败y获胜。你看整个过程中A并没有出现“拿到多数派Prepare应答后选择B批准过的值”的情况因为A的编号1太小了。如果A的编号改成3情况就不同了A先Prepare(3)多数派中一定有人应答“我批准过y”那么A必须在Accept阶段选择y而不是自己想要的x。这就是“一旦多数派中有人批准过旧值新提议必须沿用旧值”的规则。我把两个阶段的行为整理成一张表方便对照记忆阶段消息内容Acceptor的行为Proposer的行为Prepare(编号N)若N大于见过的最大编号则承诺不再接受更小编号并返回已批准的值否则拒绝记录多数派的承诺和返回的已批准值Accept(编号N, 值v)若N不小于见过的最大编号则接受否则拒绝若多数派接受则v被批准3. 理解Paxos绕不开的三个关键点编号、多数派、活锁3.1 为什么必须是“多数派”而不是“所有节点”很多人会问反正要确认为什么Paxos要取多数派而不是全体核心原因是在可用性和一致性之间做取舍。如果必须全体节点确认那么只要有一个节点宕机整个系统就无法共识。而多数派允许最多f个节点宕机系统依然可以继续三节点容忍1个五节点容忍2个。但多数派还有一个更关键的数学性质任意两个多数派必有交集。这是Paxos安全的基石。比如三节点里多数派是2个任意两个“两个节点的集合”必定至少有一个公共节点。正因如此当两个Proposer并发发起提案后一个拿到多数派Prepare时它必然能从前一个多数派成员那里打听到“已经批准过的值”于是被迫沿用旧值不会产生分裂。你可以把这个性质记成一句话两个多数派永远会碰头。这个“碰头点”就是信息交换的桥梁也是Paxos不会凭空冒出两个不同“真理”的几何直觉。3.2 编号机制单调递增、不重复、Proposer自己说了算Paxos里每个Proposer怎么生成编号论文建议可以用“(本节点ID, 序号)”的二元组来保证全局不重复比如节点1的编号是1.1、1.2、1.3节点2的编号是2.1、2.2、2.3。比较时先比整数部分再比小数部分。编号单调递增很重要否则乱序会让Acceptor的承诺失效。但Paxos不要求全局时钟同步只要求每个Acceptor比较大小记住自己见过的最大编号。这里有一个容易混淆的点编号越大越“新”但新提议不一定赢——如果旧值已经被多数派批准新提案即便编号很大也必须沿用旧值。编号的作用只是“谁先拿到锁”而不是“谁的值更正确”。工程实现里还有一个细节Proposer收到拒绝后需要重新生成更大的编号再次尝试。如果每次都只是“编号1”在并发场景下很容易被另一个Proposer压住。所以成熟实现里往往会用一个“跳跃式递增”的生成策略比如每次在上一次见过的最大编号上增加一个随机增量减少互相踩脚的概率。3.3 活锁两个Proposer互相“让路”导致谁都无法赢Paxos在无故障但有并发的情况下可能出现活锁。想象两个Proposer P1和P2。P1发出Prepare(1)一部分Acceptor答应P2发出Prepare(2)因为编号更大这些Acceptor转而答应P2。P1一看自己拿不到多数派就重新Prepare(3)Acceptor一见3比2大又转去答应P1。就这样P1、P2轮番用更大的编号打断对方没有一个能进入Accept阶段永远选不出值。活锁不是死锁系统没挂但什么都没做出来像两个人打乒乓球球永远落不了地。工程上通常引入一个Leader只有Leader能发起Prepare其他Proposer只在Leader挂了之后才有机会接管。这就是Multi-Paxos里“选主”的意义。学Basic Paxos时你可以不实现Leader但必须知道活锁的存在面试被问到“Paxos会不会死锁”的时候能不能答出“不会死锁但可能活锁工程上用Leader选举规避”这就能看出你是真懂还是背了概念。3.4 Learner值已经定了但谁去通知大家Basic Paxos还有一个角色Learner。Acceptor批准了一个值之后Learner要负责把结果扩散出去。最简单的方式是每个Acceptor都告诉每个Learner但消息量太大实际系统里常常指定一个“主Learner”由它汇总后再广播。如果主Learner挂了再指定新的。这个部分不是Paxos安全性的核心但工程上绕不开。很多讲解把它略过了导致读者以为Paxos就只有两轮投票那两拨角色其实还有一个Learner在背后干活。你在看一些Paxos实现源码时如果只盯着Prepare和Accept很可能看不懂那个单独运行的“广播协程”在干什么——那就是Learner在推动结果扩散。4. 从Basic Paxos到Multi-Paxos真实系统怎么把它跑起来4.1 Basic Paxos的缺陷每条命令都要两轮投票如果你直接拿Basic Paxos来同步数据库的每条写操作性能会很糟糕。因为每次写请求都要先Prepare再AcceptPrepare是额外的一轮RTT而且每次都可能被其他Proposer打断活锁风险很高。所以真实系统里用的是Multi-Paxos把一堆Paxos实例串成日志流让每个写操作对应一个日志槽位然后复用一个长期有效的Leader把Prepare阶段省略掉。这里补充一下为什么可以省略Leader确定之后它发起Prepare(编号N)并获得多数派应答这个“锁”理论上可以一直持有。后续的每个日志槽位Leader直接发Accept(编号N, 值)即可不用再重新Prepare。只有当Leader挂了或者有Proposer用更大的编号打断了它才需要重新走Prepare。这就是“Multi-Paxos把两阶段的成本压低到一阶段”的原因。一个常见的疑问是“既然Leader已经定了为什么还要编号直接写不就行了”答案是Leader不可能永远不挂。一旦它挂了另一个节点接管时必须通过编号机制确认自己掌握的是最新状态不能覆盖已批准的值。编号在这里相当于“Leader的任期凭证”谁编号大谁说话算话这个设计在日志复制里非常关键。4.2 日志副本、状态机与线性一致性Multi-Paxos的模型是每个节点维护一份日志日志的每个条目对应一个Paxos实例所有节点按相同顺序把日志应用到状态机比如KV存储。只要日志顺序一致状态机就一致。这个思路叫复制状态机。例如一个KV数据库日志槽位1Paxos实例选出值“set balance100”日志槽位2Paxos实例选出值“set balance90”所有节点按顺序执行这两条最终各自状态一致。要达到线性一致性客户端需要等日志条目在多数派落盘后再返回成功如果客户端读到旧数据也需要通过某种方式确认自己读到的不是被覆盖的值。传统数据库的“读已提交”“可重复读”这类隔离级别在分布式场景里就需要这套机制来兜底。这里我特别想强调“顺序”二字。Paxos本身只保证“每个槽位的值被唯一决定”它不保证槽位之间按什么顺序填写。所以Multi-Paxos实现里通常有一个“槽位补全”过程Leader知道哪些槽位已经定了哪些还空着需要在拿到新写请求时顺便推进空槽位的填值。这部分工程细节比Basic Paxos本身复杂也是实现Multi-Paxos最容易出错的地方。4.3 Raft、ZAB和Paxos的关系很多人分不清Paxos和Raft。简单说Raft是Paxos在工程上的一个变体它的目标就是“让Paxos变得更容易理解和实现”。Raft做了两件事强制选主并且把“日志复制”和“投票”绑在一起降低了理解成本。ZABZooKeeper里用的协议也是类似思路强调单一主节点和事务顺序。但Raft和ZAB都有自己的局限性比如它们更强调Leader模型如果Leader频繁抖动集群性能会受影响Paxos本身更偏向理论框架它说的是“存在这样一个通用模式能达成共识”不限定你如何选Leader。所以经常有这种说法学懂了Paxos再看Raft会觉得很轻松只学Raft碰到Paxos的变体还是会懵。我觉得反过来也算成立——理解Raft的Leader选举和日志复制再回头看Paxos的编号和多数派会容易很多。两者是互补的不是对立的。4.4 现实中哪些系统在使用PaxosGoogle的Chubby是分布式锁服务论文明确说用Paxos做副本一致性Google的Spanner内部利用Paxos做跨数据中心同步部分分布式数据库的底层一致性协议也直接或间接受到Paxos影响。如果你查资料还会看到PacificA、Vertical Paxos、Cheap Paxos这些变体它们都是Paxos在不同约束条件下的工程化改造。读懂Basic Paxos后再看这些就是按图索骥不会再有“每个字母都认识连起来不知道在说啥”的感觉。5. 学习Paxos最容易被绕晕的几个坑以及我的建议5.1 坑一把“批准”和“提交”搞混Paxos里的“批准”chosen是指一个值被多数派Acceptor接受这跟“客户端看到结果”是两回事。值被批准后Learner还要花时间把它通知给所有节点。如果某个节点没收到通知它可能还会问“现在系统里这个值到底是什么”——这个问题通过查Learner或发起新一轮Paxos来解决。很多初学者以为“多数派接受了就是所有人都知道了”于是不理解为什么后面还要有Learner这一步。我当时也栽在这里后来自己用Python写模拟器时发现要把Learner的通知逻辑写对其实比两轮投票本身还要费功夫。5.2 坑二以为Paxos能保证“不丢数据”Paxos保证的是“多数派已经批准的值不会被别的值覆盖”不等于“所有已批准的数据永久不丢”。如果写成功返回了但这个值只存在于少数派节点上等系统经过多个轮次之后那些节点都挂了这个值可能事实上丢失因为没有任何节点还记得它了。这涉及“持久化”和“复制因子”的问题Paxos不是备份策略。生产中你依然需要把数据落到多数派才能在多节点同时宕机时扛住。5.3 坑三把“假定多数派可用”当成Paxos的硬性要求Paxos有个前提活着的Acceptor构成多数派否则无法继续达成共识。这不是Paxos的缺点而是任何容错系统的数学边界。三节点集群挂两台系统当然不能继续产生新共识但好消息是已批准的值仍能被读出来假如另一个节点还活着。理解这个边界你在设计运维策略时才知道该把集群规模设成多大、容忍几台节点宕机。5.4 我的学习路径建议如果让我重新学一遍Paxos我会这样安排先别看论文。先看Raft的动画演示把Leader选举和日志复制弄明白再看Basic Paxos的讲解用一个最简单的三节点例子手推两轮投票动手写一个简化版的Multi-Paxos模拟器不要求性能只要正确性——比如用内存map模拟三个节点跑并发请求、模拟宕机和分区看日志顺序是否始终一致最后回头看Leslie Lamport的论文原文你会发现已经能读懂大半了。我当年走了弯路一上来就读论文被各种数学符号和严格定义劝退多次。后来冷静下来先推演三节点场景再回看论文才发现每一段其实都有对应的物理直觉。Paxos说到底是“少数服从多数说话算话”这两条简单原则在分布式环境下的严格化。写到这里分享一个我自己做过且很受益的小实验用Python写一个三节点的Basic Paxos模拟器故意让两个Proposer并发发起提案跑一万轮日志顺序从不冲突。那个瞬间你会真正理解“多数派交集”这个数学性质的价值。如果你也在学Paxos建议别只读动手推演一遍。纸上推演真的比看十篇博客都有用。