跳转到内容

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 干活分四个阶段,环环相扣:

  1. 领导选举:所有节点投票选一个”zxid 最高的”当 leader。类比:选班长选笔记记得最全的那个,这样新班长不会丢老板书。zxid 越大说明它见过越多已提交的写入。

  2. 发现 + 同步:新 leader 当选后先收齐 follower 的日志末尾,决定一个新 epoch(任期号),把自己日志里 follower 没有的部分推过去。类比:新班长上任先盘点谁缺哪几页,把缺页发下去。

  3. 广播:日常写入阶段。客户端写到 leader,leader 给写入分配 zxid(epoch+counter),propose 给所有 follower,收到多数派 ack 后 commit 并通知 follower commit。类比:老师每写一句板书,等多数课代表点头”抄好了”,再宣布”这句作准”。

  4. zxid 双层结构:每个 zxid 是 64 位,高 32 位是 epoch(leader 任期号),低 32 位是 counter(任期内递增)。新 leader 上任 epoch+1、counter 归 0。比较 zxid 时先比 epoch 再比 counter——这样跨任期不会乱。

实践案例

案例 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

def sync_follower(leader_log, follower_last_zxid):
# 找出 follower 还没有的部分(zxid 大于它的最后一条)
missing = [entry for entry in leader_log
if entry.zxid > follower_last_zxid]
return missing # leader 把这些 propose 给 follower

逐部分解释

  • leader_log 是 leader 已 commit 的所有事务列表,按 zxid 升序
  • follower_last_zxid 是 follower 上报的”我最后一条是什么”
  • 返回”缺的那段”,leader 在 sync 阶段一次性发过去;这一步保证 follower 不会漏已 commit 的写入

案例 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 看到的状态序列一致”

踩过的坑

  1. 误以为 Zab 就是 Paxos — Zab 是主备协议,强调 primary-order(同一 primary 内的顺序在所有副本一致),不是对称共识,protocol 行为模型差很多。
  2. 忽略 zxid 的双层结构 — 直接当整数比较没问题,但 epoch 切换时如果 counter 不重置就会错乱,新 leader 必须 epoch+1、counter=0。
  3. 把 leader election 当 Raft — Zab 选 zxid 最高的节点防丢已提交日志,Raft 用随机超时;两者目标类似但实现机制不同。
  4. 误认为读也走共识 — Zab 只对写做多数派 ack,读可以从任意 follower 直接返回(牺牲一点新鲜度换吞吐),生产里写吞吐受限于 leader 磁盘 fsync。

适用 vs 不适用场景

适用:

  • 协调服务:分布式锁、leader 选举、配置同步、服务发现(典型 ZooKeeper / etcd 类负载)
  • 写远少于读、单条数据小(KB 级)、要求强一致的”元数据”系统

不适用:

  • 大数据量主复制(GB 级 blob):Zab 走 leader 单点磁盘,吞吐撑不住,应选 Kafka / 对象存储
  • 跨地域强一致:Zab 多数派 ack 受限于最慢节点 RTT,跨洲场景延迟过高,应看 Spanner / Raft + Paxos commit
  • 客户端不需要全序、只要”最终一致” → 用 Dynamo / gossip 更便宜

历史小故事(可跳过)

  • 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,自己只管崩溃恢复,比”什么都自己保证”的协议简单很多

延伸阅读

关联

  • 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 — 网络一断电,一致性和可用性只能留一个
  • chubby —— Chubby — 给凡人用的分布式锁服务
  • gfs —— GFS — 编译器决定不做哪些事
  • hdfs-2010 —— HDFS — 把 GFS 用 Java 重写一遍并撑到 25 PB
  • kafka-2011 —— Kafka NetDB 2011 — 把消息中间件砍成”会写文件的水管”
  • lamport-1978 —— Lamport 1978 — 分布式系统里没有”绝对的同时”
  • linearizability-1990 —— Linearizability 1990 — 让并发对象看起来像一次只执行一个操作
  • paxos-1998 —— Paxos 1998 — 古希腊议会寓言里藏的共识协议
  • paxos-simple-2001 —— Paxos Made Simple — Lamport 用平直英语把共识协议推导一遍
  • raft —— Raft — 易理解的共识算法
  • smr-1990 —— SMR 1990 — 把”容错服务”还原成”多副本一起跑同一台状态机”
  • spanner —— Spanner — 全球分布式 SQL 数据库