☰
ZAB不是Paxos变体:共识与全序广播的分野解析
2026/9/30 7:51:08 网站建设 项目流程

1. 血缘:ZAB为什么会被误读成“Paxos的一支”

1.1 最流行的那句“错误答案”

我面试分布式系统工程师的时候,几乎每三个候选人就有两个会说出同一句话:“ZooKeeper里用的ZAB协议,是Paxos的一种变体实现。”问细一点,他们会补充:ZAB也是两阶段提交、也有过半机制、也有类似leader的角色,所以本质上就是把Paxos改成了能选主的版本。

这个回答不能算全错,但它掩盖了最有价值的信息。ZAB和Paxos确实是分布式一致性协议这个大家族里的近亲,两者共享“多数派”“两轮投票”“提议编号”这些关键基因。可ZAB的官方论文是《A Simple Totally Ordered Broadcast Protocol》,标题里根本没有Paxos三个字——它要解决的事情,从一开始就和Paxos是两条路线。先把血缘讲清楚,再看分野才有意义。

1.2 被忽略的“另一个祖先”:Viewstamped Replication

很多人把ZAB的出身简化成“Paxos的儿子”,但实际家谱要复杂一点。ZAB论文的作者自己也承认,它既受到了Paxos的启发,也受到了Viewstamped Replication(VR)的影响。VR诞生于1990年代,是最早一批把“全序广播”直接作为第一公民来设计的复制协议,它有清晰的primary节点、view编号、视图切换流程。这些概念在ZAB里几乎都能找到影子:leader对应primary,epoch对应view number,崩溃恢复对应view change。

所以准确地说,ZAB不是Paxos的直系后代,而是“Paxos的思想 + VR的框架”这两条血脉合流后的产物。它继承了Paxos的法定多数和两阶段提交骨架,又继承了VR的leader中心化、视图切换、日志同步设计。这个背景解释了为什么ZAB读起来既像Paxos又明显不是Paxos——它本来就是另一套设计目标的产物。

1.3 血缘的真正焦点:法定多数,而不是“Paxos算法”

再往深挖一层,ZAB和Paxos血缘的真正交点,是“法定多数(quorum)”这个概念。在分布式系统里,任何两个多数派集合一定有交集,这是几乎所有容错协议的地基。Paxos靠这个交集保证决议不可翻转,ZAB靠这个交集保证上一任leader留下的“可能已提交事务”不会丢失。

我之前遇到一位同事,讨论时坚持说“ZAB就是Paxos,只是改了名字”。我当时给他的回复是:你把“多数派”当成了Paxos的本质,但多数派只是手段。Paxos抓住的是“对一个值达成一致”的抽象,ZAB抓住的是“把所有值按同一个顺序广播出去”的抽象。分母一样,分子完全不同。掌握了这句话,才算真正理解了血缘在哪里、分野在哪里。

2. Paxos的投票骨架:单个值如何达成共识

2.1 Prepare/Promise/Accept:三次消息里藏着两条纪律

要对照ZAB,得先把Paxos这个“共识操作”的底层机制摸透。最基本的Basic Paxos只有两个阶段、三类角色:Proposer、Acceptor、Learner。它解决的核心问题是:在可能宕机、可能网络分区的环境下,让一组节点对某个value达成一致,并且已经达到的一致结论不允许被篡改。

我习惯用伪代码去看Paxos的骨架,否则只看文字很容易绕晕:

Phase 1(Prepare/Promise) Proposer -> Acceptors: prepare(ballot=n) Acceptor: 如果 n > 自己见过的最大ballot: 承诺不再接受任何编号小于n的提案 回复 promise,并附上自己接受过的最高编号提案 否则: 拒绝或保持沉默 Phase 2(Accept/Accepted) Proposer: 如果收到多数派的promise: 取出其中编号最高的value(如果有则沿用,没有则自由决定) 向所有Acceptor发送 accept(n, value) Acceptor: 如果 n 仍 >= 自己见过的最大ballot: 接受该value Learner: 从多数派那里确认某value已被接受,完成学习

