Apache ZooKeeper — 给一群机器装一个共同的小脑
是什么
Apache ZooKeeper 是一套让一群机器在关键事情上保持口径一致的协调服务。
日常类比:像一个剧组场记——50 个演员同时拍戏,谁演主角、当前场号是几、哪个镜头已开拍,全部记在场记板上;任何人想知道当前状态,看场记一眼即可;任何人想改状态,必须先举手让场记同意。ZooKeeper 就是分布式系统里那块『所有人都信』的场记板。
它不是数据库,不是消息队列。它存的是『谁是主』『谁还活着』『当前配置是什么』这类少量但关键、所有节点必须共识的元数据。
一个反向定义可能更直观:如果你在分布式系统里发现自己想写『好像需要一个全局变量但又不能放在某一台机器上,因为那台挂了大家都瞎了』——这就是 ZooKeeper 的用武之地。
为什么重要
不理解 ZooKeeper,下面这些事都说不清:
- 为什么 Kafka 在 3.x 之前必须配一套 ZK 才能跑——broker 谁是 controller、topic 元数据存在哪,全靠它
- 为什么 HBase / Hadoop YARN / Solr / Dubbo 这些『大数据时代基建』全都把 ZK 当底座
- 为什么 etcd / Consul / Kafka KRaft 这些后继者出现时大家会立刻关注——它们是奔着替代 ZK 来的
- 为什么聊『分布式锁』『leader 选举』时,工业界第一反应是 ZK 而不是 Paxos 论文
- 为什么读分布式系统论文时『Wait-free coordination』『顺序一致性』这些词反复出现——ZK 把它们写进了一代基建工程师的肌肉记忆
核心要点
ZooKeeper 的世界由四件事拼起来:
- 数据模型 znode 树:像一个微型文件系统,路径长这样
/services/kafka/broker-1,每个节点叫 znode,可存少量数据(建议 < 1MB)。它不是文件系统,是树形 KV——既能像目录一样组织层级,又像 KV 一样按路径直接读写。 - 三种 znode:
- 持久节点:你不删它就一直在,重启集群也还在
- 临时节点(ephemeral):客户端 session 断了自动消失——这是『感知谁还活着』的关键。临时节点不能有子节点,因为它命运随 session
- 顺序节点(sequential):创建时 ZK 自动加 10 位单调递增编号,比如
lock-0000000017。两种属性可以叠加,最常用的是『临时 + 顺序』
- Watch 机制:客户端在某个 znode 上注册 watch,节点变化(数据改 / 子节点增删 / 节点本身被删)时触发一次回调。注意是一次性——触发后必须重新注册,否则漏事件。这是 ZK 区别于 etcd 长连接 watch 的关键差异点。
- ZAB 协议:ZooKeeper Atomic Broadcast,是 ZK 内部用来『让多台机器写入顺序完全一致』的共识协议。类似 Paxos 但专为『主备复制 + 顺序广播』场景做了简化,比通用 Paxos 容易实现和理解。所有写入由 leader 串行编号广播给 follower,过半 ack 后 commit。
部署形态固定:奇数节点集群(3 / 5 / 7),多数派存活即可服务。2N+1 节点容忍 N 台故障。读吞吐随节点数线性扩展(任意 follower 都可读),写需要过半确认(瓶颈在 leader)。一致性保证有五条:顺序一致、原子性、单一系统镜像、可靠性、有界实时性——理解这五条边界比死记 API 重要。
实践案例
案例 1:leader 选举(ZK 最经典的用法)
3 台 broker 都想当 controller,谁来决定?
所有候选者各自创建顺序临时 znode: /controller/candidate-0000000001 (broker A) /controller/candidate-0000000002 (broker B) /controller/candidate-0000000003 (broker C)编号最小者为 leader,即 broker A。A 挂了 → session 断 → 它的 znode 自动消失 → B 编号最小 → B 自动接任。整个过程没有任何中心仲裁者,全靠 ZK 的『顺序 + 临时』两个属性自动出结果。Kafka 0.x-2.x、HBase Master、YARN ResourceManager HA 全用这一套。
案例 2:分布式锁
10 个进程同时想改一份文件,怎么排队?
每个进程在 /lock 下创建顺序临时节点。查看 /lock 下所有节点,自己编号是不是最小? 是 → 拿到锁,干活,干完删自己的节点 否 → watch 紧邻自己前一个节点,等它消失再轮到自己为什么不让所有人都 watch 最小节点?那叫羊群效应——前一个释放锁,1000 个客户端同时被唤醒抢,浪费。watch 紧邻前序节点是公认的正确姿势。
案例 3:配置中心 + 服务发现
把数据库连接串、feature flag 写到 /config/db-url,所有应用启动时读一次 + 注册 watch。运维改一次 znode 内容,所有应用毫秒级收到通知刷配置。这是 Dubbo 注册中心、Solr 集群配置的核心机制。
服务发现复用同一套:服务实例启动时在 /services/order-svc/ 下建临时节点 instance-xxx,写入自己的 IP:port;客户端 list 这个目录就拿到当前所有活实例,注册 watch 实时感知上下线。临时节点 + 子节点 watch 这一对原语,构成了 2010-2020 年代国内微服务注册中心的事实模板。
踩过的坑
-
watch 是一次性的:触发后不会自动续订,回调里必须重新调用
getData(..., watch=true)重新注册。新人写出『以为订阅了所有变化』的代码很常见。 -
session 过期 ≠ 连接断开:网络抖一下 ZK 客户端会自动重连,session 还在,临时节点不删;但如果断开超过 session timeout(默认 30 秒),session 过期,所有该 client 的临时节点立即被删——下游可能误以为服务下线。两者要分别处理。
-
不要把 znode 当 KV 存数据:ZK 单 znode 默认上限 1MB,整个集群内存型存储,全量 snapshot 到磁盘。塞 GB 级数据会让 follower 同步超时、leader 选举失败。规则:只放元数据和指针,业务数据放 S3 / DB / Kafka。
-
写吞吐是单 leader 瓶颈:所有写都串行走 leader → 过半 follower ack → commit。QPS 通常在几千上限,不要把它当高吞吐数据库——它是为『慢但一致』优化的。
-
JVM GC 停顿会拖死集群:ZK 是 Java 写的,长时间 Full GC 会让 leader 心跳超时被踢,触发选举抖动。生产部署必须独立物理机或独立 JVM、关闭 swap、tune 好堆大小,绝对不要和应用进程混部。
-
跨机房延迟放大写延迟:ZAB 写需要过半 ack,如果 5 节点跨 3 机房部署,每次写都要等最远机房的 ack 回来。同城多机房 OK,跨城(>10ms RTT)写延迟会肉眼可见地变慢——这也是 Consul 主打『多数据中心』时把 ZK 当反例的原因。
适用 vs 不适用场景
适用:
- leader 选举(HBase / Kafka 老版本 / YARN HA)
- 服务注册与发现(Dubbo 早期、Solr 集群、ByteDance / Meituan 等大厂早期微服务底座)
- 分布式锁、屏障、队列等协调原语
- 配置中心(少量、变化不频繁、所有节点必须一致)
- 集群成员管理(谁在线、谁掉线)
- 任务分配 / 分片管理(典型如 Kafka topic partition 到 broker 的映射)
不适用:
- 高吞吐写(>10k QPS)→ 用 Kafka / Redis
- 大数据存储 → 用对象存储 / 数据库
- 需要丰富查询语义 → 用 etcd(支持 range、事务)或 KV 数据库
- Kafka 3.x+ 新部署 → 用 KRaft 替代,不再需要外置 ZK
- 跨地域强一致 → ZAB 的过半 ack 在跨城延迟下表现差,考虑 Spanner 类 TrueTime 方案
- 单机或两节点部署 → 没有多数派概念,等于裸奔,至少 3 节点起步
历史小故事(可跳过)
- 2006 年:Yahoo! 内部 Hadoop 集群规模膨胀,多个组件各写各的协调逻辑,bug 频出。Mahadev Konar 提议抽出一个通用协调服务给所有组件共用。
- 2007 年:Yahoo! Research 的 Mahadev Konar / Benjamin Reed / Flavio Junqueira 开发,论文 ZooKeeper: Wait-free coordination for Internet-scale systems 发表于 USENIX ATC 2010。名字 ZooKeeper 来自 Hadoop 生态的『动物园』隐喻——Pig / Hive / HBase 各种动物,需要一个看管员。
- 2008 年:开源给 Apache,最初是 Hadoop 子项目。
- 2010 年:升级为 Apache 顶级项目。同期 Yahoo 内部已用它管几千节点的 Hadoop 集群。
- 2013 年起:Kafka / HBase / Solr / Dubbo 等先后把 ZK 选为元数据底座,奠定『大数据基建标配』地位。
- 2018 年:CoreOS 推出 etcd,主打 Raft + gRPC,社区开始迁移。
- 2022 年(Kafka 3.3):KRaft 模式 GA,Kafka 自己用 Raft 管元数据,ZK 进入逐步退场阶段。
- 2024 年(Kafka 3.5+):ZK 模式正式标记为 deprecated,新部署默认走 KRaft。
- 至今:HBase / 老 Hadoop / Solr 仍大量在生产用 ZK,运维老系统绕不开;新项目几乎一律选 etcd 或 KRaft。
学到什么
- 协调服务是分布式系统的『内核态』——它不解决业务问题,只解决『大家对关键事实达成一致』这一件事;解决之后,上层各种业务才能并行。把这一层独立出来是 ZK 最重要的工程贡献。
- ZAB 是工业级共识协议的活样本——理解 ZAB 才能理解后来的 Raft 把哪些点简化、etcd 又改进了什么;从 ZAB → Raft → KRaft 是一条清晰的演化线,是学共识协议的最佳教学路径。
- 临时节点 + 顺序节点 + watch 三个原语组合,能拼出 leader 选举、分布式锁、屏障、队列、组成员——这是设计协调原语的范式课。学习 ZK 不是为了用 ZK,而是为了看懂『少数原语如何组合出复杂语义』。
- 痛点驱动后继者——ZK 的写瓶颈、watch 一次性、运维复杂直接催生了 etcd / KRaft;学一个老系统的『不好用之处』比学它的『好用之处』更长见识。
- CAP 里 ZK 选 CP——网络分区时少数派 follower 拒绝服务而不是返回旧数据,这是它『协调服务』身份决定的硬选择;如果选了 AP,下游的 leader 选举就会脑裂。
延伸阅读
- 官方文档(首选):ZooKeeper Documentation
- 原始论文:Hunt et al. 2010 — ZooKeeper: Wait-free coordination for Internet-scale systems
- ZAB 协议论文:Junqueira et al. 2011 — Zab: High-performance broadcast for primary-backup systems
- 配方手册(最实用):ZooKeeper Recipes —— 官方给出的 leader 选举、分布式锁、屏障、双屏障、队列、共享锁、可重入锁的标准实现
- Curator 客户端:Apache Curator —— Netflix 开源的 ZK 高级封装,把 recipes 打包成开箱即用的 API,生产环境几乎都用它而不是裸 ZK 客户端
- 对照阅读:etcd —— Raft + gRPC 的现代替代者;kafka —— 从依赖 ZK 到 KRaft 自管的迁移代表
关联
- etcd —— 后继者,用 Raft 替代 ZAB,社区逐步迁移的目的地
- kafka —— 老版本依赖 ZK 管 broker / topic 元数据,3.x 起转 KRaft
- hbase —— Master 选举与 RegionServer 注册全靠 ZK
- hadoop —— YARN ResourceManager HA failover 用 ZK 协调
- dubbo —— 早期默认注册中心是 ZK,后来支持 Nacos
- consul —— HashiCorp 的协调 + 服务发现替代品,主打多数据中心
- nacos —— 阿里开源注册中心,AP 模式 + 配置管理一体化,常被对照
- paxos —— 共识协议鼻祖,ZAB 是它在主备复制场景下的简化变体
- raft —— ZAB 的精神继任者,把可理解性放在第一位