github:https://github.com/apache/zookeeper
官网:https://zookeeper.apache.org/
Zookeeper 是一个开源的分布式协调服务,用于管理分布式应用程序的配置、命名服务、分布式同步和组服务。其核心是通过高效的一致性协议(如 Zab 协议)保证分布式系统的数据一致性,它负责存储和管理大家都关心的数据,然后接受观察者的注册,一旦这些数据的状态发生变化,Zookeeper 就将负责通知已经在 Zookeeper 上注册的那些观察者做出相应的反应。。
zxid
实现)。ZooKeeper 集群包含以下角色:
数据模型:
/services/db
)。zxid
最大原则)和数据同步。ZooKeeper 采用树形结构(类似文件系统)存储数据,称为 ZNode 树。每个节点(ZNode)具有以下特性:
/
分隔的层级路径唯一标识(如 /services/db/master
)。version
),用于实现原子操作(如 CAS)。示例 ZNode 树结构:
/
├── /config
│ └── database_url (存储数据库连接信息)
├── /services
│ ├── service1 (临时节点,表示在线服务实例)
│ └── service2 (临时节点)
└── /locks
└── lock-00000001 (顺序节点,用于分布式锁)
ZooKeeper 支持多种 ZNode 类型,通过组合实现不同功能:
类型 | 特性 | 应用场景 |
---|---|---|
持久节点 | 节点在客户端断开后仍存在 | 存储长期配置(如 /config ) |
临时节点 | 节点生命周期与客户端会话绑定,会话结束自动删除 | 服务实例注册(如 /services ) |
顺序节点 | 节点路径末尾自动追加全局唯一递增序号(如 /locks/lock-00000001 ) |
分布式锁、队列管理 |
持久顺序节点 | 持久节点 + 顺序特性 | 需持久化且有序的场景 |
临时顺序节点 | 临时节点 + 顺序特性 | 临时有序资源分配 |
树形节点存储
Zookeeper 采用树形结构(ZNode 树)存储配置数据,每个节点路径如 /config/database/config/database
,节点可存储配置内容(如 JSON/XML 格式)。节点类型分为持久节点(PERSISTENT)和临时节点(EPHEMERAL),配置管理通常使用持久节点。
Watcher 监听机制
客户端通过注册 Watcher 监听特定节点(如getData("/config", true)
)。当节点数据变更时,Zookeeper 主动推送事件通知客户端。Watcher 为一次性触发,需在回调函数中重新注册以实现持续监听。
版本控制(Versioning)
每个节点包含版本号 version,更新操作需验证版本号:
setData(path,data,versioncurrent)
若 versioncurrent 与服务端不一致,操作失败,防止并发冲突。
setData("/config", new_data)
getData("/config")
拉取最新数据集群数据一致性保障
Zookeeper 通过 ZAB 协议实现集群节点间的数据同步。当配置变更时,Leader 节点会将操作序列化为事务,通过两阶段提交(Phase 1:Proposal,Phase 2:Commit)广播给所有 Follower 节点,确保所有节点数据最终一致。
树形节点存储结构
集群中所有节点共享同一个 ZNode 树结构,例如:
/cluster-config
/database
/master (存储主库配置)
/slave1 (存储从库配置)
/service
/api-timeout (服务超时参数)
每个节点最大存储数据量为1MB,适合存储小规模配置信息。
分布式 Watcher 机制
客户端在任意集群节点注册 Watcher 后,事件通知由服务端集群统一管理。例如,当节点 /cluster-config/database
数据变更时,所有订阅该节点的客户端都会收到跨集群的事件通知。
tickTime=2000
initLimit=5
时,超时时间为 5×2000=10000mssyncLimit=2
(对应 2×2000=4000ms 超时)/tmp
目录(易被系统清理)dataDir=/var/lib/zookeeper/data
dataLogDir=/var/lib/zookeeper/log
clientPort
clientPort=2181
3server.id
集群节点声明格式:
server.1=node1:2888:3888
server.2=node2:2888:3888
server.3=node3:2888:3888
端口说明:
2888
:Leader-Follower 数据同步端口3888
:选举通信端口autopurge.snapRetainCount=3
autopurge.purgeInterval=24
# 基础配置
tickTime=2000
initLimit=10
syncLimit=5
dataDir=/data/zookeeper
clientPort=2181
# 集群节点配置
server.1=192.168.1.101:2888:3888
server.2=192.168.1.102:2888:3888
server.3=192.168.1.103:2888:3888
# 日志管理
autopurge.purgeInterval=24
autopurge.snapRetainCount=5
状态初始化
所有节点启动时均为 LOOKING
状态,触发首次选举。每个节点先投自己一票,包含其 ZXID
和 SID
。
选票交换规则
ZXID
(事务ID),数值越大表示数据越新;若 ZXID
相同,则选择 SID
(服务器ID)较大的节点。Leader
。选举过程示例
假设集群有3个节点(SID = 1、2、3,初始 ZXID = 0):
(ZXID = 0, SID = 1)
;(ZXID = 0, SID = 2)
;(ZXID = 0, SID = 3)
;SID = 3
最大,最终节点3以3票当选 Leader
,其余节点转为 FOLLOWING
。触发条件
当 Leader
节点宕机或网络中断时,剩余节点重新进入 LOOKING
状态,触发新一轮选举。
选票比较逻辑
ZXID
的 epoch
(任期),若 epoch
相同则比较 counter
(事务计数器);ZXID
完全相同,再比较 SID
。动态改票机制
节点在收到更高优先级的投票时(如更大的 ZXID
或 SID
),会更新自己的投票并广播给其他节点,加速达成共识。
示例场景
假设原 Leader
(SID = 3,ZXID = 0x1001)宕机,剩余节点 ZXID 分别为:
ZXID
更大,优先成为新 Leader
。维度 | 首次选举 | 非首次选举 |
---|---|---|
触发条件 | 集群初始化 | Leader 故障或失联 |
ZXID 初始值 | 全为0 | 包含历史事务数据 |
选举复杂度 | 较高(需全员协商) | 较低(已有数据参考) |
改票频率 | 频繁(初始状态无优先级) | 较少(已有明确优先级) |
ZXID 结构:
ZXID = epoch × 232 + counter
每次选举后 epoch
递增,确保旧 Leader
无法干扰新任期.
网络优化:采用 TCP 连接减少丢包,逻辑时钟(epoch
)避免历史投票干扰。
zookeeper 的节点主要分为临时节点和持久节点。根据生命周期和顺序性可分为四类:
类型 | 生命周期 | 顺序性 | 子节点限制 | 典型应用场景 |
---|---|---|---|---|
持久节点 | 永久存在,需手动删除 | 无 | 允许创建子节点 | 存储配置信息、元数据 |
持久顺序节点 | 永久存在,需手动删除 | 有序 | 允许创建子节点 | 分布式任务队列、全局有序ID生成 |
临时节点 | 随会话结束自动删除 | 无 | 禁止创建子节点 | 心跳检测、服务注册 |
临时顺序节点 | 随会话结束自动删除 | 有序 | 禁止创建子节点 | 分布式锁、选举协调 |
生命周期:
节点创建后永久存在于 ZooKeeper 中,除非显式调用 delete
删除。
特性:
/config
下创建 /config/database
)用途:
存储需长期保存的数据,如系统配置、集群元数据。
顺序性:
ZooKeeper 自动在节点名称后追加单调递增的10位数字序号(例如 /task/task-0000000001
)。
用途:
生命周期:
节点的存在与客户端会话绑定。若客户端断开连接或会话超时,节点自动删除。
限制:
临时节点不能创建子节点(例如在 /service
下创建 /service/node1
后,无法再创建 /service/node1/subnode
)。
用途:
组合特性:
同时具备临时节点的生命周期和顺序节点的序号特性。
用途:
/lock/lock-0000000001
)判断节点类型:
通过 Curator 框架的 Stat
对象中的 ephemeralOwner
属性可区分节点类型。若 ephemeralOwner
值为0,则为持久节点;非0则为临时节点。
// 示例:使用Curator检查节点类型
Stat stat = curatorFramework.checkExists().forPath("/node");
if (stat.getEphemeralOwner() == 0) {
System.out.println("持久节点");
} else {
System.out.println("临时节点");
}
Zookeeper 的监听器(Watcher)是一种事件驱动机制,允许客户端实时感知 ZNode 的状态变化。其核心特性包括:
exists
、getData
、getChildren
等 API 注册。zk.getData("/sanguo", new Watcher() {
@Override
public void process(WatchedEvent event) {
// 处理事件逻辑
}
}, null);
此时服务端会记录客户端对 /sanguo
节点的监听关系。
EventType.NodeDataChanged
EventType.NodeDeleted
EventType.NodeChildrenChanged
客户端通过单线程从事件队列取出事件,调用 process()
方法:
public void process(WatchedEvent event) {
System.out.println("检测到事件类型:" + event.getType());
// 重新注册监听器以持续监听[^3]
zk.getData(event.getPath(), this, null);
}
此时原 Watcher 已失效,需在回调中重新注册才能继续监听
sessionTimeout
参数(默认2倍 tickTime)维持长连接特性 | 说明 | 数学表达 |
---|---|---|
一次性监听 | 每个 Watcher 仅触发一次,避免服务端状态维护压力 | W a c t i v e W_{active} Wactive = ∑ i = 1 n ∑_{i=1}^{n} ∑i=1n W i W_i Wi |
事件有序性 | 客户端保证事件处理的 FIFO 顺序 | E 1 → E 2 → E 3 E1 → E2 → E3 E1→E2→E3 |
轻量级通知 | 仅通知事件类型,不传递具体数据,需客户端主动查询 | N o t i f i c a t i o n = ( T y p e , P a t h ) Notification = (Type,Path) Notification=(Type,Path) |
配置中心
// 注册配置节点监听
zk.getData("/config/database", watcher, null);
当数据库配置变更时,立即触发应用配置刷新。
分布式锁释放检测
zk.exists("/lock/resource1", lockWatcher);
当锁持有者会话断开时,临时节点删除触发锁释放通知
服务发现
监控/services
子节点变化,实时更新服务实例列表。
Zookeeper 的写数据机制基于 ZAB(Zookeeper Atomic Broadcast)协议,确保分布式环境下数据的一致性。其核心流程如下:
setData
)。写成功条件:收到半数以上节点的 ACK(即满足 n ≥ N 2 + 1 n ≥ \frac{N}{2}+1 n≥2N+1)
Zookeeper 的动态上下线机制主要依赖其临时节点和 Watcher 监听机制实现。
服务注册
服务节点启动时,在 Zookeeper 的 /servers
路径下创建临时顺序节点(如 /servers/server000000001
),节点数据存储服务元信息(IP、端口等)。
服务发现
客户端首次启动时,直接读取/servers
下所有子节点,获取当前可用服务列表。
监听注册
客户端通过 exists(path, true)
或 getChildren(path, true)
注册对 /servers
节点的子节点变更监听。
动态更新
NodeChildrenChanged
事件 → 客户端更新列表。Zookeeper 通过临时顺序节点 + Watcher 监听实现分布式锁,核心原理基于其强一致性和顺序性特征。
/locks
路径下创建临时顺序节点(如 /locks/lock_00000001
)// 使用Curator简化实现
public class ZkDistributedLock {
private final CuratorFramework client;
private final String lockPath = "/locks/resource_lock";
private InterProcessMutex lock;
public ZkDistributedLock() {
client = CuratorFrameworkFactory.newClient("localhost:2181",
new RetryNTimes(3, 1000));
client.start();
lock = new InterProcessMutex(client, lockPath); // 封装锁逻辑
}
public void executeWithLock(Runnable task) throws Exception {
lock.acquire(); // 获取锁(阻塞)
try {
task.run(); // 执行业务代码
} finally {
lock.release(); // 释放锁
}
}
}
// 1. 创建临时顺序节点
String nodePath = zk.create("/locks/lock_",
new byte[0],
ZooDefs.Ids.OPEN_ACL_UNSAFE,
CreateMode.EPHEMERAL_SEQUENTIAL);
// 2. 获取所有子节点并排序
List<String> children = zk.getChildren("/locks", false);
Collections.sort(children);
// 3. 判断是否为最小节点
int index = children.indexOf(nodePath.substring(nodePath.lastIndexOf('/') + 1));
if (index == 0) {
return true; // 获取锁
}
// 4. 监听前序节点
String prevNode = children.get(index - 1);
zk.exists("/locks/" + prevNode, watchedEvent -> {
if (watchedEvent.getType() == Watcher.Event.EventType.NodeDeleted) {
// 前序节点删除 → 重新尝试获取锁
}
});