两条纪律就藏在Acceptor的行为里:第一,只回应编号更大的prepare,并且此后屏蔽更小编号的提案;第二,接受了某个提案之后,如果未来新proposer来问,必须如实把最高编号的value交回去。第一条纪律阻止了老提案“插队”,第二条纪律保证了“已经被多数派接受的值,后来者不能装不知道”。

2.2 为什么两条纪律就能防止“翻盘”

这里有一个经常被追问的问题:为什么Paxos能保证决议一旦达成,就不会再被覆盖掉?关键在两阶段之间的交集性。

假设一个value已经由某个ballot被多数派接受,那它被“决定”的集合至少覆盖了3个节点(在5节点集群里)。此时另一个Proposer带着更大的ballot号发起prepare,它拿到的多数派反馈里一定会碰到至少一个接受过那个值的Acceptor。根据纪律二,这个Acceptor会把旧value交回去,新Proposer在Phase 2就不得不继续提交这个旧value,而不能换一个新值。

我用一个生活化类比解释给团队里的新人听:Paxos像一次陪审团裁决,第一轮投票是“我要不要保留王某的证词”,一旦多数人确认了证词内容,后来换一个主持人重新组织投票,也必须在这个证词的基础上继续,而不是推翻重审。你可以质疑过程、提高票号,但不能在裁决已定之后把已认可的证据换成别的。

2.3 Multi-Paxos:给Paxos加上“稳定阶段”的工程补丁

Basic Paxos在任何一轮提案上都跑全套两阶段,性能差且存在著名的活锁问题——两个Proposer互相提高ballot数抢占,导致谁也无法完成Accept阶段。工程上通常不是直接布署Basic Paxos,而是把它改造为Multi-Paxos:先选出一个稳定的leader,让leader在任期内优化的执行路径上跳过Prepare阶段,直接发Accept。

这正是“Paxos是共识引擎、不是完整复制协议”的体现。Multi-Paxos还需要自己做leader租约、实例序号分配、日志本地store、快照与日志截断等一系列工程决策。协议只保证了“每个实例内决议被多数派认可”,至于日志条目按什么顺序排列、leader换了之后新leader从哪个日志位置继续,Paxos本身并不强制规定,纯粹靠上层实现自己拼装。

这部分功夫容易被人低估,也是后来ZAB、Raft纷纷试图“把上层细节内建进协议”的根本动机。

3. ZAB的广播基因:全序是怎么被“内建”到协议里的

3.1 三阶段轮廓:发现、同步、广播

ZAB交给ZooKeeper的,不是得到一个共识结果,而是得到一条严格有序的事务流。它的运行周期可以拆成三个连续阶段:发现(Discovery)、同步(Synchronization)、广播(Broadcast)。论文把这三个阶段统称为“atomic broadcast”,意思就是:所有服务器按同一顺序应用同一批事务。

  • 发现阶段:新选出的leader带着自己的epoch,向各follower收集它们的最近历史记录。目的是搞清楚集群里目前已经有哪几笔可能已提交的事务。
  • 同步阶段:leader拿到完整历史后,让那些落后的follower补上缺失的日志;如果某些follower有leader不承认的多余记录(通常是旧leader发出但没能提交的),按规则丢弃或回滚。
  • 广播阶段:集群进入正常服务,所有写请求由leader分配全局单调递增的zxid,广播给follower,等多数确认后commit,再广播commit消息。

这里最值得注意的细节是:leader只有在前两个阶段确认“所有follower的日志不落后于已提交历史”之后,才允许进入广播阶段对外提供写服务。也就是说,ZAB的每一次leader上任,都必须先向“过去”对齐,才能打开“未来”。这种“先补齐再对外”的设计,是保证全序广播安全性的命门。

