很多后端同学对 ZooKeeper 的印象停留在”Dubbo 的注册中心”或”Kafka 的依赖”上,被追问原理时往往只能说出一两个名词。本文从数据模型讲起,把 znode 类型、Watcher 语义、会话机制和 ZAB 提供的一致性保证串成一条线,最后落到配置中心、注册中心、分布式锁、选主四类典型用法。

ZooKeeper 解决什么问题

分布式环境里,进程之间总要完成几件”小事”:共享一份配置、感知同伴是否在线、抢一把全局唯一的锁、在多个实例中挑出一个负责干活的主。这些事自己实现,要么靠数据库轮询(延迟高、压力大),要么自研一套共识逻辑(极易写错)。ZooKeeper 的定位就是高可用的分布式协调服务:把”小块元数据的可靠存储 + 变更通知”封装成一组简单原语,业务进程基于这些原语搭建自己的协调逻辑。它的数据全量放内存,为读多写少、低延迟而设计,不是通用数据库。

数据模型:znode 树

ZooKeeper 把数据组织成一棵层次化的多叉树,和 Unix 文件系统路径很像:根节点是 /,每个节点称为 znode,拥有全局唯一路径,既能存一小段数据,也能挂载子节点。znode 上的数据建议控制在 KB 级,单个节点数据上限约 1MB——它是协调状态的载体,不是业务存储。

四种节点类型

znode 有两个正交属性:生命周期(持久/临时)和命名方式(普通/顺序),组合出四种常用类型:

  • 持久节点(PERSISTENT):创建后一直存在,只能显式删除,集群重启也不丢。适合放配置与元数据。
  • 临时节点(EPHEMERAL):生命周期绑定创建它的客户端会话,会话失效节点即被服务端删除,且不能拥有子节点。服务注册、在线感知都靠它。
  • 持久顺序节点(PERSISTENT_SEQUENTIAL):在持久特性之外,创建时 ZK 会在节点名后追加一个 10 位递增序号,如 /tasks/task0000000001,可做全局发号。
  • 临时顺序节点(EPHEMERAL_SEQUENTIAL):同时具备临时与顺序特性,是分布式锁和选主的标准构件。

Stat 与版本

每个 znode 关联一个 Stat 结构,记录创建事务 ID(cZxid)、最近修改事务 ID(mZxid)、数据版本(dataVersion)、子节点版本(cversion)、ACL 版本(aclVersion)、临时节点所属会话(ephemeralOwner)等。dataVersion 可做乐观并发控制:写入时带上读到的版本号,版本不匹配就拒绝本次更新,避免并发覆盖。

Watcher 机制

Watcher 是客户端在读操作时(exists、getData、getChildren)顺带注册在节点上的监听器。节点发生对应变化——创建、删除、数据变更、子节点列表变更——服务端就向注册过 watch 的客户端推送一条事件通知。

一次性触发语义

最关键也最容易被忽视的一点:Watcher 触发一次即失效。收到事件后若还关心后续变化,必须在回调里重新发起一次带 watch 的读。这个设计让服务端无需维护订阅状态与推送水位,实现很轻,但带来两个后果:

  1. 事件只代表”发生过变化”。两次注册窗口之间的多次变更会被压缩成一次通知,客户端应把事件当作”去重新拉取最新状态”的线索,而不是增量日志。
  2. 单客户端视角下事件按序投递,但不能假设自己看到了每一次中间状态。

工程后果

用裸 Watcher 搭配置中心,要自己处理重新注册和断连期间的补拉。生产上通常用 Curator 的 NodeCache、TreeCache,它们在内部完成重新注册与初始同步,把”一次性”语义包装成持续视图,踩坑面小得多。

会话与心跳

客户端与服务器之间通过一条 TCP 长连接维持会话(session),每个会话有全局唯一的 sessionId 和超时时间 sessionTimeout。客户端周期性发心跳;服务端在超时窗口内没收到心跳,就判定会话过期,随即删除该会话创建的所有临时节点并触发相应事件。网络抖动导致的短暂断连,只要在超时前重连上集群任意一台机器,会话仍然有效;真正收到 SESSION_EXPIRED 才意味着临时节点已被清除,客户端必须重建全部状态。把 sessionTimeout 设得比网络抖动 P99 大一些,是减少误判的第一手段。

ZAB 与顺序一致性

集群模式下,ZK 通过 ZAB 协议保证写入可靠:所有写请求由 Leader 以事务形式广播,过半节点确认后提交,每条事务携带全局递增的 zxid。由此换来的一致性口径是顺序一致性:同一客户端的操作按其发送顺序生效,所有客户端看到的变更顺序与全局提交顺序一致。但读请求可以在任意 Follower 本地应答,刚写完立刻读另一台机器可能拿到旧值;需要最新数据时,先调用 sync 让所在节点追平 Leader 再读。

典型应用场景

配置中心

配置放在持久节点上,客户端启动时读取并注册 watch,变更后收到通知再拉取新值。适合数据量小、全集群共享、运行期会变的配置,如功能开关、限流阈值。

注册中心

服务提供者启动时在 /services/{name} 下创建临时节点并写入自身地址;消费者 watch 该目录的子节点列表。实例下线触发临时节点删除,地址列表随之更新。Dubbo 的经典用法正是如此。

分布式锁

竞争者在锁目录下创建临时顺序节点,序号最小者持锁,其余各自监听前一个序号的节点;持有者主动删除或会话结束后,只有紧邻的后继被唤醒,避免惊群。

选主

多个实例争抢创建同一个临时节点,成功者为 Master;其余实例监听该节点,节点消失时重新竞争。HBase 的 Master 高可用即基于这一模式。

工程实践

  • 路径先规划后使用:约定 /{业务}/{服务}/{用途} 层级,防止多团队混用一棵树互相干扰。
  • watch 回调里只做标记和轻量分发,重逻辑丢给业务线程池——回调线程被堵住会拖延后续事件投递。
  • 故障演练要覆盖会话过期:kill 客户端进程、注入网络分区,观察临时节点回收时序与业务重建逻辑。
  • 不要把大对象塞进 znode;数据放别处,ZK 里只存指针与版本。

面试视角

临时节点和持久节点的区别? 生命周期绑定会话 vs 显式删除;临时节点不能有子节点;会话过期后临时节点由服务端删除并触发事件——这是注册中心与分布式锁的基石。

Watcher 为什么是一次性的? 服务端不必为每个客户端维护订阅与水位状态,实现简单、内存可控。代价是客户端要自行重新注册,且可能错过中间变更,因此 Watcher 只保证”变化至少通知一次”,不是可靠事件流。

ZK 是强一致吗? 准确说法是顺序一致性:写由 Leader 排序、过半提交,所有节点按同一顺序应用;默认读本地副本可能短暂偏旧,先 sync 再读可得最新视图。

小结

  • ZK 的核心模型是”层次化 znode 树 + 一次性 Watcher + 绑定会话的临时节点”。
  • 四种节点类型中,临时顺序节点支撑锁与选主,临时节点支撑注册与在线感知。
  • ZAB 提供顺序一致性与过半提交的可靠性,默认读可能略旧,强一致读先 sync。
  • 配置中心、注册中心、分布式锁、选主,本质都是这三类原语的组合。