Zab — ZooKeeper 怎么把客户端写入按顺序复制到所有副本
待复核Zab(ZooKeeper Atomic Broadcast)是一个让”一组机器互相抄作业、抄得一字不差”的协议。日常类比:像班里的课代表抄板书——老师(primary)写一句、课代表们(backup)必须按同样顺序抄到自己本子上,谁中途换人,新课代表也得先把之前的板书补齐才能继续。
ZooKeeper 是 Hadoop / Kafka / HBase 都依赖的”协调服务”,存的是配置、锁、leader 标记这些小数据。这些数据要求”任意机器读到的状态是一致的”,所以底下必须有协议保证写操作的全序复制——这就是 Zab。它跑在 TCP 之上(保证两节点之间消息 FIFO),上层暴露三个保证:可靠送达、全序、因果序(先发生的事必须先被看到,不能颠倒)。
简短示意:
client ──写──▶ leader ──propose──▶ followers ▲ │ │◀────── ack ──────────┘ │ (多数派 ack 后) │ └──── commit ────▶ followers每条写入要走”propose → 等多数派 ack → commit”两次往返,但两节点间用 TCP,message 不会乱序,所以协议本身不再处理”消息丢失/乱序”,只处理”节点崩溃/换主”。
不理解 Zab,下面这些事都没法解释:
- 为什么 Kafka 早期版本要先装 ZooKeeper 才能跑——因为 controller 选举、ISR 列表都靠 Zab 复制
- 为什么 ZooKeeper 论文比代码晚了好几年——先有生产实现再补形式化,Zab 是”工业先行”的典型
- 为什么 Raft 论文里说”我们写 Raft 是因为 Paxos 难懂”——Zab 也是”想把 Paxos 改造得能用”的另一条路
- 为什么 ZooKeeper 集群要求奇数节点(3、5、7)——Zab 用多数派 ack 提交,奇数节省机器还能容忍同样数量故障
论文里 Zab 本体是三阶段(发现 → 同步 → 广播);领导选举由外部 oracle 选出候选人,选完再进三阶段。zxid 是贯穿全程的编号,不是第四阶段:
-
领导选举(oracle):节点投票选”zxid 最高”的当候选人。类比:选班长选笔记最全的那个,避免新班长丢老板书。
-
发现 → 同步:发现阶段定新 epoch 并收齐 quorum 历史;同步阶段把 follower 对齐到该历史(可截断冲突尾部再补缺),完成后 leader 才算 established。类比:先盘点谁缺哪几页、撕掉抄错的尾页,再把缺页发齐。
-
广播 + zxid:日常写入。leader 分配 zxid(高 32 位 epoch、低 32 位 counter),propose → 多数派 ack → commit。新任期 epoch+1、counter 归 0;比较时先比 epoch 再比 counter。类比:老师写一句、多数课代表点头后才作准。
案例 1:用 zxid 比较两个节点谁的日志更新
Section titled “案例 1:用 zxid 比较两个节点谁的日志更新”选举时每个节点把自己最大的 zxid 报出来,谁大谁当 leader。
def zxid_greater(a, b): # zxid 是 64 位整数,高 32 位 epoch,低 32 位 counter a_epoch, a_counter = a >> 32, a & 0xFFFFFFFF b_epoch, b_counter = b >> 32, b & 0xFFFFFFFF if a_epoch != b_epoch: return a_epoch > b_epoch return a_counter > b_counter逐部分解释:
>> 32是右移 32 位,把高位的 epoch 单独取出来& 0xFFFFFFFF是只保留低 32 位,取出 counter- 先比 epoch 再比 counter:epoch 大意味着”经历过更晚的任期”,比 counter 多算更新
案例 2:模拟新 leader 把日志同步给 follower
Section titled “案例 2:模拟新 leader 把日志同步给 follower”def sync_follower(leader_log, follower_log): # 教学简化:先截断 follower 上与 leader 历史冲突的未提交尾部,再补缺 cut = len(follower_log) while cut > 0 and follower_log[cut - 1] not in leader_log: cut -= 1 missing = [e for e in leader_log if e not in follower_log[:cut]] return follower_log[:cut] + missing逐部分解释:
leader_log是新 leader 选定的初始历史(按 zxid 升序)- 先截断:follower 若有未提交的分叉条目,不能只追加,要先撕掉冲突尾部
- 再补缺:把 leader 有、follower 没有的条目一次性发过去,保证不漏已 commit 的写入
案例 3:ZooKeeper 在 Kafka 中干什么
Section titled “案例 3:ZooKeeper 在 Kafka 中干什么”broker 启动 → 在 /brokers/ids/<id> 写一个临时节点controller 选举 → 谁先在 /controller 写成功谁当controller 监听 /brokers → 节点掉就触发 ISR 更新逐部分解释:
- 临时节点的”创建/删除”事件本质是 Zab 全序广播——所有 broker 看到的事件顺序一致
- “谁先写成功”靠 Zab 的全序保证:两个 broker 同时写 /controller,Zab 排序后只一个赢
- 老 Kafka(< 2.8)所有 metadata 走 ZooKeeper;新版 KRaft 自带 Raft,但生产里 ZooKeeper 路线还在跑
- 这就是 Zab 在生产里的常见姿势:上层只调 ZooKeeper API(create / setData / watch),下面 Zab 替你保证”所有 broker 看到的状态序列一致”
- 误以为 Zab 就是 Paxos — Zab 是主备协议,强调 primary-order(同一 primary 内的顺序在所有副本一致),不是对称共识,protocol 行为模型差很多。
- 忽略 zxid 的双层结构 — 直接当整数比较没问题,但 epoch 切换时如果 counter 不重置就会错乱,新 leader 必须 epoch+1、counter=0。
- 把 leader election 当 Raft — Zab 选 zxid 最高的节点防丢已提交日志,Raft 用随机超时;两者目标类似但实现机制不同。
- 误认为读也走共识 — Zab 只对写做多数派 ack,读可从任意 follower 返回(换吞吐);需要最新值时调 ZooKeeper 的
sync先追上 leader。写吞吐仍受限于 leader 磁盘 fsync。
适用 vs 不适用场景
Section titled “适用 vs 不适用场景”适用:
- 协调服务:分布式锁、leader 选举、配置同步、服务发现(典型 ZooKeeper / etcd 类负载)
- 写远少于读、单条数据小(KB 级)、要求强一致的”元数据”系统
不适用:
- 大数据量主复制(GB 级 blob):Zab 走 leader 单点磁盘,吞吐撑不住,应选 Kafka / 对象存储
- 跨地域强一致:Zab 多数派 ack 受限于最慢节点 RTT,跨洲场景延迟过高,应看 Spanner / Raft + Paxos commit
- 客户端不需要全序、只要”最终一致” → 用 Dynamo / gossip 更便宜
历史小故事(可跳过)
Section titled “历史小故事(可跳过)”- 2007:雅虎研究院开始做 ZooKeeper,先有代码在生产跑(Hadoop 调度依赖它)
- 2008-2010:协议没正式论文,但代码已经支撑了大量生产集群
- 2011:Junqueira、Reed、Serafini 在 DSN 会议发表 Zab 论文,把已运行 4 年的协议形式化
- 2014:Raft 论文出现,作者明确说”Paxos 难懂”——Zab 也是同一个动机的另一条路
- 2021:Kafka 推出 KRaft 模式,开始去 ZooKeeper 化;但 ZooKeeper 在 Hadoop / HBase 生态仍是默认选择
有个有趣的旁注:Zab 论文比代码晚 4 年发表,这种”工业先行”在分布式系统圈很常见——Chubby、Borg 都是”先在公司里跑稳,再写论文给学界”。先证明再实现 vs 先实现再形式化,两条路都能产出可用协议。
- 共识不止 Paxos 一条路:主备复制(primary-backup)+ FIFO 通道 + 多数派 ack 也能做到全序,且更直观
- “工业先行”也能产出好协议:Zab 是先有 4 年生产再有论文,不是先证明再实现
- zxid 双层结构是个通用招式:把”任期”和”序号”分开,跨任期比较立刻清楚,etcd 的 term/index 同源思路
- 读不走共识是吞吐关键:但代价是读可能落后于最新写,业务要清楚自己能不能接受
- 协议设计可以”借底层力”:Zab 把 FIFO 这块脏活外包给 TCP,自己只管崩溃恢复,比”什么都自己保证”的协议简单很多
- 论文 PDF:Zab DSN 2011
- ZooKeeper 官方文档:zookeeper.apache.org
- 视频:ZooKeeper Internals (CMU 15-445)
- 对比阅读:paxos-1998 / paxos-simple-2001 / raft
- 工程实现:ZooKeeper 源码
org.apache.zookeeper.server.quorum
- paxos-1998 —— Paxos 是 Zab 设计的对照组,Zab 论文重点解释为何不直接用 multi-Paxos
- paxos-simple-2001 —— Lamport 自己写的简化版 Paxos,可以先读它再看 Zab 怎么改造
- raft —— Raft 走”易懂”路线,Zab 走”主备复制”路线,三者解决同一类问题
- lamport-1978 —— 全序广播的理论起点(逻辑时钟),Zab 的全序保证可追到这
- chubby —— Google 版协调服务,ZooKeeper 是它的开源对应物
- spanner —— 跨地域强一致,对比 Zab 单数据中心场景的取舍
- brewer-cap-2000 —— Brewer CAP — 网络一断电,一致性和可用性只能留一个
- gfs —— GFS — 为工作负载反向定制的分布式文件系统
- hdfs-2010 —— HDFS — 把 GFS 用 Java 重写一遍并撑到 25 PB
- kafka-2011 —— Kafka NetDB 2011 — 把消息中间件砍成”会写文件的水管”
- linearizability-1990 —— Linearizability 1990 — 让并发对象看起来像一次只执行一个操作
- raft —— Raft — 易理解的共识算法
- smr-1990 —— SMR 1990 — 把”容错服务”还原成”多副本一起跑同一台状态机”