Zookeeper
开篇:分布式系统的"协调员"
假设你们公司有一个重要会议室,所有团队都想用。如果没有预约系统,大家就会抢着占,谁先到谁用,迟到的只能干等。更糟的是,有人占着不用,其他人也没办法。
这个预约系统需要做几件事:保证同一时间只有一个团队使用(分布式锁),记录会议室的状态让所有人都能看到(配置中心),当会议结束时通知排队的人(事件通知)。
ZooKeeper 就是分布式系统中的这个"预约系统"——或者更准确地说,它是一个分布式协调服务。它不存储业务数据,而是帮助多个服务节点之间达成一致、互相协调。
一、ZooKeeper 数据模型
1.1 ZNode 树结构
ZooKeeper 的数据以目录树的形式组织,和文件系统很像。每个节点叫做 ZNode,有一个唯一的路径标识(如 /app/config/db)。
/
├── app
│ ├── config
│ │ ├── db
│ │ └── redis
│ └── services
│ ├── order-service
│ └── user-service
└── locks
└── order-lock每个 ZNode 可以存储少量数据(通常是 KB 级别的配置信息或状态信息),也可以有子节点。ZNode 不支持部分读写,每次都是完整读取或完整写入。
重要提示:ZooKeeper 的数据全部存储在内存中,这保证了极高的读写速度,但也意味着不能存储大量数据。它是协调服务,不是数据库。
1.2 四种节点类型
ZNode 有四种类型,创建时确定,之后不可修改:
| 类型 | 特点 | 典型用途 |
|---|---|---|
| 持久节点(PERSISTENT) | 创建后一直存在,除非主动删除 | 存储配置信息 |
| 持久顺序节点(PERSISTENT_SEQUENTIAL) | 持久节点 + 自动追加递增序号 | 全局唯一 ID 生成 |
| 临时节点(EPHEMERAL) | 客户端会话断开后自动删除 | 服务注册、分布式锁 |
| 临时顺序节点(EPHEMERAL_SEQUENTIAL) | 临时节点 + 自动追加递增序号 | 公平分布式锁 |
临时节点是 ZooKeeper 最有价值的特性之一:当创建这个节点的客户端断开连接(或崩溃),节点会自动删除。这天然解决了"持有锁的进程挂了导致死锁"的问题。
注意:临时节点不能有子节点。
1.3 节点的版本控制
每个 ZNode 都维护了一个版本号(version)。每次修改数据,版本号自动递增。更新时可以带上版本号做 CAS 操作,避免并发写冲突:
// 只有当前版本是 5 时才能更新成功
zk.setData("/config/db", newData, 5);二、ZAB 协议
ZAB(ZooKeeper Atomic Broadcast)是 ZooKeeper 自研的一致性协议,类似于 Paxos 和 Raft,但针对"读多写少"的场景做了优化。
2.1 核心思想
ZAB 的核心规则只有三条:
- 所有写操作都由 Leader 处理,Follower 收到写请求会转发给 Leader。
- 所有节点按相同顺序接收和应用写操作。
- 一次写操作必须获得过半节点确认才算提交成功。
2.2 写入流程
每个事务都有一个全局唯一的事务 ID(zxid),由两部分组成:高 32 位是 epoch(领导周期),低 32 位是计数器。epoch 在每次 Leader 选举后递增,保证不同 Leader 产生的 zxid 不会冲突。
2.3 ZooKeeper 的一致性级别
ZooKeeper 是一个 CP 系统(一致性 + 分区容错),会牺牲可用性。具体表现:
- 如果集群中存活节点少于半数,整个集群无法接受写请求。
- Leader 选举期间,集群无法接受写请求。
但要注意,ZooKeeper 保证的是顺序一致性,而非线性一致性。区别在于:
- 线性一致性:任何时刻从任何节点读取的都是最新数据。
- 顺序一致性:每个节点最终会看到相同的数据,且操作顺序一致,但某个时刻可能读到旧数据。
原因:当 Leader 完成写入后,Follower 的数据同步可能有延迟。如果此时客户端从尚未同步的 Follower 读取,就会读到旧值。
如何保证读到最新数据? 调用 sync() 命令,强制当前 Follower 与 Leader 同步完成后再读取。
三、Watcher 机制
Watcher 是 ZooKeeper 最核心的功能之一,它允许客户端监听 ZNode 的变化,实现事件驱动的编程模型。
3.1 工作原理
ZooKeeper 的 Watch 机制涉及两端的组件:
客户端:ZkWatcherManager 管理客户端注册的所有 Watcher,处理事件回调。
服务端:WatchManager 管理所有注册到 ZNode 上的 Watcher 信息。
完整流程:
- 客户端调用
getData("/config/db", true)注册一个 Watcher。 - 客户端的 ZkWatcherManager 记录这个 Watcher,并将注册信息发送到服务端。
- 服务端的 WatchManager 将 Watcher 关联到
/config/db节点。 - 当
/config/db的数据被修改时,WatchManager 通知 ZooKeeper Server。 - Server 将变更事件推送给注册了 Watcher 的客户端。
- 客户端的 ZkWatcherManager 触发对应的回调方法。
3.2 重要特性
一次性触发:Watcher 触发一次后就失效了。如果想持续监听,需要在回调中重新注册。这是 ZooKeeper 的设计决策,避免大量 Watcher 导致的性能问题。
可以设置 Watch 的操作:exists、getData、getChildren。
可以触发 Watch 的操作:create、delete、setData。
四、Leader 选举
4.1 选举触发条件
两种情况会触发选举:
- 集群初始启动:还没有 Leader,所有节点都是 LOOKING 状态。
- Leader 崩溃:Follower 检测到与 Leader 的心跳超时,进入 LOOKING 状态。
4.2 选举规则
选举遵循一个核心原则:遇强投强。
"强"的标准是 zxid 越大越强(数据越新)。如果 zxid 相同,则 sid(服务器 ID)越大越强。
选举流程:
- 每个节点先投自己一票,广播自己的选票
<zxid, sid>。 - 收到别人的选票后,比较 zxid。如果别人的 zxid 更大,就改投别人。
- 如果某个候选人获得过半节点的投票,当选 Leader。
- 其他节点变为 Follower,与 Leader 同步数据后开始对外服务。
4.3 启动阶段选举示例
假设有 5 台服务器(sid 分别为 1~5),依次启动:
- Server 1 启动:投自己
<1,1>。总票数 1,未过半,继续等待。 - Server 2 启动:投自己
<2,2>。Server 1 比较后发现 2 的 zxid 更大,改投 Server 2。Server 2 得 2 票,未过半。 - Server 3 启动:投自己
<3,3>。Server 1 和 2 都改投 Server 3。Server 3 得 3 票,过半,当选 Leader。 - Server 4、5 启动:发现已有 Leader,直接变为 Follower。
注意:这个例子中 zxid 都是初始值所以比较的是 sid。实际运行中 zxid 通常不同。
4.4 Leader 失效重新选举
假设 Server 3(Leader)宕机,各节点的 zxid 分别为:Server 1 = 88, Server 2 = 102, Server 4 = 100, Server 5 = 101。
所有 Follower 进入 LOOKING 状态,各自先投自己:
<88, 1>、<102, 2>、<100, 4>、<101, 5>
经过比较和改投,zxid 最大的 Server 2 (zxid=102) 获得过半投票,当选新 Leader。
4.5 节点角色
| 角色 | 职责 |
|---|---|
| Leader | 处理读写请求,发起投票,维护心跳 |
| Follower | 处理读请求,将写请求转发给 Leader,参与投票 |
| Observer | 处理读请求,将写请求转发给 Leader,不参与投票 |
Observer 的作用是扩展读能力而不影响选举性能。因为参与投票的节点越多,达成共识越慢。
五、典型应用场景
5.1 分布式锁
基于临时顺序节点实现:
- 所有客户端在同一个父节点下创建临时顺序节点。
- 序号最小的节点获得锁。
- 没有获得锁的节点 Watch 前一个节点,等待通知。
- 释放锁时删除自己的节点,下一个节点收到通知后获得锁。
- 如果持有锁的客户端崩溃,临时节点自动删除,锁自动释放。
可以直接使用 Apache Curator 客户端的 InterProcessMutex:
InterProcessMutex lock = new InterProcessMutex(client, "/locks/order");
if (lock.acquire(10, TimeUnit.SECONDS)) {
try {
// 执行临界区代码
} finally {
lock.release();
}
}ZK 分布式锁的优缺点:
优点:天然避免死锁(临时节点自动删除)、支持阻塞等待、支持可重入。
缺点:性能不如 Redis 分布式锁(每次创建/删除节点需要 Leader 同步);在极端情况下,网络抖动导致会话断开,临时节点被删除,其他客户端可能获得锁,产生并发问题(可通过合理的重试策略和超时设置缓解)。
5.2 服务注册与发现
- 服务提供者在 ZooKeeper 上创建临时节点注册自己(节点数据包含 IP、端口等)。
- 服务消费者通过 Watch 监听服务节点变化。
- 服务提供者下线(会话断开)时,临时节点自动删除,消费者收到通知更新列表。
5.3 配置中心
将配置信息存储在 ZNode 中,所有应用通过 Watch 监听配置节点。配置变更时,所有应用实时收到通知。
5.4 Leader 选举
多个应用实例通过在同一路径下创建临时顺序节点来竞选 Leader。序号最小的成为 Leader。Leader 崩溃后临时节点删除,序号次小的自动成为新 Leader。
六、脑裂问题
6.1 什么是脑裂?
脑裂是指由于网络分区,一个集群被分成两个(或多个)独立的子集群,每个子集群独立运行并各自认为自己是合法的。如果两个子集群各自选出一个 Leader,就会导致数据不一致。
6.2 ZooKeeper 如何避免脑裂?
ZooKeeper 通过 过半机制(Quorum) 来避免脑裂:一个 Leader 必须获得超过半数节点的支持才能当选。
假设 5 节点集群因网络分区分成了 2+3 两组:
- 3 节点的那组可以选出 Leader(3 > 5/2),继续对外服务。
- 2 节点的那组无法选出 Leader(2 <= 5/2),停止写入服务。
这确保了任何时刻最多只有一个合法的 Leader。代价是少数派分区变得不可用——这是 CP 系统的典型特征。
6.3 脑裂恢复
网络分区恢复后,ZooKeeper 会自动发现并恢复:少数派分区的节点重新连接到 Leader,同步数据后回到正常状态。
七、ZooKeeper vs Nacos vs Eureka
| 对比项 | ZooKeeper | Nacos | Eureka |
|---|---|---|---|
| CAP | CP | CP + AP(可切换) | AP |
| 一致性协议 | ZAB | Raft(CP 模式)/ Distro(AP 模式) | 无(最终一致) |
| 数据存储 | 内存 | 内存 + 数据库 | 内存 |
| 健康检查 | 会话心跳 | 主动探测 + 客户端心跳 | 客户端心跳 |
| 配置管理 | 支持(简单) | 支持(完善) | 不支持 |
| 易用性 | 较复杂 | 简单,自带控制台 | 简单 |
| 维护状态 | Apache 维护 | 阿里持续维护 | Netflix 已停更 |
ZooKeeper 最初不是为服务发现设计的,它是一个通用的分布式协调组件。Nacos 和 Eureka 是专门的服务注册中心,在易用性和功能丰富度上更好。
如果你的系统已经用了 ZooKeeper(比如 Dubbo 的注册中心),可以继续用。新项目建议优先考虑 Nacos。
八、ZooKeeper 的缺点
- 写性能瓶颈:所有写操作必须经过 Leader,Leader 是整个集群的性能天花板。
- 内存限制:数据全在内存中,不适合存储大量数据。
- 选举期间不可用:Leader 崩溃到新 Leader 选出之间,集群无法处理写请求(通常 200ms 内完成)。
- 运维复杂:相比 Nacos 等现代组件,ZooKeeper 的运维和配置更复杂。
- 网络敏感:频繁的网络波动会触发 Leader 选举,影响可用性。
九、常见面试题精选
Q1:ZooKeeper 是 CP 还是 AP?
ZooKeeper 是 CP 系统。它通过 ZAB 协议保证数据一致性,当集群出现网络分区或 Leader 选举时,会牺牲可用性(无法处理写请求)。但注意 ZK 保证的是顺序一致性而非线性一致性——通过 Follower 读取可能读到旧数据。
Q2:ZAB 协议的核心思想是什么?
所有写操作由 Leader 处理,Leader 将写操作广播给所有 Follower,过半 Follower 确认后才算提交成功。通过全局唯一的 zxid 保证操作顺序。
Q3:ZooKeeper 的 Watch 机制有什么特点?
Watch 是一次性的——触发一次后失效,需要重新注册。Watch 事件从服务端推送到客户端,延迟很低。可以监听数据变化(data watch)和子节点变化(child watch)。
Q4:如何用 ZooKeeper 实现分布式锁?
基于临时顺序节点:所有客户端在同一父节点下创建临时顺序节点,序号最小的获得锁。释放锁时删除节点,下一个节点收到 Watch 通知后获得锁。客户端崩溃时临时节点自动删除,避免死锁。
Q5:什么是脑裂?ZooKeeper 如何避免?
脑裂是集群因网络分区被分成多个独立运行的子集群。ZooKeeper 通过过半机制避免:Leader 必须获得超过半数节点支持,网络分区后少数派无法选出 Leader,从而保证任何时刻最多一个 Leader。
十、节点创建的唯一性保证
ZooKeeper 如何保证在并发情况下不会创建出重复的节点?它使用了两层保障:
第一层:Leader 单点写入。所有写请求都由 Leader 处理,即使客户端连接的是 Follower,写请求也会被转发到 Leader。这从根本上减少了并发冲突。
第二层:锁 + CAS。在 Leader 内部,节点创建操作使用 synchronized 锁住父节点,确保同一时刻只有一个线程在父节点下操作。在锁内部,先检查子节点是否已存在,存在则抛出 NodeExistsException,不存在才创建。节点数据存储在 NodeHashMap(底层是 ConcurrentHashMap)中。
简单来说就是:一锁、二判、三创建——和解决接口幂等问题的思路如出一辙。
十一、ACL 权限控制
每个 ZNode 创建时都可以设置 ACL(Access Control List),控制谁可以对它执行什么操作。
ZooKeeper 支持以下权限:
| 权限 | 缩写 | 说明 |
|---|---|---|
| CREATE | c | 创建子节点 |
| READ | r | 读取节点数据和子节点列表 |
| WRITE | w | 修改节点数据 |
| DELETE | d | 删除子节点 |
| ADMIN | a | 设置节点的 ACL |
认证方式有 world(所有人)、auth(已认证用户)、digest(用户名密码)、ip(IP 地址)等。
在开发测试环境通常使用 OPEN_ACL_UNSAFE(所有人可操作),生产环境应当配置合适的 ACL 策略。
十二、ZooKeeper 使用建议
12.1 集群规模
ZooKeeper 集群推荐使用奇数个节点(3、5、7 个),原因是过半机制:
- 3 节点集群:最多允许 1 个节点宕机(2 > 3/2)。
- 4 节点集群:也最多允许 1 个节点宕机(3 > 4/2)。
4 节点和 3 节点的容错能力相同,但 4 节点需要更多的同步开销。所以 3 就够了,用 4 反而浪费。
12.2 数据大小
每个 ZNode 的数据量不要超过 1MB(ZooKeeper 的默认限制)。实际使用中,建议控制在 几 KB 以内。ZooKeeper 是协调服务,不要把它当数据库用。
12.3 Watch 的使用
由于 Watch 是一次性的,在回调中要及时重新注册。推荐使用 Curator 框架,它封装了 Watch 的自动重注册逻辑(如 TreeCache、NodeCache),大大降低了使用复杂度。
12.4 连接管理
客户端和 ZooKeeper 集群之间维护着一个 TCP 长连接(Session)。如果客户端在 Session 超时时间内没有发送心跳,ZooKeeper 会认为客户端已断开,删除其创建的所有临时节点。
Session 超时时间需要合理设置:太短容易因为网络波动误判;太长则在客户端真正崩溃后需要等很久才能释放资源。通常设置为 5~30 秒。
十三、ZooKeeper 的持久化机制
ZooKeeper 虽然数据全部在内存中,但为了崩溃恢复,它提供了两种持久化手段:
事务日志(Transaction Log)
每一次写操作(create/setData/delete)都会先写入事务日志文件。事务日志是顺序写入的,性能很高。即使节点崩溃,重启后也可以通过重放事务日志恢复数据。
数据快照(Snapshot)
ZooKeeper 会定期将内存中的完整数据树导出为快照文件。快照不需要每次写操作都做,只在达到一定事务数量后触发。
恢复流程:先加载最近的快照,再重放快照之后的事务日志,即可恢复到崩溃前的状态。
这和 Redis 的 AOF + RDB 持久化策略非常类似:事务日志对应 AOF,数据快照对应 RDB。
十四、ZooKeeper 与 Dubbo 的配合
在 Dubbo 生态中,ZooKeeper 最常见的角色是作为注册中心:
- Provider 启动时在 ZooKeeper 上创建临时节点注册服务。
- Consumer 启动时从 ZooKeeper 订阅服务列表。
- Provider 下线时临时节点自动删除,Consumer 通过 Watch 机制收到通知。
- Consumer 根据更新后的服务列表进行负载均衡。
ZooKeeper 的临时节点 + Watch 机制天然适合这个场景:节点挂了自动注销,列表变了自动通知。
不过随着 Nacos 的崛起,越来越多的新项目选择 Nacos 作为注册中心。Nacos 的优势在于:配置管理功能更完善、支持 AP/CP 切换、自带控制台、运维更简单。
小结
ZooKeeper 是分布式系统中久经考验的协调组件。理解它需要把握四个核心:
- 数据模型:ZNode 树结构 + 四种节点类型,其中临时节点是实现分布式锁和服务发现的关键。
- ZAB 协议:Leader 单点写入 + 过半确认 + 全局有序,保证了 CP 特性。
- Watcher 机制:一次性触发的事件通知,是实现配置管理和服务发现的基础。
- Leader 选举:遇强投强(zxid 优先,sid 兜底),过半即当选。
ZooKeeper 不是万能的,它的定位是"协调服务"——帮助分布式系统中的节点达成一致。数据量大用数据库,缓存用 Redis,服务注册发现用 Nacos。把合适的工具用在合适的场景,才是分布式架构的正确打开方式。