适用读者:要用一个"外部仲裁者"解决锁、选主、注册的人;搞不清 ZK 与 etcd/Redis 该选谁的人。
前置:S17-B02(服务注册与发现)、J04(分布式事务)、S12(缓存一致性)、S15-J06(集群前置)。
覆盖旧文:4584(ZooKeeper 入门:核心概念、核心特性、架构原理与实战用法)→ 1 篇 / 6,960 字符。
扩展点:E-S17-021(znode 四种类型与会话语义:为什么"临时节点"是分布式协调的地基)、E-S17-022(watch 的一次性语义与"惊群"处置)、E-S17-023(ZAB、选主与脑裂:ZK 保证什么、不保证什么)。
下一篇:S17-X02 · 缓存与工具簇登记。
1. 一句话结论
ZK 的全部能力来自两个原语:znode 的"顺序 + 临时性",和 watch 的"变更通知"。 锁 = 顺序临时节点排队;选主 = 最小序号;注册 = 临时节点 + 会话存活。业务代码里不该出现"手写锁",而是用现成 recipe(Curator)。
出处:
A-4584(核心概念/特性/架构/用法)。
2. 数据模型与会话语义(E-S17-021)
树形命名空间:/app/config/db.url = "…"
每个 znode:数据(小,≤1 MB 建议)+ 版本号 + ACL + 子节点
| 类型 | 生命周期 | 典型用途 |
|---|---|---|
| 持久(PERSISTENT) | 显式删除才消失 | 配置、元数据 |
| 临时(EPHEMERAL) | 会话结束即删 | 存活注册、锁持有标记 |
| 持久顺序(PERSISTENT_SEQUENTIAL) | 持久 + 单调递增序号 | 队列、分发序号 |
| 临时顺序(EPHEMERAL_SEQUENTIAL) | 临时 + 序号 | 锁排队、选主 |
会话(session)机制:
客户端连上 → 建立 session(sessionTimeout,如 30s)→ 定期心跳
心跳丢失超过 timeout → 会话过期 → 该会话创建的所有临时节点被删除
判据:"临时节点 = 用会话存活表达的租约"——进程崩溃、网络断开都会让会话过期,临时节点自动消失。这就是 ZK 能表达"谁活着"的根本原因,也是它比"Redis 存个 key 带 TTL"更强的地方(ZK 的过期判定在服务端仲裁,Redis 的 TTL 只是键过期)。
3. watch 的一次性语义与惊群(E-S17-022)
watch 规则:
① 一次性:触发后即失效,要持续关注必须重新注册
② 只通知"变了",不告诉"变成什么"(需重新读取)
③ 顺序性:客户端先收到通知,再看到新数据
惊群(herd effect)问题:
朴素锁实现:所有等待者 watch 同一个 /lock 节点
→ 锁释放时,全部客户端同时被唤醒,一起去抢 → ZK 瞬时压力放大
正确做法(顺序节点):
每个客户端创建 /lock/lock-<seq>,只 watch「比自己小一位的那个节点」
→ 释放时只有一个人被唤醒,O(1) 通知
判据:"watch 前一个"是 ZK 锁的标准配方——它把"广播唤醒"改成"接力唤醒"。手写这段逻辑极易出错(断连重注册、序号回绕、父节点不存在),生产一律用 Curator 的 InterProcessMutex。
4. 选主与 ZAB:ZK 保证什么(E-S17-023)
选主:所有候选创建 /election/n_<seq>(临时顺序),序号最小者为 leader
leader 崩溃 → 其临时节点消失 → 次小者 watch 触发 → 成为新 leader
ZAB 协议:类似 Paxos 的原子广播
① 选主(leader election)
② 广播(所有写请求经 leader,过半 follower ack 才提交)
| ZK 保证 | ZK 不保证 |
|---|---|
| 同一客户端看到的是有序的更新(FIFO per session) | 跨客户端的实时一致性(异步复制,可能读到旧值) |
| 写请求过半成功才提交 | 强一致读(除非显式 sync()) |
| leader 崩溃后能选出新 leader | 网络分区下的可用性(少数派直接不可写) |
脑裂(split-brain):ZK 靠"过半"防脑裂——少数派分区无法选出 leader,也就无法写。判据:三节点容忍 1 台故障,五节点容忍 2 台;偶数节点不增加容错,只增加延迟。
版本注意:ZK 3.5+ 移除了 leader 单点依赖(引入 leader-only 与动态配置),但部署仍建议奇数节点;3.8/3.9 的变更按官方文档核对(待核具体小版本行为)。
5. 典型用途与替代选型
| 用途 | ZK 实现 | 更轻的替代 | 判据 |
|---|---|---|---|
| 配置管理 | 持久节点 + watch | etcd / Nacos / Apollo | 只需 KV + watch → etcd/Nacos 更简单 |
| 服务注册 | 临时节点 + watch | Nacos / Consul | 已有 ZK(Kafka 旧版、HBase)→ 复用 |
| 分布式锁 | 临时顺序节点 | Redis(Redlock 有争议)/ etcd lease | 强一致要求高 → ZK/etcd |
| 选主 | 临时顺序 + 最小序号 | etcd election / K8s Lease |
K8s 内优先用 Lease 对象 |
| 队列 | 顺序节点 | Kafka / RabbitMQ | ZK 不适合高吞吐队列(数据小、写放大) |
判据:"ZK 是协调者,不是存储、不是队列、不是数据库"——把业务数据放进 znode(几 MB)会把整个集群拖垮(所有写经 leader,过半 ack)。
运维红线:
□ 奇数节点(3/5),跨机架/可用区分散
□ 独立磁盘存事务日志(dataLogDir 与 dataDir 分开)
□ JVM 堆不宜过大(≤4 GB,ZK 对 GC 停顿敏感)
□ 监控:会话数、znode 数、请求延迟、leader 变更次数
□ 快照与事务日志定期备份(恢复靠这两者)
6. 反例:七种"用错 ZK"
| 做法 | 后果 |
|---|---|
| 把业务配置大对象写进 znode | 每次写都过半 ack,集群吞吐骤降 |
| 所有等待者 watch 同一节点 | 惊群,释放瞬间打满 |
| 手写锁逻辑(断连重注册、序号判断) | 并发下偶发双持有 |
| 部署 4 或 6 节点 | 容错不增,延迟增加 |
| 单节点 ZK 用于生产 | 无仲裁,脑裂与丢数据 |
| 用 ZK 当消息队列 | 吞吐低、堆积能力差 |
| 忽略会话超时设置 | 网络抖动 → 频繁误判掉线 → 锁与注册抖动 |
7. 自测
- 四种 znode 类型与各自用途?
- 为什么"临时节点"能表达存活?与 Redis TTL 的本质差异?
- watch 的三条语义?惊群如何避免?
- ZK 保证什么、不保证什么?为什么必须奇数节点?
- ZK 不适合做队列/存储的原因?
答案
- 持久/临时/持久顺序/临时顺序;配置、存活、队列序号、锁与选主。
- 会话过期由服务端仲裁并删除临时节点;Redis TTL 只是键到期,无会话仲裁。
- 一次性、只通知变更、顺序性;只 watch 比自己小一位的节点。
- 保证单会话 FIFO、过半写提交、可重新选主;不保证跨客户端实时一致与分区可用性。过半机制要求奇数。
- 所有写经 leader 且过半 ack,数据小、写放大,不适合高吞吐与堆积。
8. 下一篇
S17-X02 · 缓存与工具簇登记 —— Caffeine/多级缓存/雪崩穿透击穿/延迟双删、分布式 ID、CompletableFuture 并行化、MQ 选型、Lucene 的逐条处置与主讲篇指向。
本篇属于系列《S17 · 分布式与消息队列》。旧文来源:A-4584;扩展点 E-S17-021/022/023。ZK 行为按 3.5+ 口径描述,3.8/3.9 具体变更待核。