ZooKeeper 是分布式系统里的「协调员」:配置管理、命名服务、分布式锁、选主、注册中心,都能用它做。虽然现在很多场景被 Nacos/Etcd 替代,但理解它的设计对理解分布式一致性很有帮助。
一、数据模型:一棵树
四种节点类型:
| 类型 | 特点 | 用途 |
|---|---|---|
| 持久(PERSISTENT) | 一直存在,需显式删除 | 配置、元数据 |
| 临时(EPHEMERAL) | 会话断开自动删除 | 服务注册、存活检测 |
| 持久顺序 | 自动追加递增序号 | 分布式队列 |
| 临时顺序 | 临时 + 序号 | 分布式锁、选主 |
每个 ZNode 存什么:数据(最大 1MB,别存大对象)+ 子节点 + 状态(version、ctime、mtime、ephemeralOwner)。
二、集群与 ZAB 协议
ZooKeeper 集群是一主多从(Leader/Follower,还有 Observer):
- 写请求全部转发给 Leader,Leader 用 ZAB 协议(类似两阶段提交)广播给 Follower,超过半数 ack 就提交。
- 读请求任意节点都能处理(可能读到旧值,需要强一致读就先发
sync)。 - 节点数必须是奇数:3 台容忍 1 台故障,5 台容忍 2 台。4 台和 3 台容错能力相同却更贵,所以没有 4 台。
选主:Leader 挂了,剩余节点进入 LOOKING 状态,按 (zxid, myid) 投票 —— 先比 zxid(数据越新越优先),相同则比 myid(配置里配的编号,大的优先),得票过半成新 Leader。整个过程通常几秒内完成,期间集群不可写。
# zoo.cfg(3 节点集群)
tickTime=2000
initLimit=10
syncLimit=5
dataDir=/data/zookeeper
clientPort=2181
server.1=10.0.0.1:2888:3888
server.2=10.0.0.2:2888:3888
server.3=10.0.0.3:2888:3888
# 各节点 dataDir 下要有 myid 文件,内容就是编号 1/2/3
2888 是 Follower 连 Leader 的端口,3888 是选主用的端口,防火墙都要放行。
三、Watch 机制:一次性的坑
Watcher 是一次性的:触发一次就失效,想持续监听必须在回调里重新注册。
// 错误:watch 只生效一次
zk.exists("/config", watchedEvent -> { ... });
// 正确:回调里重新注册
private void watch() {
zk.exists("/config", event -> {
if (event.getType() == Event.EventType.NodeDataChanged) {
reloadConfig();
watch(); // ★ 重新注册
}
});
}
Curator(Netflix 开源的 ZK 客户端)已经封装好了持续监听(NodeCache / PathChildrenCache),生产上不要直接用原生 API,一律用 Curator,它把重连、重试、Watcher 重复注册都处理了。
CuratorFramework client = CuratorFrameworkFactory.builder()
.connectString("10.0.0.1:2181,10.0.0.2:2181")
.retryPolicy(new ExponentialBackoffRetry(1000, 3))
.sessionTimeoutMs(30000)
.connectionTimeoutMs(10000)
.build();
client.start();
四、典型应用
1. 分布式锁(临时顺序节点)
InterProcessMutex lock = new InterProcessMutex(client, "/locks/order");
if (lock.acquire(10, TimeUnit.SECONDS)) {
try { doBiz(); } finally { lock.release(); }
}
原理:每个客户端在 /locks/order 下创建临时顺序节点,序号最小的获得锁;其它节点监听自己的前一个节点,前一个删除后自己变成最小(避免惊群)。客户端崩溃时临时节点自动删除,锁自动释放 —— 这解决了 SETNX 死锁的问题。
2. 服务注册(临时节点)
Provider 启动时在 /services/order 下创建临时节点(内容是 IP:端口),Consumer 监听子节点变化。Provider 宕机 → 会话超时 → 临时节点消失 → Consumer 收到通知更新列表。
这也是为什么注册中心要用临时节点:它天然提供了存活检测。
3. 配置中心
配置存在持久节点,客户端加 Watcher。配置变更时 ZK 通知所有客户端(这也是 ZK 做配置中心的局限:Watcher 数量大时通知风暴,所以大规模配置下发更适合 Nacos/Apollo)。
4. 选主
LeaderSelector selector = new LeaderSelector(client, "/master", new LeaderSelectorListenerAdapter() {
public void takeLeadership(CuratorFramework c) {
// 成为 Leader,执行任务(方法返回即放弃领导权)
}
});
selector.autoRequeue(); // 放弃后重新参与选举
selector.start();
五、四字命令(运维必备)
# 需要先在 zoo.cfg 加:4lw.commands.whitelist=*
echo stat | nc 127.0.0.1 2181 # 角色(leader/follower)、连接数、延迟
echo srvr | nc 127.0.0.1 2181 # 服务器信息
echo ruok | nc 127.0.0.1 2181 # 健康探测,返回 imok
echo conf | nc 127.0.0.1 2181 # 运行中的配置
echo cons | nc 127.0.0.1 2181 # 客户端连接详情
echo wchs | nc 127.0.0.1 2181 # watch 数量(★ 排查 watch 泄漏)
echo mntr | nc 127.0.0.1 2181 # 监控指标全集(znode 数、延迟、包量)
echo dump | nc 127.0.0.1 2181 # 会话与临时节点
六、生产实践
案例:会话超时导致服务反复上下线
背景:某 B2B 电商公司,早期用 ZooKeeper 做 Dubbo 注册中心。
现象:服务频繁掉线又上线,消费者报 No provider available 抖动,日志里有大量 Session expired。
排查:
echo mntr | nc ... 2181看到zk_max_latency很高(超过 2000ms)。- 看 ZK 节点的磁盘:
iostat显示%util接近 100。 - 查数据目录:事务日志(version-2 下的 log 文件)和快照堆积了几十 GB 没清理。
- 会话超时是 5s,而 ZK 的 GC 停顿 + 磁盘慢导致心跳处理不过来,会话被判超时。
处理:
- 定期清理事务日志和快照(
autopurge.purgeInterval=1,保留 3 份)。 - 事务日志目录(dataLogDir)单独挂 SSD,和快照目录分开 —— 这是 ZK 官方明确建议的,因为日志是顺序写,快照是随机写,混在一起会互相拖累。
- 会话超时从 5s 调到 15s(给 GC 和网络留余量)。
- JVM 调大堆 + 用 G1,降低停顿。
案例:Watcher 泄漏
现象:ZK 内存缓慢上涨,连接数增加后变慢。
排查:echo wchs | nc ... 2181 显示 watch 数量几十万。查代码,有个监听器在回调里每次都新建 Watcher 却没有释放,且父节点的递归监听覆盖了整棵子树。
处理:改用 Curator 的 TreeCache 统一管理,并且监听粒度下沉到具体节点,不要监听根节点递归整棵树。
七、注意事项
- ZNode 数据不能超过 1MB,ZK 不是存储系统。
- Watcher 是一次性的,持续监听要用 Curator。
- 集群节点数奇数,且要允许 2 台同时故障就上 5 台。
- 事务日志和快照分盘,并开启自动清理。
- 不要用 ZK 做大规模配置下发(Watcher 风暴),配置中心用 Nacos/Apollo。
- 会话超时别设太短(建议 10–30s),否则 GC 或网络抖动就会误判掉线。
- ZK 的写性能有限(几千 TPS),不要高频写。
一起聊聊