3.2 zxid:把“epoch + 序号”直接写进每个提案

ZAB的全序能力,很大程度来自一个精心设计的数据结构:zxid(ZooKeeper Transaction ID)。它是64位整数,拆开来看:

  • 高32位是epoch,代表当前leader的任期编号;
  • 低32位是counter,代表该leader任期内的事务序号。

每个新leader当选,epoch加1,counter归零。任何两个事务的先后顺序只需比较zxid:先比epoch,再比counter。这个设计让协议的“顺序信息”自带在消息里,不需要额外维护一套全局时钟。

反观Multi-Paxos,日志顺序通常靠“实例编号”来区分——核心思想其实是等价的,但这个实例编号的分配细节常常是每个实现自己定制的,协议层面没有一个统一、内建的定义。ZAB把顺序直接焊进了每个提案的头部,这也是它被称为total order broadcast的底气。

3.3 同一轮广播中,FIFO如何不被打破

ZooKeeper对外承诺的“顺序一致性”,很大程度上依赖ZAB的这个内建顺序。一个session内的多个写请求,会按客户端发起的先后被leader赋予递增的counter,然后按这个顺序广播给follower。follower收到后不能乱序提交,必须按zxid从小到大落盘。

我在排查线上问题时遇到过一种典型误解:有人觉得ZooKeeper的读请求也线性一致,于是用它做分布式锁之外的“配置读取”。其实ZooKeeper的读请求在没有调用sync的情况下可能读到旧数据,它严格保证的是“写路径上的全序”。如果对读一致性有硬性要求,要么走sync路径,要么接受会话内的FIFO语义。这个边界是ZAB协议的使用者们最容易踩空的地方,后面讲工程选型时还会再展开。

4. 分野的真正本质:共识、全序广播与状态机复制

4.1 Paxos提供“点”,ZAB提供“线”

现在可以正面回答标题里的“分野”了。Paxos的原始抽象是“单值共识”:一群节点对一个value给出一个确定的、不可篡改的答案。你可以把它理解成一个“点”。ZAB的原始抽象是“全序广播”:一群节点对一串按顺序排列的操作日志达成一致。你可以把它理解成一条“线”。

“线”当然可以借助“点”拼出来——把日志的每个位置都当成一个独立的Paxos实例,决定这个位置上放哪条操作,这就是Multi-Paxos的工作原理。但这不是唯一的拼法,ZAB选择的是另一条路:不把日志切成孤立的Paxos实例,而是直接让协议本身维护“整个日志”的连续性。ZAB用一个统一的epoch来界定leader任期,用zxid把顺序编码进每个事务,在处理leader切换时直接定义好历史日志的合并规则。

4.2 leader角色的本质差异:加速器 vs 路由中枢

Paxos里的leader(或者说被选出的稳定Proposer)只是一个优化项。理论上Basic Paxos没有leader也能正确运行,只是会产生活锁;有了稳定leader,多数阶段可以省略Prepare,吞吐显著提升。一旦leader挂掉,协议依然能通过新ballot的Prepare恢复推进,没有谁是不可替代的。

ZAB里的leader则不同,它不只是“性能加速器”,而是协议运行结构本身的一半。没有leader,就没有zxid的连续性,没有广播发起的起点,浏览器的epoch也没有来源。ZAB的leader更像是“路由中枢”:所有写操作都要经过它,它负责给事务排队、编号、广播、收集确认。它的身份是协议运行的必须组件,而不是可选的性能附件。

4.3 一个比喻:食堂打菜 vs 后厨流水线

如果觉得抽象,可以用一个生活比喻把两类协议的区别装进头脑里。Paxos像是食堂里的“打菜窗口裁决”:每次打菜规则临时约定,几个窗口的师傅抢着提出自己的菜单,谁拿到了多数票谁就定今天的菜。即使今天菜单变了、窗口师傅也换了,已经出过的菜不会有人推翻——但每一位新师傅上任都可能需要重新换一轮票。

