ZooKeeper: 不发钥匙, 改立公告板的协调服务
- handle: Chubby 里 client
open一个 path 后拿到的, 绑定 session 的远程对象引用; 不是 session 本身, 是 session 内部的一个对象级句柄; - znode: ZooKeeper 状态树上的一个节点, 既能当目录又能存一小坨 data, 带 version;
- ephemeral / sequential: 临时节点 (session 一断自动删) / 顺序节点 (创建时由 ZK 追加一个全局单调的编号), 选主 recipe 的两块积木;
- Zab: ZooKeeper Atomic Broadcast, ZK 内部的 leader-based 原子广播协议, 跟 Paxos / Raft 同级但不是同一个;
- A-linearizability: ZooKeeper 自己定义的一致性, 写线性 + 同一 client 的请求按发送序进入全局顺序 (per-client FIFO);
- wait-free: 一个 client 可以同时挂多个 in-flight 请求, 不用等上一个回包就发下一个 (能 pipeline);
- control plane / data plane: 控制面 (谁是 master, 配置是啥, 谁还活着) / 数据面 (真流量怎么转发);
定位: 三家放一起, 真正的分水岭在哪
这篇接着 11.chubby 和 12.etcd 往下说; 把 Chubby, ZooKeeper, etcd 摆到一张桌子上, 第一直觉会想"它们内部共识协议不一样啊, Paxos / Zab / Raft", 但这恰恰不是最该盯的地方; 三家内部都是同一套路子: leader-based replicated state machine, 都要全局顺序 + quorum commit 来护住自己那点 metadata;
真正把它们区分开的, 是对外暴露的协调抽象:
| 系统 | 内部共识 | 对外抽象 | 心智 |
|---|---|---|---|
| Chubby | Paxos / Multi-Paxos | file + handle + lock | coordination as ownership |
| ZooKeeper | Zab | znode + version + watch + ephemeral/sequential | coordination as ordered state |
| etcd | Raft | MVCC KV + revision + watch + lease + txn/CAS | coordination as revisioned KV state |
一句话先甩在这: Chubby 给你一把钥匙, 谁攥着钥匙谁有权开门; ZooKeeper 给你一块强一致的公告板, 板上的状态按顺序变, 大家看公告行事; 这篇就是把"为什么是钥匙 vs 公告板"这件事拆开讲清楚;
Chubby 的 handle 到底是什么
ZooKeeper 论文专门说自己不用 Chubby 那套 handle-based method, 所以得先搞懂 handle 是个啥; 一个常见误解是把 handle 当成 session, 其实不是, handle 是 session 内部的一个对象级引用:
1 | session |
分工是这样的:
- session 管"你还活着吗": lease 有没有过期, failure detection, client 整体是否在线;
- handle 管"你正在操作哪个对象, 你对它有啥状态": 打开的是哪个 path, open mode, 是否持锁, 缓存状态, stale-handle 检测的 metadata;
所以 handle 最准的一句描述是: 一个 client 打开某个 Chubby path 后拿到的, 绑定 session 的远程对象引用;
handle 跟 Unix file descriptor 像不像
很像, 但 handle 是 fd 的分布式增强版; 调用形态几乎一模一样:
1 | open(path) → fd / handle |
区别全在"分布式"那几个字上:
| 维度 | Unix fd | Chubby handle |
|---|---|---|
| 所在环境 | 单机 OS kernel | 分布式服务 |
| 绑定什么 | 进程 | client session / lease |
| distributed lease | 无 | 有 |
| stale client 保护 | 无 | 有 |
| master epoch / generation | 无 | 可能有 |
| 直接表达分布式锁状态 | fd 本身不表达 (flock/fcntl 是额外机制) |
handle 可绑定 lock state |
所以更准的类比是: Chubby handle ≈ distributed file descriptor + lease/session 绑定 + 一致性 metadata;
ZooKeeper 为什么不要 handle
Chubby 的 API 心智是 object/handle 为中心: 先 open 一个对象拿 handle, 之后所有操作都通过 handle 走;
1 | Chubby: open(path) → handle → lock(handle) → read/write(handle) |
ZooKeeper 偏不让你先 open 再操作, 而是每次请求都直接把要操作的 path 报出来:
1 | ZooKeeper: create(path) / delete(path) / setData(path) |
这就导致状态挂的地方完全不同:
| 状态挂在哪 | 应用关心什么 | |
|---|---|---|
| Chubby | handle / session / object | 谁持有某对象的 ownership? 谁拿到了 lock? 这个 handle 还有效吗? |
| ZooKeeper | znode tree / version / watch / 全局顺序 | 这个 znode 在不在? version 多少? watch 触发没? 这些写的全局顺序是啥? |
Chubby 是 object-centric, ZooKeeper 是 path / order-centric;
连 path 的含义都不一样
两边表面都长得像文件系统路径, 但语义重心是反的:
- Chubby path 指向 cell 里一个 file / directory / lock object, 比如
/bigtable/master; 应用把它解读成"某个外部资源或角色的 ownership 位置", 拿到手第一件事是open完lock; 所以它表示我要打开/加锁/读写的远程对象; - ZooKeeper path 指向 znode tree 上的一个状态节点, 比如
/config/serviceA/ready,/election/candidate-00000001; 应用直接对它create/setData/watch; 所以它表示控制面状态树上的一个状态事实;
收口: Chubby path = object to open/lock, ZooKeeper path = state node to create/watch/version;
两种用法 pattern: 抢钥匙 vs 看公告
两家都能干 leader election / service discovery / config 管理, 但 pattern 是两条路:
- Chubby:
acquire lock → become owner → operate resource; 它回答的是"谁有权做事? 谁是唯一 owner?"; - ZooKeeper:
write state → watch/read state → infer what to do; 它回答的是"系统状态现在是啥? 推进到哪个 phase 了? 某配置 ready 没? 某成员还在不在?";
拿 leader election 当标尺最清楚:
1 | Chubby 风格: |
Chubby 靠 lock ownership, ZooKeeper 靠 znode state + sequence order; 这跟 11.chubby 里讲的"Chubby 大方直接发锁, ZK 抠门只给积木"是同一件事的两个侧面;
先别急着比, 它们到底是不是一类东西
是, 三家都是 control-plane metadata / coordination system, 干的活就那几样: leader election, distributed lock, membership, service discovery metadata, configuration state, 小块 metadata 存储;
但有个常见误会要先掐掉: Envoy 不是同类; Envoy 是 data-plane proxy, 干的是 HTTP/gRPC routing, load balancing, retry, circuit breaking, mTLS 这些真流量上的活; 两者的关系是上下游, 不是替代:
1 | ZooKeeper / etcd / Consul → 存控制面状态 (谁是 leader, 谁还活着, 配置版本几号) |
内部其实都长一个样: leader-based RSM
把对外抽象的壳剥掉, 三家内部都是 leader-based 的复制状态机; 以 ZooKeeper 的 Zab 为例走一遍:
1 | client write |
所以 ZooKeeper 内部照样有 leader, replicated log (proposal sequence), quorum commit, state machine apply, 只是对外暴露的不是 raw log, 而是这条 log 顺序投影出来的 znode tree;
这里要顺手拆一个误会: 别以为"ZooKeeper 有 log, Chubby 没 log, master 直接 apply"; Chubby 内部必须也有 replicated Paxos log, 否则 master 本地写完就挂, 新 master 不知道这笔写发生过, 一致性当场崩盘; Chubby 内部状态 (file contents, directory metadata, lock ownership, session state, handle metadata, ACL) 全靠 Paxos log 护着; 真正的区别不是有没有 log, 而是:
- ZooKeeper: log / order 是对外编程模型的地基, 直接摆给你看;
- Chubby: log / order 更像内部实现细节, 对外只露 handle / lock / ownership;
比 Chubby 和 ZooKeeper, 别盯着内部共识, 盯对外抽象;
wait-free + per-client FIFO: A-linearizability 的命根
ZooKeeper 标榜 wait-free, 意思是一个 client 可以同时挂多个 in-flight 请求, 不必等上一个回包再发下一个, 这样能 pipeline, 把吞吐拉上去; 比如要刷一组配置:
1 | delete /ready |
但你一旦允许 pipeline, 就得保证同一个 client 的请求不乱序, 否则 create /ready 可能比 set /cfg/5000 先生效, 灾难; 所以 ZK 给了一条硬保证:
per-client FIFO ordering: 同一个 session 发出的请求, 虽然异步 pipeline, 进入全局顺序时仍按发送序排列;
写本身线性一致 + 这条 per-client FIFO, 合起来就是 ZooKeeper 的 A-linearizability; (读那一侧能走任意 follower, 只保 sequential consistency 的事, 在 11.chubby 的"动刀点 3"里讲过, 这里不重复)
ready pattern: 它解决什么, 又埋了什么坑
配置更新往往不是一个 key, 而是一坨 metadata 要原子发布:
1 | /config/a /config/b /config/c /config/ready |
直接挨个 set /config/a|b|c, reader 可能读到半新半旧的配置; ZooKeeper 的经典解法是立一个 commit marker:
1 | delete /config/ready # 开工: 先把 ready 撤了 |
应用约定: ready 不在 = 配置不可用/正在更新; ready 在 = 配置完整可用; 靠 per-client FIFO + 全局写顺序, reader 一旦看到 ready, 就能推断它前面那串 config 写都已经按序落地了;
但 ready 不是锁, 多 writer 一上就漏
这是个真实漏洞, 得讲清楚: Zab 只保证单个写进全局顺序, 不会把你这几步自动打包成一个不可插入的大事务; 两个 writer 同时跑这套 recipe, 操作会交错:
1 | A delete ready |
A create ready 之后, reader 看到 ready 就以为配置齐了, 可 B 还在写; ready 只是个 commit marker, 既不是锁也不是事务; 要堵这个坑, 得在它之上再叠一层:
- single writer / leader election / writer lock (谁能写);
- epoch / fencing token / version check (CAS) (写出来的东西怎么互斥);
更稳的做法是 versioned config, 写新版本 + 最后 CAS 切指针:
1 | /config/versions/41/... |
reader 要么读到旧版要么读到新版, 永远读不到混合版本;
ZooKeeper 只管 substrate 对不对, 不管你业务语义对不对
顺着上面那个坑往上抽象一层: ZooKeeper 保证的是 coordination substrate correctness, 它不懂你的 application semantic correctness;
它会替你保证的:
- path 操作是否合法, version check 过没, ACL 准不准;
- write 是否进了全局 zxid 顺序;
- ephemeral node 是否随 session 失效而删;
- watch event 是否按规则触发;
它压根不知道的:
/config/ready表示"配置完整"这件事;- 谁是业务上合法的 leader;
- reader 该不该等
ready; - 当前状态在业务上算不算可用;
ZooKeeper guarantees ordering, not meaning; 业务语义得由你的应用协议, 搭在它的 ordering / version / watch / session 之上自己建;
两种原子性: log ordering vs mutex critical section
退一步看个更普适的取舍: 用 log 顺序做原子, 和用传统 mutex 临界区做原子, 不是一回事:
| mutex / lock 原子性 | log ordering / ready marker 原子性 | |
|---|---|---|
| 关注 | 谁能进 critical section? 谁是唯一 updater? | 状态变化以啥顺序可见? reader 啥时候能认状态已 commit? |
| 适合 | leader election, 单 owner job, 外部资源/shard ownership | config publish, membership change, barrier, workflow phase 推进 |
| 强项 | 心智简单, 直接表达 ownership, 适合护外部资源 | 适合表达状态机推进, watch/notification, 让 reader 认 committed state |
| 软肋 | 只保证"别人进不来", 不保证进去那位中途 crash 后状态完整, 不自动解决 partial update | 唯一 writer 不自动保证, ready/epoch/version/CAS 得自己设计, pattern 写错就出语义漏洞 |
所以最实用的不是二选一, 而是两层叠着用: lock / election 负责"谁能写", log ordering / ready / version 负责"写出来的状态啥时候可见"; 这也正好解释了上一节那个多-writer 坑的修法 —— ready 这层 ordering 原子性, 底下得垫一层 lock/election 的 ownership 原子性;
顺带把另一个直觉修一下: 不能简单说"Chubby 的锁只护外部资源, ZooKeeper 的 log 只护内部 metadata"; 两家内部 metadata 都各自靠自己的共识护着 (Chubby 用 Paxos 护 file/lock/session, ZK 用 Zab 护 znode tree); Chubby 那把 lock 不是给 Chubby master 写内部 metadata 用的, 而是给外部 client/application 当协调原语用的; 更准的说法只是: Chubby 更自然地表达 ownership, ZooKeeper 更自然地表达 shared state transition;
两层 leader 别看串了
既然 Chubby 内部已经有 master, 为啥还要 lock? 因为这是两层不同的 leader:
- Chubby internal master: 5 个 replica 用 Paxos 选出来的, 负责处理 client 请求, 给内部操作排序, 护 Chubby metadata;
- application leader: 比如 Bigtable 那几台 server, 通过抢 Chubby 的
/bigtable/master这把 lock 来选自己的 master;
Chubby lock 是给外部应用用的, 不是 Chubby 内部为了写自己 metadata 而用的; 外部系统的 data plane 还能自带任意架构 (single master / per-shard leader / primary-backup / leaderless quorum), Chubby 只帮它定"谁是 master, 谁拥有 shard-17", 真正的数据更新协议是外部系统自己的事;
把这条线浓缩成一张图:
- 内部 —— 三家都是 leader-based replicated state machine: Chubby = Paxos / Multi-Paxos, ZooKeeper = Zab, etcd = Raft; 都要全局顺序 + quorum commit 护 metadata, 这层不是它们的分水岭;
- 对外 —— Chubby = file + handle + lock, 心智是 coordination as ownership; ZooKeeper = znode + version + watch + ephemeral/sequential, 心智是 coordination as ordered state transition; etcd = MVCC KV + revision + watch + lease + txn/CAS, 心智是 coordination as revisioned KV state transition;
- ZooKeeper 不直接暴露 raw transition log, 而是暴露 log 顺序投影出来的 znode 状态树;
- wait-free 允许 pipeline, 靠 per-client FIFO 兜住乱序, 合成 A-linearizability;
- ready pattern 是 commit marker 不是锁/事务, 多 writer 必须再叠 lock/election + epoch/version/CAS, 或干脆走 versioned config + CAS 切指针;
- ZooKeeper 只保 substrate correctness (ordering), 业务 meaning 得应用自己建; 实战里 lock 管"谁能写", ordering 管"何时可见", 两层叠着用;
- 最短一句: Chubby 是 ownership-first, ZooKeeper 是 ordered-state-first, etcd 是 revisioned-KV-state-first; 内部同源, 对外分流;
