服务治理 列表全部文章
服务治理
技术笔记›服务治理第 1 / 2 篇

ZooKeeper 笔记:数据模型、Watch 机制、选主原理与典型应用

ZooKeeper 的原理与实战:ZNode 四种节点类型、ZAB 协议与选主过程、Watcher 一次性特性带来的坑、分布式锁与注册中心的正确实现方式,附四字命令与运维要点。

更多

ZooKeeper 是分布式系统里的「协调员」:配置管理、命名服务、分布式锁、选主、注册中心,都能用它做。虽然现在很多场景被 Nacos/Etcd 替代,但理解它的设计对理解分布式一致性很有帮助。

一、数据模型:一棵树

/(根节点) /services /config /locks /services/order/prov1(临时) /services/order/prov2(临时) /locks/order_0001(顺序) 临时节点(EPHEMERAL)随会话消失 → 做服务注册;顺序节点(SEQUENTIAL)自动加序号 → 做分布式锁与选主。

四种节点类型:

类型 特点 用途
持久(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。

排查:

  1. echo mntr | nc ... 2181 看到 zk_max_latency 很高(超过 2000ms)。
  2. 看 ZK 节点的磁盘:iostat 显示 %util 接近 100。
  3. 查数据目录:事务日志(version-2 下的 log 文件)和快照堆积了几十 GB 没清理。
  4. 会话超时是 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 统一管理,并且监听粒度下沉到具体节点,不要监听根节点递归整棵树。

七、注意事项

  1. ZNode 数据不能超过 1MB,ZK 不是存储系统。
  2. Watcher 是一次性的,持续监听要用 Curator。
  3. 集群节点数奇数,且要允许 2 台同时故障就上 5 台。
  4. 事务日志和快照分盘,并开启自动清理。
  5. 不要用 ZK 做大规模配置下发(Watcher 风暴),配置中心用 Nacos/Apollo。
  6. 会话超时别设太短(建议 10–30s),否则 GC 或网络抖动就会误判掉线。
  7. ZK 的写性能有限(几千 TPS),不要高频写。
— 感谢读到这里 —发现错误,或想补充?告诉我 →
TURN KNOWLEDGE INTO SOMETHING REAL

有想法,也想把它做出来?

从技术方案到上线交付,聊聊你的下一个项目。

聊聊需求

一起聊聊

搜索文章

输入关键词,按标题和摘要即时筛选。

浏览分类文章归档

阅读设置

字号
行宽
专注模式

专注模式会隐藏页头与侧栏,只留正文。设置保存在当前浏览器。

稍后阅读

保存在当前浏览器,留给下一次安静阅读。

总浏览 -- 次 · 总访客 -- 人 · 今日浏览 -- 次 · 今日访客 -- 人