这是 2020 年 2 月整理的 ZooKeeper 笔记。彼时 ZooKeeper 还是分布式协调的事实标准(Dubbo、Kafka、HBase 都依赖它做注册中心/元数据/选主),
ZooKeeper 3.5+刚引入容器节点等新特性。文中命令基于 3.5.x,与 3.4.x 基本兼容。
ZooKeeper 是什么
ZooKeeper 是一个分布式协调服务,为分布式应用提供:
- 命名服务(注册中心)
- 配置管理
- 分布式锁
- 集群选主
一句话:让一堆机器像一台机器一样协同工作。
数据模型
ZooKeeper 的数据模型可以理解为 Linux/Unix 的文件目录,是一棵树:
/
├── dubbo (Dubbo 注册中心根节点)
│ └── com.example.UserService
│ ├── providers
│ └── consumers
├── config
│ └── redis (统一配置)
└── locks
└── order_1001_0000000001 (分布式锁临时顺序节点)
树的每个节点称之为 znode,节点上可以保存数据,也可以有子节点。
节点类型
创建节点时可指定类型,常用组合是持久 + 顺序、临时 + 顺序:
| 类型 | 参数 | 生命周期 | 说明 |
|---|---|---|---|
| 持久节点 | 默认 | 一直存在,直到显式删除 | 适合保存配置、元数据 |
| 临时节点 | -e | 客户端会话断开后自动消失 | 适合注册中心、选主 |
| 顺序节点 | -s | 同持久/临时 | 名字自动追加 10 位递增序号 |
| 容器节点 | -c | 最后一个子节点被删除后自动删除 | 3.5+ 新增,适合容器语义 |
版本号(乐观锁)
每个节点都有各自的版本号 version,每当节点数据变化,版本号都会累加。
删除、修改一个节点时,如果传入的版本号不匹配会报错:
# 读取当前版本号
stat /config/redis
# 假设版本号为 3
# 带版本号更新(乐观锁),版本不匹配则更新失败
set -v 3 /config/redis "host=10.0.0.1"
这是 ZooKeeper 实现分布式锁的基础——通过 CAS(比较并交换)保证并发安全。
其他特性
- 数据大小:每个 znode 存储数据不宜过大,几 K 即可(ZooKeeper 是协调服务,不是数据库)。
- ACL 权限:节点可以设置权限,通过权限控制用户的访问(
world/auth/digest/ip四种权限模式)。
典型用途
| 用途 | 原理 | 场景 |
|---|---|---|
| 主节点选举 | 各节点竞争创建临时节点,创建成功的成为 Leader | 主从集群高可用,主挂了从节点通过选举选出新主 |
| 统一配置管理 | 配置存 znode,各节点监听变化 | 只需在一台机器上改配置,即可同步到所有服务器(如修改 Redis 统一配置) |
| 发布与订阅 | 发布者写数据到 znode,订阅者监听 | 类似 MQ 机制;如 Dubbo 发布者把服务信息存于 znode,订阅者读取到数据 |
| 分布式锁 | 临时顺序节点 + Watcher | 多个进程互斥访问共享资源 |
| 强一致性 | ZAB 协议保证 | 集群环境中保证数据的强一致性(写) |
分布式锁的一种经典实现
基于临时顺序节点:
- 所有客户端在
/locks/order_1001下创建临时顺序节点。 - 谁创建的节点序号最小,谁拿到锁。
- 没拿到的客户端监听比自己序号小一号的节点,该节点删除时(持锁者释放)再去检查自己是否最小。
- 持锁者会话断开,临时节点自动删除,锁自动释放(不会死锁)。
# 三个客户端竞争,创建出的节点序号递增
create -e -s /locks/order_1001/lock ""
# 返回 /locks/order_1001/lock0000000001(拿到锁,序号最小)
# 返回 /locks/order_1001/lock0000000002(等待 0000000001 删除)
# 返回 /locks/order_1001/lock0000000003(等待 0000000002 删除)
Session 会话
客户端与 ZooKeeper 服务端建立连接后即创建 Session:
- 每个会话都能设置一个超时时间。
- 通过心跳(客户端向服务端 ping 包)维持连接。
- 心跳结束 → Session 过期 → 该会话创建的临时节点被删除。
Session 是临时节点生命周期的判据,也是客户端状态(如 Watcher)的载体。
基础命令
| 命令 | 作用 |
|---|---|
help | 查看帮助 |
ls path | 查看节点下的子节点列表 |
stat path | 查看节点状态信息 |
ls2 path | ls + stat 的组合 |
get path | 获取节点的值 |
create [-s] [-e] [-c] [-t ttl] path [data] | 创建节点 |
set [-s] [-v version] path data | 设置节点的值 |
delete [-v version] path | 删除节点 |
create:临时与顺序
# 创建持久节点
create /config "hello"
# 创建临时节点(stat 时 ephemeralOwner 有值 = 持有该节点的 session id)
create -e /config/tmp "tmp"
# 创建顺序节点:节点名字 = 当前输入 path + 10 位数字,从 0000000001 开始
create -s /config/seq "seq"
# 返回 /config/seq0000000001
create -s /config/seq "seq2"
# 返回 /config/seq0000000002
注意:不管 path 是否相同,累加的数字都是唯一的、顺序增长的(全局序号由父节点维护)。
set:带版本号更新(乐观锁)
set /config/redis "host=10.0.0.1"
# 修改节点数据,版本号 +1
set -v 3 /config/redis "host=10.0.0.2"
# 带上版本号更新:版本号匹配则成功,不匹配(已被别人改过)则报错
Watcher 监听机制
当监控的某个节点发生变化,会触发 Watcher 事件,类似于 SQL 中的触发器。
Watcher 是一次性的:对一个节点设置的 Watcher,触发后立即销毁,需要再次注册才能继续监听。
客户端 服务端
│ ① 注册 Watcher │
├────────────────────────▶ │
│ │ 节点发生变化
│ ② 触发事件并销毁 Watcher │
│◀─────────────────────────┤
│ ③ 重新注册 Watcher │
├────────────────────────▶ │
针对不同的操作,触发的 Watcher 事件也不同:
| 事件 | 触发条件 |
|---|---|
NodeCreated | 节点被创建 |
NodeDeleted | 节点被删除 |
NodeDataChanged | 节点数据被修改(set) |
NodeChildrenChanged | 子节点列表变化(创建/删除子节点) |
ls 与 stat 监控范围的差异:
- 使用
ls创建的 Watcher 能为父节点设置,子节点变化时可以接收到。 - 使用
stat设置的 Watcher 不能监控子节点(只能监控节点本身的数据变化)。
ZAB 协议:保证一致性
ZAB(ZooKeeper Atomic Broadcast)协议的消息广播过程使用的是原子广播协议,类似二阶段提交(2PC)过程。
读写分离
- 读请求:Leader 和 Follower 都可以处理(Follower 直接返回本地数据)。
- 写请求:全部转发到 Leader 接收。
写请求的完整流程
客户端 ──写请求──▶ Leader
│ ① 封装成事务 Proposal,分配全局唯一递增的 ZXID
│ ② 放入与每个 Follower 之间的 FIFO 队列
▼
┌─────┴─────┐
▼ ▼
Follower1 Follower2 Follower3 ...
│ Ack │ Ack │ Ack
└─────┬─────┘
│ ③ 超过半数成功响应,执行 commit
│ (先提交自己,再发送 commit 给所有 Follower)
▼
事务生效
关键点:
- ZXID:全局递增的唯一事务 ID,ZAB 必须将每一个事务按 ZXID 先后排序后处理。
- FIFO 队列:Leader 与每个 Follower 之间维护独立的 FIFO 消息队列,异步解耦——同步方式会引起阻塞,性能下降很多。
- 过半机制:超过半数 Follower 成功响应即 commit(先提交自己,再广播 commit 给所有 Follower)。
CAP 取舍
由于”过半 Follower 确认提交”的设计:
- ✅ A(可用性):满足。
- ✅ P(分区容错性):满足。
- ✅ C 中的写入强一致性:满足(写必须过半确认)。
- ❌ C 中的读取一致性:丧失(Follower 可能读到稍旧的数据)。
所以 ZooKeeper 属于 CP 系统(强一致优先,牺牲部分可用性与读一致性)。
崩溃恢复与消息广播
ZAB 协议有两种模式:
- 崩溃恢复模式:启动过程中,或 Leader 出现网络中断、崩溃退出与重启等异常时,进入恢复模式并选举产生新的 Leader。当选举出新 Leader 且集群过半机器与其完成状态同步后,退出恢复模式。
- 消息广播模式:集群中过半 Follower 完成和 Leader 的状态同步后,进入消息广播模式。新加入的服务器会自觉进入数据恢复模式:找到 Leader,与其数据同步,然后一起参与广播。
崩溃情况 1:Leader 提交后、commit 广播前挂掉
一个事务在 Leader 上提交,得到半数 Follower 的 Ack 反馈,但在把 commit 消息发给所有 Follower 之前 Leader 挂掉了:
- ZAB 保证选举出来的新 Leader 拥有集群中最高编号(ZXID)的事务 Proposal(即所有已提交的提案)。
- 新 Leader 为每个 Follower 准备一个队列,把未同步的事务以 Proposal 形式逐个发给 Follower,并紧接着发送 commit。
- Follower 将尚未同步的事务都从 Leader 同步过来并成功应用后,才加入真正可用 Follower 列表(所以 Leader 得到半数 Follower 反馈即可成功,不成功的 Follower 暂时不可用)。
崩溃情况 2:Leader 提出事务后、尚未广播就挂掉
Leader 提出一个事务后就崩溃,导致其他服务器没收到这个事务:
- 该服务器恢复并再次加入集群时,需要丢弃这个事务。
- 关键在于 ZXID 的设计:
ZXID(64 位)
┌───────────────────────────────┬──────────────────────────────┐
│ 高 32 位:epoch(Leader 周期) │ 低 32 位:计数器(单调递增) │
└───────────────────────────────┴──────────────────────────────┘
- 低 32 位:简单的单调递增计数器。针对客户端的每个事务请求,Leader 在产生新 Proposal 时对该计数器 +1。
- 高 32 位:代表 Leader 周期 epoch 编号。每当选举产生新 Leader,会从它的本地日志中取出最大事务 Proposal 的 ZXID,解析出对应 epoch 值后 +1 作为新 epoch,并把低 32 位置 0 开始生成新的 ZXID。
当一个包含上一个 Leader 周期中未被提交的事务 Proposal 的服务器加入集群:
- 自动以 Follower 角色加入。
- Leader 发现它有上一个 Leader 周期的事务 Proposal 时,会要求它执行回退操作。
- 回退到一个确实已经被集群中过半机器提交的最新事务 Proposal(丢弃未提交的孤儿事务)。
安装与配置
- 下载安装包(如
apache-zookeeper-3.5.10-bin.tar.gz),解压。 - 复制
conf/zoo_sample.cfg为conf/zoo.cfg。 - 按需修改
zoo.cfg。
zoo.cfg 参数说明
| 参数 | 作用 |
|---|---|
tickTime | 基本时间单元(ms),其他时间配置 = 数值 × tickTime |
initLimit | 集群:允许从节点连接并同步到主节点的初始连接时间(倍数 × tickTime) |
syncLimit | 集群:主节点与从节点之间发送消息、请求和应答的时间长度(心跳机制),超时从节点被抛弃 |
dataDir | ZooKeeper 的数据存放位置 |
dataLogDir | ZooKeeper 的日志存放位置(建议与 dataDir 分开磁盘) |
clientPort | 客户端连接端口(默认 2181) |
# conf/zoo.cfg 单机示例
tickTime=2000
initLimit=10
syncLimit=5
dataDir=/usr/local/zookeeper/data
dataLogDir=/usr/local/zookeeper/logs
clientPort=2181
集群搭建
使用 conf/zoo.cfg 配置文件:
- 修改
dataDir,为每个节点设置好目录。 - 设置
dataLogDir,配置日志。 - 在配置的
dataDir里创建文件myid,里面的数值要与下面配置中server.后面的序号一致。 - 添加集群节点配置(
2888为集群通信端口,3888为 Leader 宕机后的选举端口)。
三节点集群示例
节点 1(192.168.1.10):
# conf/zoo.cfg
tickTime=2000
initLimit=10
syncLimit=5
dataDir=/usr/local/zookeeper/data
dataLogDir=/usr/local/zookeeper/logs
clientPort=2181
server.0=192.168.1.10:2888:3888
server.1=192.168.1.11:2888:3888
server.2=192.168.1.12:2888:3888
# dataDir 下创建 myid,内容与 server. 后的序号一致
echo "0" > /usr/local/zookeeper/data/myid
节点 2(myid 为 1)、节点 3(myid 为 2)同理,zoo.cfg 内容相同。
验证集群
# 三台机器都启动后
bin/zkServer.sh start
bin/zkServer.sh status
# 输出 Mode: leader / follower,集群中恰好一个 leader,其余为 follower
要点:myid 必须与 server.N 的 N 一致;集群最少 3 台(过半机制要求 2n+1),2 台无法形成过半共识。
小结
- 数据模型:树形 znode,分持久/临时/顺序/容器节点;版本号做乐观锁;数据保持几 K;支持 ACL。
- 典型用途:选主、统一配置、发布订阅、分布式锁、强一致性协调。
- Session:心跳维持,过期则删除其临时节点。
- 命令:
ls/stat/get/create(-e 临时、-s 顺序)/set(-v 版本)/delete。 - Watcher:一次性,触发即销毁;事件分创建/删除/数据变更/子节点变更;
ls可监控子节点变化,stat不行。 - ZAB:写请求全走 Leader,ZXID 排序 + FIFO 队列 + 过半提交;崩溃恢复靠 epoch 高 32 位区分 Leader 周期,未提交事务回退。
- 集群:
myid+server.N=ip:2888:3888,最少 3 台保证过半共识。