1.1 ZooKeeper 概述:分布式系统的书记员 本节摘要:ZooKeeper 是一个开源的分布式协调服务,它不处理业务逻辑,只负责维护一份集群公认的共享状态,并保证这份状态的一致与有序。本节讲清它的定位边界、四大职责,以及它为什么不是数据库。 先旁听一桩事故 某团队的定时任务服务跑了半年一直太平,某天运维为容灾把服务扩到两台。两台机器各自的定时器都到了触发时刻,同时拉起了同一个补偿任务——用户收到两条重复的退款短信。事后复盘,根因一句话就够:没有任何一方负责裁定"此刻谁有权执行"。单机时代靠操作系统互斥锁解决的问题,到了多机环境突然失去了裁判。 这类问题在分布式系统里有一长串名字:选主、互斥、配置一致、服务发现。
本节摘要:ZooKeeper 是一个开源的分布式协调服务,它不处理业务逻辑,只负责维护一份集群公认的共享状态,并保证这份状态的一致与有序。本节讲清它的定位边界、四大职责,以及它为什么不是数据库。
某团队的定时任务服务跑了半年一直太平,某天运维为容灾把服务扩到两台。两台机器各自的定时器都到了触发时刻,同时拉起了同一个补偿任务——用户收到两条重复的退款短信。事后复盘,根因一句话就够:没有任何一方负责裁定"此刻谁有权执行"。单机时代靠操作系统互斥锁解决的问题,到了多机环境突然失去了裁判。
这类问题在分布式系统里有一长串名字:选主、互斥、配置一致、服务发现。它们各自细节不同,内核却相同——多台机器需要就某件事达成一致,而且这个"一致"必须扛得住机器宕机、网络抖动。自己从零实现一遍,等于把分布式一致性理论里的坑重新踩完。ZooKeeper 的价值就在这里:它把这些协调需求抽象成一套小而稳的原语,你基于它搭锁、搭选主、搭配置中心,而不是亲自去证明一致性协议的正确性。
回到本章的法庭比喻:ZooKeeper 是集群的书记员。所有参与方(客户端)都可以向它递交记录(写入 ZNode),也可以查阅记录(读取 ZNode);书记员对外的承诺是——无论你问哪一台(集群里每个节点都能响应读请求),你查到的案卷顺序都一样,而且一旦记录在案就不会凭空消失。至于业务怎么用这些记录,书记员一概不问。
这个定位带来两个常被忽视的推论。第一,它存的数据必须小。案卷柜的空间有限(默认单节点 1 MB),你往里面塞大文件、塞报表,既拖慢共识速度,也违背它的设计初衷;它只该存"谁是我的主""当前配置是哪一版""这个锁现在归谁"这类元信息。第二,它读快写慢。读请求由各节点本地直接响应,可以水平扩;写请求必须经过 Leader 走多数派表决,吞吐上限由单机决定。所以它的典型场景是读多写少——配置读一万次,改一次,正合适。

按使用频率排,ZooKeeper 在生产系统里承担四类协调职责。配置管理:把配置放在 ZNode 里,各节点注册监听,配置一改全场生效。选主:若干候选节点在同一个目录下登记,谁排到最前谁当主,主挂了队列自动补位。分布式锁:想独占资源的进程来书记员这里排号,号最靠前的持有锁,别人只能等。服务注册与发现:服务提供者上线时登记一个临时节点,会话断开节点自动消失,消费者永远看到的是活着的实例列表。
这四类事表面上千差万别,往下拆其实全靠同一组原语:ZNode 的增删改查、四种节点类型、Watcher 通知、版本号条件更新。学完 1.2 节的数据模型,再回头看这张清单,你会发现所谓"协调"不过是"把状态放在一个大家都信的地方,并在它变化时及时知道"。
装好单机版(第二章手把手带装)后,用自带的命令行工具连上去,就能体验这套原语。下面是一段真实会话,注释为说明而加:
# 连接本机 ZooKeeper,默认端口 2181 $ zkCli.sh -server 127.0.0.1:2181 [zk: 127.0.0.1:2181(CONNECTED) 0] ls / # 输出:ZooKeeper 内部保留节点 [zookeeper] # 创建一个持久节点,写入一句配置 [zk: 127.0.0.1:2181(CONNECTED) 1] create /app-config "pay-timeout=3000" Created /app-config # 读取验证 [zk: 127.0.0.1:2181(CONNECTED) 2] get /app-config pay-timeout=3000 # 修改数据,注意输出的 dataVersion 从 0 变成 1 [zk: 127.0.0.1:2181(CONNECTED) 3] set /app-config "pay-timeout=5000" [zk: 127.0.0.1:2181(CONNECTED) 4] stat /app-config cZxid = 0x5 mtime = Mon Aug 29 10:15:02 CST 2026 dataVersion = 1 # 每改一次加一,可用于条件更新
短短六行命令,已经覆盖了"增改查 + 版本号"三个原语。换成 Java 客户端,同样的动作长这样(Curator 写法,第三章细讲,这里只看形状):
// 构建客户端:连接地址 + 重试策略(首次 1 秒,最多 3 次) CuratorFramework client = CuratorFrameworkFactory.newClient( "127.0.0.1:2181", new ExponentialBackoffRetry(1000, 3)); client.start(); // 建立会话,相当于登录法庭 // 创建节点并写入配置 client.create().forPath("/app-config", "pay-timeout=3000".getBytes()); // 读取:拿到的字节数组转字符串 byte[] data = client.getData().forPath("/app-config"); System.out.println(new String(data)); // 输出 pay-timeout=3000 client.close(); // 结束会话,临时节点将随之消失
两段代码对应同一件事,这不是巧合——任何语言、任何框架,最终都落在同一套协议原语上。先在脑子里装下"原语"这个概念,后面学哪个客户端都不会迷路。
问:有了数据库,为什么还要 ZooKeeper 做协调? 数据库能存配置,却无法在"配置变了"的瞬间主动通知每个客户端,也无法在持有锁的进程崩溃时自动收回锁。ZooKeeper 的临时节点与 Watcher 把这两件事变成了系统行为,不需要应用方自己写心跳与超时清理逻辑。
下一节走进书记员的档案室,看 ZNode 到底长什么样。