ZAB更像“后厨流水线”:车间里必须有一个固定的工段长(leader)负责排工序,每道菜(事务)出锅前都要打上一个唯一的流水号(zxid)。工段长换班时,必须先把上一班已经出过的菜数和菜品记录清点一遍(发现与同步),确认账目一致之后,流水线才重新开动。前者重视“裁决的可靠性”,后者重视“工序的一致性和顺序性”。

5. 崩溃恢复现场:两种协议对待“旧leader之死”的差异

5.1 Paxos的恢复路径:新一轮ballot,多数派说了算

Paxos的崩溃恢复没有一个叫“选主”的强制步骤,只有新一轮共识的启动。假设旧leader在提交某条日志后宕机,新的Proposer只要发一轮更高的ballot号Prepare,拿到多数派反馈,就能知道之前是否已经有value被多数派接受。如果有,它只能循着旧value补交;如果没有,它就可以自由写入新值。

这个过程很优雅,但代价是:每次真正的leader故障都需要重新执行Prepare,且旧leader如果又活了,它的低ballot号请求会被所有已经看到新ballot的节点拒绝。有些实现不额外引入租约机制就无法避免“双主脑裂”的假象——两个Proposer都以为自己是当前主节点,造成客户端请求被短暂中断。Paxos的安全不依赖leader,但这种不依赖也意味着它把leader管理、租约、恢复策略统统推给了外面那一层。

5.2 ZAB的恢复路径:epoch隔离旧主,zxid确定同步基准

ZAB的崩溃恢复是一次结构化的leader切换,而不是一次临时发起的consensus。新leader当选后,它的epoch会高于旧leader,所有旧leader的proposal在比较zxid时天然排在后面,不必担心旧leader恢复后干扰广播顺序。

从同步角度看,新leader会询问每个follower的last zxid,然后按两种方向修正历史:

  • leader有、follower没有的已提交事务,会补发给follower,让落后的节点追上。
  • follower有、leader没有的未提交事务(例如旧leader广播给某个follower、但未完成多数确认),会按规则丢弃,因为在旧epoch里它并没有被正式commit。

这个处理让ZAB恢复后的日志集合,恰好等于“所有曾经可能被commit的事务”的并集,按zxid排序后就是完整且唯一的全序历史。

5.3 一个具体推演:x=1 到底会在哪些情况下幸存

为了看清两者处理路径的差别,我常和同事推演一个三个节点的具体场景。设节点A是旧leader,B、C是follower:

  • 场景一:A收到客户端写请求x=1,向B、C广播proposal,B确认,C还没收到,A宕机。此时x=1只存在于A和B,未提交。ZAB的新leader在B和C中选出,比如B当选。B的last zxid高于C,B会向C补齐x=1,并在新epoch内再次发起commit,最终x=1成功提交。这跟Paxos“把可能已接收的值延续下来”的安全语义是一致的。
  • 场景二:A向B广播了x=1,B还没确认,A就宕机。集群内存活的B、C都没有见过这个proposal,新leader在B或C中产生,x=1就彻底消失了,这没毛病——因为从未被多数派接受,就从未成为可提交事务。
  • 场景三:x=1已经在A、B、C三个节点都被确认,A在commit前宕机。新leader从B或C选出后,日志里有x=1,它不需要重复提交,只需在新epoch里继续对外服务,所有节点已经拥有这条记录。

这三个场景直观说明了一个事实:ZAB和Paxos在“已被接受但未提交”“从未被多数派看到”“已提交”三种状态上的处理结果几乎一致,因为它们共享多数派的安全底线。不同的是机制:Paxos依赖新ballot携带的旧值继续补票,ZAB依赖醒目而直接的zxid同步规则来决定补发还是截断。从工程实践的角度看,ZAB的恢复逻辑更容易推理,这也是ZooKeeper在实施上比许多自研Multi-Paxos更简洁的原因之一。

