ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

ZAB不是Paxos变体:共识与全序广播的分野解析

ZAB不是Paxos变体:共识与全序广播的分野解析 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 ReplicationVR的影响。VR诞生于1990年代是最早一批把“全序广播”直接作为第一公民来设计的复制协议它有清晰的primary节点、view编号、视图切换流程。这些概念在ZAB里几乎都能找到影子leader对应primaryepoch对应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 1Prepare/Promise Proposer - Acceptors: prepare(ballotn) Acceptor: 如果 n 自己见过的最大ballot: 承诺不再接受任何编号小于n的提案 回复 promise并附上自己接受过的最高编号提案 否则: 拒绝或保持沉默 Phase 2Accept/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的全序能力很大程度来自一个精心设计的数据结构zxidZooKeeper Transaction ID。它是64位整数拆开来看高32位是epoch代表当前leader的任期编号低32位是counter代表该leader任期内的事务序号。每个新leader当选epoch加1counter归零。任何两个事务的先后顺序只需比较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 一个具体推演x1 到底会在哪些情况下幸存为了看清两者处理路径的差别我常和同事推演一个三个节点的具体场景。设节点A是旧leaderB、C是follower场景一A收到客户端写请求x1向B、C广播proposalB确认C还没收到A宕机。此时x1只存在于A和B未提交。ZAB的新leader在B和C中选出比如B当选。B的last zxid高于CB会向C补齐x1并在新epoch内再次发起commit最终x1成功提交。这跟Paxos“把可能已接收的值延续下来”的安全语义是一致的。场景二A向B广播了x1B还没确认A就宕机。集群内存活的B、C都没有见过这个proposal新leader在B或C中产生x1就彻底消失了这没毛病——因为从未被多数派接受就从未成为可提交事务。场景三x1已经在A、B、C三个节点都被确认A在commit前宕机。新leader从B或C选出后日志里有x1它不需要重复提交只需在新epoch里继续对外服务所有节点已经拥有这条记录。这三个场景直观说明了一个事实ZAB和Paxos在“已被接受但未提交”“从未被多数派看到”“已提交”三种状态上的处理结果几乎一致因为它们共享多数派的安全底线。不同的是机制Paxos依赖新ballot携带的旧值继续补票ZAB依赖醒目而直接的zxid同步规则来决定补发还是截断。从工程实践的角度看ZAB的恢复逻辑更容易推理这也是ZooKeeper在实施上比许多自研Multi-Paxos更简洁的原因之一。下面用一张表把恢复路径的分野再压缩一下对比维度PaxosBasic/MultiZAB恢复入口新一轮Prepare发现已接受的旧值新leader当选epoch1先同步历史再广播顺序来源由上层的实例编号/日志位置决定由zxidepochcounter内建决定leader作用可选只是防止活锁和提升效率的优化必须协议结构的一部分旧leader恢复低ballot号被拒绝依赖外部租约防双主检测到更高epoch自动降级为follower未提交事务若未获得多数派则被自然放弃若在旧epoch未被commit新epoch会截断安全性核心多数派交集 高编号覆盖低编号多数派交集 epoch单调 zxid全序6. 工程选型不是看热度ZooKeeper与Paxos系统的真实取舍6.1 为什么ZooKeeper选择ZAB而不是PaxosZooKeeper解决的是分布式协调问题分布式锁、队列、元数据发布订阅、故障检测。这些场景的全部价值都建立在一个词上顺序。两台客户端先后创建同一个锁节点谁先成功必须被所有观察者以相同的先后次序看到。ZAB的全序广播正好把这个顺序作为第一公民写进协议使用起来不需要额外约定。如果ZooKeeper当初直接选Multi-Paxos理论上也能做但代价是要在协议外面再设计一套“日志实例排序”“新leader上任后的恢复规则”“follower落后时的追赶流程”。ZAB把这些写进了协议本身的定义里工程实现者这里是雅虎团队只需照着规范实现不必在论文与现实之间反复试错。这是我理解ZooKeeper选型时最重要的一条逻辑。6.2 为什么Google选择了Paxos而不是ZABGoogle之路是另一套取舍。Chubby、Spanner等系统的底层都需要一个非常通用的复制状态机库Paxos作为共识引擎优势是抽象层次低、适用范围宽。你想在状态机模型上叠事务、叠锁、叠租约、叠配置变更这些都可以在Paxos外面自由发挥如果你的底层协议已经把“顺序”和“leader”焊死了反而限制了更上层定制。请注意这里不存在“谁更高级”的分高下结论。ZAB是把顺序语义做成协议的一部分Paxos是把共识做成一切上层建筑的模块。两者服务的目标系统不一样因此各自是各自场景里的正确答案。6.3 我的选型经验先问你要“共识”还是“全序广播”做架构选型时我一般建议团队先问自己一个问题你的分布式系统需要的是“对某个值定案”还是“对一组操作排序执行”如果你的核心诉求是“所有节点对某个决策不篡改、不强覆盖”那你需要共识抽象可以先考虑Multi-Paxos的实现或者类似抽象的系统。如果你的核心诉求是“多个节点按同一顺序执行一串操作”那你需要全序广播抽象ZAB和Raft这类协议比裸Paxos更贴题。很多团队把ZooKeeper当KV来用把etcd当锁服务来用没有看清底层协议的设计目标最后在性能、一致性边界上撞得头破血流问题多半在选型的第一天就埋下了。就我个人的实际操作体会来说还有一点特别想说学习Paxos和ZAB时与其死记硬背算法步骤不如时刻逼自己回答“这个协议到底在保护什么不变量”。Paxos保护的是“决议一旦多数确认就不可篡改”ZAB保护的是“旧epoch的历史必须接得上、新epoch的事务必须按zxid全序发送”。这两句话记住了任凭面试官怎么绕你都能把话题扳回关键分歧点做工程时遇到疑难也更容易从协议不变量反推出故障原因。这是我自己从反复调试、排障、重读论文里得到的最大收获也是我认为理解这两套协议真正值得投入时间的地方。
返回列表