ZooKeeper 是 Apache 的分布式协调服务,为分布式应用提供一致性数据存储、配置管理、命名服务、分布式锁和集群管理。
1. 核心特性
| 特性 | 说明 |
|---|---|
| 顺序一致性 | 客户端的更新按发送顺序执行 |
| 原子性 | 更新操作要么全部成功,要么全部失败 |
| 单一视图 | 所有客户端看到相同的数据视图 |
| 可靠性 | 写入一旦完成,数据将持续有效 |
| 实时性 | 在一定时间内,客户端能读到最新数据 |
2. ZAB 协议 (ZooKeeper Atomic Broadcast)
ZAB 是 ZooKeeper 的一致性协议,类似 Raft 但专为协调服务设计。
2.1 协议阶段
阶段 1: 崩溃恢复(Leader 选举)
- 选举拥有最新数据的节点为 Leader
- Leader 同步数据给 Follower
阶段 2: 消息广播(正常运行)
- 所有写请求转发给 Leader
- Leader 生成 ZXID,广播给 Follower
- 过半 Follower ACK 后提交
2.2 ZXID 结构
ZXID: 64 位
高 32 位: epoch(Leader 轮次)
低 32 位: 事务计数(单调递增)
3. 数据模型
ZooKeeper 提供类文件系统的树形命名空间:
/
├── zookeeper # 内置节点
│ └── config
├── services # 服务注册
│ ├── user-service
│ │ ├── node-001 [10.0.1.1:8080]
│ │ └── node-002 [10.0.1.2:8080]
│ └── order-service
├── config # 配置管理
│ └── database
│ └── url [jdbc:mysql://...]
└── locks # 分布式锁
└── order-123
节点类型:
| 类型 | 说明 |
|---|---|
| Persistent | 持久节点,客户端断开后仍存在 |
| Ephemeral | 临时节点,会话结束自动删除(用于服务注册) |
| Sequential | 顺序节点,自动附加递增序号 |
ZK 应用场景全景
场景 1:分布式配置中心
// 配置集中管理,所有节点监听变更
// 优点:配置变更实时生效,无需重启应用
public class DistributedConfig {
private static final String CONFIG_PATH = "/config/app";
public void init() {
// 1. 加载配置
loadConfig(CONFIG_PATH);
// 2. 注册监听器
zk.watch(CONFIG_PATH, event -> {
if (event.getType() == NodeDataChanged) {
loadConfig(CONFIG_PATH); // 热更新
log.info("配置已热更新");
}
});
}
}
场景 2:分布式唯一 ID 生成
// 利用 Sequential 节点特性生成全局唯一 ID
String path = zk.create("/seq/id-", data, EPHEMERAL_SEQUENTIAL);
// 返回 /seq/id-0000000001,序号即唯一 ID
long id = Long.parseLong(path.substring(path.lastIndexOf('-') + 1));
场景 3:Master 选举
所有节点竞争创建同一个临时节点,创建成功者成为 Master,其他节点监听该节点的删除事件。
节点 A 创建 /election/master (成功) → A 成为 Master
节点 B 创建 /election/master (失败) → B 监听 /election/master
节点 A 崩溃 → /election/master 自动删除
节点 B 收到事件 → B 创建 /election/master → B 成为 Master
适用边界:存储状态数据 < 1MB。ZK 不是通用数据库,其设计目标是协调而非存储。
ZooKeeper 提供类文件系统的树形命名空间:
/
├── zookeeper # 内置节点
│ └── config
├── services # 服务注册
│ ├── user-service
│ │ ├── node-001 [10.0.1.1:8080]
│ │ └── node-002 [10.0.1.2:8080]
│ └── order-service
├── config # 配置管理
│ └── database
│ └── url [jdbc:mysql://...]
└── locks # 分布式锁
└── order-123
节点类型:
| 类型 | 说明 |
|---|---|
| Persistent | 持久节点,客户端断开后仍存在 |
| Ephemeral | 临时节点,会话结束自动删除(用于服务注册) |
| Sequential | 顺序节点,自动附加递增序号 |
4. Watcher 机制
// 一次性 Watch
zk.getData("/config/database", new Watcher() {
@Override
public void process(WatchedEvent event) {
if (event.getType() == EventType.NodeDataChanged) {
// 重新读取并重新注册 Watch
byte[] data = zk.getData("/config/database", this, null);
System.out.println("配置更新: " + new String(data));
}
}
}, null);
// Curator 框架的持久 Watch
curator.watchers().add().withMode(AddWatchMode.PERSISTENT)
.usingWatcher((CuratorWatcher) event -> {
System.out.println("事件: " + event);
}).forPath("/services");
注意:Watch 是一次性的,触发后需重新注册。
5. 分布式锁
5.1 排他锁
// Curator 互斥锁
InterProcessMutex lock = new InterProcessMutex(curator, "/locks/resource-1");
try {
if (lock.acquire(10, TimeUnit.SECONDS)) {
// 执行业务逻辑
}
} finally {
lock.release();
}
5.2 共享锁(读写锁)
InterProcessReadWriteLock rwlock = new InterProcessReadWriteLock(
curator, "/locks/rw-resource");
// 读锁
InterProcessMutex readLock = rwlock.readLock();
readLock.acquire();
// 写锁
InterProcessMutex writeLock = rwlock.writeLock();
writeLock.acquire();
5.3 羊群效应优化
原始方案:所有客户端 Watch 父节点,导致大量通知。
优化方案:每个客户端只 Watch 前一个顺序节点。
public class ImprovedDistributedLock {
public void lock() throws Exception {
// 1. 创建临时顺序节点
String node = curator.create()
.withMode(CreateMode.EPHEMERAL_SEQUENTIAL)
.forPath("/locks/mylock/lock-");
// 2. 获取所有子节点
List<String> nodes = curator.getChildren().forPath("/locks/mylock");
Collections.sort(nodes);
// 3. 如果是第一个,获取锁
if (node.endsWith(nodes.get(0))) {
return;
}
// 4. 找到前一个节点,只 Watch 它
int index = nodes.indexOf(node.substring(node.lastIndexOf('/') + 1));
String prevNode = nodes.get(index - 1);
CountDownLatch latch = new CountDownLatch(1);
curator.watchers().add()
.usingWatcher((org.apache.zookeeper.Watcher) event -> {
if (event.getType() == Event.EventType.NodeDeleted) {
latch.countDown();
}
}).forPath("/locks/mylock/" + prevNode);
// 5. 等待前一个节点删除
latch.await();
}
}
6. Leader 选举
// Curator Leader 选举
LeaderSelector selector = new LeaderSelector(curator, "/election/leader",
new LeaderSelectorListenerAdapter() {
@Override
public void takeLeadership(CuratorFramework client) throws Exception {
System.out.println("成为 Leader!");
// 保持领导地位(此方法不返回则保持领导权)
Thread.sleep(Long.MAX_VALUE);
}
});
selector.autoRequeue();
selector.start();
7. 配置管理
// 配置中心
public class ZkConfigCenter {
private static final String CONFIG_PATH = "/config/app";
private final Map<String, String> configCache = new ConcurrentHashMap<>();
public void init() throws Exception {
// 加载配置
List<String> keys = curator.getChildren().forPath(CONFIG_PATH);
for (String key : keys) {
byte[] data = curator.getData().forPath(CONFIG_PATH + "/" + key);
configCache.put(key, new String(data));
}
// 监听变更
curator.watchers().add().withMode(AddWatchMode.PERSISTENT_RECURSIVE)
.usingWatcher(event -> reloadConfig())
.forPath(CONFIG_PATH);
}
}
8. 与 etcd 对比
| 特性 | ZooKeeper | etcd |
|---|---|---|
| 协议 | ZAB | Raft |
| API | 专用客户端 | gRPC + HTTP |
| Watch | 一次性 | 长期流式 |
| 事务 | 多版本并发 | 简单事务 |
| K8s | 旧版用 | 原生集成 |
| 性能 | 万级 TPS | 万级 TPS |
| 生态 | Hadoop/HBase/Kafka | K8s/CoreDNS/TiKV |
ZooKeeper 性能调优与运维
连接数与 Watch 数限制
单个 ZooKeeper 节点的连接数上限约为 6 万(受文件描述符限制),Watch 数量过多会导致内存压力。监控指标:
# ZK 四字命令查看状态
echo mntr | nc localhost 2181
# 关键指标:
# zk_num_alive_connections 当前连接数
# zk_watch_count Watch 总数
# zk_approximate_data_size 数据总量
# zk_open_file_descriptor_count 文件描述符使用
# zk_max_file_descriptor_count 文件描述符上限
ZK 集群部署方案
| 模式 | 节点数 | 容错能力 | 适用场景 |
|---|---|---|---|
| 单机 | 1 | 无 | 开发测试 |
| 集群 | 3 | 1 节点故障 | 生产标准 |
| 集群 | 5 | 2 节点故障 | 高可用需求 |
| 集群 | 7+ | 3+ 节点故障 | 不推荐(选举延迟增加) |
奇数法则:ZK 集群节点数应为奇数。5 节点集群和 6 节点集群都最多容忍 2 个节点故障,但 6 节点写入时需要 4 个 ACK,延迟更高。
延伸阅读
- https://plumephp.com/distributed-id-generation/ — 分布式 ID 生成策略对比
- https://plumephp.com/observability-kubernetes/ — K8s 集群监控与 etcd 运维
- https://plumephp.com/event-driven-architecture-domain-events/ — 分布式协调与事件驱动
总结
ZooKeeper 是分布式协调领域的经典实现,ZAB 协议、Watcher 机制和临时节点设计至今仍被广泛借鉴。在 Hadoop、Kafka、HBase 等大数据生态中,ZK 的地位短期内难以撼动。
然而,对于新项目,建议优先考虑 etcd:
- API 现代化:gRPC + HTTP/JSON,多语言 SDK 更丰富
- K8s 原生集成:作为 K8s 的默认数据存储,云原生项目的自然选择
- Watch 更优雅:长期流式 Watch,无需反复注册
- Raft 协议:更简洁的共识算法,社区理解度更高
选型决策应根据技术栈生态匹配:大数据项目选 ZK,云原生项目选 etcd,通用协调需求两者皆可。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。