下面用一张表把恢复路径的分野再压缩一下:

对比维度Paxos(Basic/Multi)ZAB
恢复入口新一轮Prepare,发现已接受的旧值新leader当选,epoch+1,先同步历史再广播
顺序来源由上层的实例编号/日志位置决定由zxid(epoch+counter)内建决定
leader作用可选,只是防止活锁和提升效率的优化必须,协议结构的一部分
旧leader恢复低ballot号被拒绝,依赖外部租约防双主检测到更高epoch,自动降级为follower
未提交事务若未获得多数派则被自然放弃若在旧epoch未被commit,新epoch会截断
安全性核心多数派交集 + 高编号覆盖低编号多数派交集 + epoch单调 + zxid全序

6. 工程选型不是看热度:ZooKeeper与Paxos系统的真实取舍

6.1 为什么ZooKeeper选择ZAB而不是Paxos

ZooKeeper解决的是分布式协调问题:分布式锁、队列、元数据发布订阅、故障检测。这些场景的全部价值都建立在一个词上:顺序。两台客户端先后创建同一个锁节点,谁先成功必须被所有观察者以相同的先后次序看到。ZAB的全序广播正好把这个顺序作为第一公民写进协议,使用起来不需要额外约定。

如果ZooKeeper当初直接选Multi-Paxos,理论上也能做,但代价是要在协议外面再设计一套“日志实例排序”“新leader上任后的恢复规则”“follower落后时的追赶流程”。ZAB把这些写进了协议本身的定义里,工程实现者(这里是雅虎团队)只需照着规范实现,不必在论文与现实之间反复试错。这是我理解ZooKeeper选型时最重要的一条逻辑。

6.2 为什么Google选择了Paxos而不是ZAB

Google之路是另一套取舍。Chubby、Spanner等系统的底层都需要一个非常通用的复制状态机库,Paxos作为共识引擎,优势是抽象层次低、适用范围宽。你想在状态机模型上叠事务、叠锁、叠租约、叠配置变更,这些都可以在Paxos外面自由发挥;如果你的底层协议已经把“顺序”和“leader”焊死了,反而限制了更上层定制。

请注意,这里不存在“谁更高级”的分高下结论。ZAB是把顺序语义做成协议的一部分,Paxos是把共识做成一切上层建筑的模块。两者服务的目标系统不一样,因此各自是各自场景里的正确答案。

6.3 我的选型经验:先问你要“共识”还是“全序广播”

做架构选型时,我一般建议团队先问自己一个问题:你的分布式系统需要的是“对某个值定案”,还是“对一组操作排序执行”?

如果你的核心诉求是“所有节点对某个决策不篡改、不强覆盖”,那你需要共识抽象,可以先考虑Multi-Paxos的实现或者类似抽象的系统。如果你的核心诉求是“多个节点按同一顺序执行一串操作”,那你需要全序广播抽象,ZAB和Raft这类协议比裸Paxos更贴题。很多团队把ZooKeeper当KV来用,把etcd当锁服务来用,没有看清底层协议的设计目标,最后在性能、一致性边界上撞得头破血流,问题多半在选型的第一天就埋下了。

就我个人的实际操作体会来说,还有一点特别想说:学习Paxos和ZAB时,与其死记硬背算法步骤,不如时刻逼自己回答“这个协议到底在保护什么不变量”。Paxos保护的是“决议一旦多数确认就不可篡改”,ZAB保护的是“旧epoch的历史必须接得上、新epoch的事务必须按zxid全序发送”。这两句话记住了,任凭面试官怎么绕,你都能把话题扳回关键分歧点;做工程时遇到疑难,也更容易从协议不变量反推出故障原因。这是我自己从反复调试、排障、重读论文里得到的最大收获,也是我认为理解这两套协议真正值得投入时间的地方。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询