本章定位:全面对比 ZooKeeper 的四类节点组合类型,理解各自的创建方式、生命周期和典型应用场景。
定义与作用
ZooKeeper 提供 4 种节点类型,由两个维度组合而成:
| 维度 | 选项 | 含义 |
|---|---|---|
| 持久性 | Persistent / Ephemeral | 断开连接后节点是否保留 |
| 顺序性 | 普通 / Sequential | 是否自动追加 10 位递增编号 |
组合后得到 4 种类型:
Persistent(持久) Persistent + Sequential(持久顺序)
Ephemeral(临时) Ephemeral + Sequential(临时顺序)
ZooKeeper 3.5.3 后还引入了 Container 节点和 TTL 节点,前者在最后一个子节点删除后自动删除,后者基于 TTL 过期自动删除。完整节点类型共 6 种:Persistent、Persistent Sequential、Ephemeral、Ephemeral Sequential、Container、TTL。
核心原理
各类型生命周期
临时节点在 Session 结束时由服务端自动删除,不会留下"孤儿"节点。临时节点不能拥有子节点(Container 和 TTL 节点除外)。
类型特性对比
| 特性 | Persistent | Ephemeral | Persistent Sequential | Ephemeral Sequential |
|---|---|---|---|---|
| Session 断开后 | 保留 | 删除 | 保留 | 删除 |
| 可否有子节点 | 可以 | 不可以 | 可以 | 不可以 |
| 名称格式 | 用户定义 | 用户定义 | 用户定义 + 10 位序号 | 用户定义 + 10 位序号 |
| 典型场景 | 配置存储 | 服务注册 | 全局唯一 ID | 分布式锁 |
| 创建命令 | create /path data | create -e /path data | create -s /path data | create -e -s /path data |
Container 节点(扩展类型)
Container 节点是 ZooKeeper 3.5.3 引入的持久节点变体,核心语义是当最后一个子节点被删除时,容器节点自动被清理。它解决了"递归创建目录结构后需手动逐层清理"的运维痛点。
启用方式:需在 zoo.cfg 中设置 zookeeper.extendedTypesEnabled=true(3.5.3~3.5.x 需要,3.6.0+ 默认开启)。
创建命令:
create -c /containers/my-container "container-data"
# Created /containers/my-container
自动清理机制:
Container 节点的清理由 Leader 异步执行,不是即时删除,存在短暂的时间窗口。Leader 每
cnxTimeout左右执行一次容器清理任务。
重要特性:
| 特性 | 说明 |
|---|---|
| 子节点限制 | 可以有子节点 |
| Session 退出后 | 保留(属于持久类) |
| 清理触发 | 最后一个子节点被删除后,容器在下次清理周期中被删除 |
| 清理范围 | 不递归——只清理到孙子层为空的 Container 节点,不向更深层传播 |
| 实现类 | ContainerManager,Leader 端运行 |
| 删除接口超时 | 如果创建子节点时 Container 已经不存在,服务端返回 NoNodeException,客户端需自行 exists() 检测并重建 |
清理不递归的含义:
create -c /grandparent # 容器 A
create /grandparent/parent # 持久节点 B
create -c /grandparent/parent/leaf # 容器 C
delete /grandparent/parent/leaf # 容器 C 被清理
# /grandparent/parent 不会被自动删除(它是持久节点)
# /grandparent 有子节点 parent,不会触发清理
TTL 节点(扩展类型)
TTL 节点创建时指定一个存活时间(毫秒),到期后由服务端自动删除。适用于"临时状态但希望跨 Session 保留一段时间"的场景——例如某个请求的中间结果,在请求处理完成后一定时间内自动回收。
启用方式:需在 zoo.cfg 中设置 zookeeper.extendedTypesEnabled=true,并配置 zookeeper.ttlEnabled=true(3.6.0+)。默认 TTL 过期检查间隔由 zookeeper.ttlIntervalInSeconds 控制(默认 300 秒,即 5 分钟)。
创建命令:
# TTL 值以毫秒为单位
create -t 5000 /ttl-nodes/cache "cached-data"
# Created /ttl-nodes/cache (约 5 秒后自动删除)
TTL 编码规则:TTL 值以 8 字节(Big-Endian long)存储在节点的 stat 结构中,其值为 System.currentTimeMillis() + ttl(创建时刻 + 存活时间),即一个绝对到期时间戳而非相对时长。
过期清理机制:
| 机制 | 说明 |
|---|---|
| 检查触发 | TTLNodeDeletePolicy 在服务端定期扫描 |
| 扫描范围 | 全局 TTL 节点——不区分 create -t 和 setData + stat.ttl |
| 删除时机 | System.currentTimeMillis() > stat.ttl 时删除 |
| 子节点处理 | TTL 节点的子节点(若有)在父节点过期删除时成为孤儿(ZooKeeper 不会级联删除子节点),需避免给 TTL 节点创建子节点 |
TTL 节点与临时节点的区别:
| 维度 | 临时节点 | TTL 节点 |
|---|---|---|
| 生命周期 | 绑定 Session | 绑定到期时间 |
| Session 重连 | 节点保留 | 与 Session 无关,不受影响 |
| 能否跨 Session | 否 | 是 |
| 典型场景 | 服务在线性(注册中心) | 带有效期缓存(短时中间结果) |
完整示例
示例一:Ephemeral 节点生命周期验证
场景说明:模拟微服务注册场景,服务启动注册临时节点,停止后自动清除。
操作前状态:ZooKeeper 集群正常,无临时节点。
# Client A —— 连接到 ZooKeeper
zkCli.sh -server 127.0.0.1:2181
[zkshell: 0] create -e /services/order-service-1 "http://192.168.1.10:8080"
Created /services/order-service-1
[zkshell: 1] ls /services
[order-service-1]
# Client B —— 验证可见性
zkCli.sh -server 127.0.0.1:2181
[zkshell: 0] ls /services
[order-service-1]
模拟服务宕机(关闭 Client A 终端):
# Client B 再次查看
[zkshell: 1] ls /services
[]
# 临时节点 order-service-1 已被 ZooKeeper 自动删除
操作后状态:
| 时间点 | /services 子节点 | 说明 |
|---|---|---|
| Client A 创建 | [order-service-1] | 服务在线 |
| Client A 断开 10 秒后 | [] | Session 过期,节点自动删除 |
示例二:Sequential 节点序号生成
场景说明:使用 Sequential 节点实现分布式唯一 ID 生成。
# 多个客户端同时创建
# Client A:
[zkshell: 0] create -s /ids/req- "request-data"
Created /ids/req-0000000001
# Client B(几乎同时):
[zkshell: 0] create -s /ids/req- "another-request"
Created /ids/req-0000000002
# Client C:
[zkshell: 0] create -s /ids/req- "third-request"
Created /ids/req-0000000003
操作前后对比:
| 维度 | 操作前 | 操作后 |
|---|---|---|
| /ids 子节点 | 空 | req-0000000001 ~ req-0000000003 |
| 序号保证 | — | 单调递增,全局唯一 |
| 并发安全 | — | 由 Leader 原子分配 |
序号由父节点维护的计数器生成,范围为 int(4 字节),溢出后会变为负数(
req--2147483648)。实际使用中这个范围绰绰有余。
示例三:Ephemeral Sequential 实现分布式锁(最小序号策略)
场景说明:电商秒杀服务中多个实例竞争锁。
// 每个客户端创建一个临时顺序节点
String lockPath = zk.create("/lock/req-", data,
ZooDefs.Ids.OPEN_ACL_UNSAFE,
CreateMode.EPHEMERAL_SEQUENTIAL);
// 获取 /lock 下所有子节点并排序
List<String> children = zk.getChildren("/lock", false);
Collections.sort(children);
// 判断是否是最小序号
String myNode = lockPath.substring("/lock/".length());
if (children.get(0).equals(myNode)) {
System.out.println("我获取了锁: " + lockPath);
} else {
// 监听前一个节点的删除事件
int myIndex = children.indexOf(myNode);
String prevNode = children.get(myIndex - 1);
zk.exists("/lock/" + prevNode, event -> {
System.out.println("前一个节点释放,我获取锁");
});
}
操作前后对比:
| 状态 | 描述 |
|---|---|
| 操作前 | 3 个客户端各自创建节点:req-001, req-002, req-003 |
| 竞争结果 | 持有 req-001 的客户端获取锁 |
| req-001 释放后 | req-002 客户端收到通知,获取锁 |
示例四:Container 节点自动清理验证
场景说明:使用 Container 节点管理一批中间计算结果,当所有子结果被消费后容器自动清理。
操作前状态:
[zkshell: 0] ls /
[zookeeper]
[zkshell: 1] create -c /results ""
Created /results
操作过程:
# 创建子节点存放中间结果
[zkshell: 2] create /results/step1 "intermediate-data-1"
Created /results/step1
[zkshell: 3] create /results/step2 "intermediate-data-2"
Created /results/step2
[zkshell: 4] ls /results
[step1, step2]
# 消费完成后逐个子节点删除
[zkshell: 5] delete /results/step1
[zkshell: 6] delete /results/step2
# 等待 Leader 清理周期后
[zkshell: 7] ls /results
# KeeperErrorCode = NoNode for /results
# 容器 /results 已被自动删除
操作前后对比:
| 时间点 | /results 状态 | 说明 |
|---|---|---|
| 初始 | 不存在 | — |
| create -c 后 | 存在,空容器 | 等待子节点 |
| 子节点存在时 | 存在,含 2 个子节点 | 正常工作 |
| 所有子节点删除后 | 不存在(自动清理) | 无需手动 delete /results |
示例五:TTL 节点创建与过期
场景说明:缓存一条短时有效的通知消息,5 秒后自动失效。
操作前状态:
[zkshell: 0] ls /notifications
Node does not exist: /notifications
操作过程:
# TTL = 5000 毫秒(5 秒)
[zkshell: 1] create /notifications ""
Created /notifications
[zkshell: 2] create -t 5000 /notifications/msg-01 "maintenance alert"
Created /notifications/msg-01
# 5 秒内查看
[zkshell: 3] get /notifications/msg-01
maintenance alert
# 等待 5 秒后(依赖 TTL 检查周期)
[zkshell: 4] get /notifications/msg-01
# KeeperErrorCode = NoNode for /notifications/msg-01
操作前后对比:
| 时刻 | /notifications/msg-01 | 说明 |
|---|---|---|
| 创建时 | 存在,data=maintenance alert | TTL 计时开始 |
| 5 秒内 | 存在 | 可正常读写 |
| TTL 到期后 | 被删除 | 无需手动清理 |
易错场景与面试考点
易错场景
1. 给 Ephemeral 节点创建子节点
create -e /ephemeral-node "data"
# Created /ephemeral-node
create /ephemeral-node/child "child"
# KeeperErrorCode = NoChildrenForEphemerals
临时节点不允许拥有子节点,这是 ZooKeeper 的硬约束。
2. Ephemeral 节点在 Session 迁移期间的状态
如果客户端断开后在同一次 Session 超时前重新连接到另一个 Server,Ephemeral 节点不会丢失。只有当 Session 真正过期(超过 timeout)时,Ephemeral 节点才会被删除。
3. Sequential 计数器溢出
计数器是有符号 int,最大值 2147483647。溢出后变为 -2147483648,节点名格式变为 prefix--2147483648。虽然极少发生,但在极端高频场景下应知晓此行为。
4. Container 节点子节点并发删除的竞态
// 客户端 A:删除最后一个子节点
zk.delete("/container/last-child", -1);
// 客户端 B:几乎同时尝试在同一个 Container 下创建新子节点
zk.create("/container/new-child", data, ...);
// 可能收到 NoNodeException:因为 Container 已在清理周期中被删除
应对方案:创建前先 exists() 检查,若 Container 被删则重新 create -c 重建容器。
5. TTL 节点在服务器时间回拨时的异常行为
TTL 到期时间戳 System.currentTimeMillis() + ttl 是绝对时间。如果服务器发生 NTP 时钟同步导致时间回拨(例如回拨 10 分钟),已到期的 TTL 节点可能"复活"——原先判定为过期的节点,新的 currentTimeMillis() 小于到期时间戳,节点重新变为"未过期"。生产环境务必确保 ZooKeeper 服务器的 NTP 同步稳定。
面试高频题
Q:ZNode 到底有几种类型?(高频陷阱)
A:标准回答是 6 种:
- 4 种持久类型:Persistent、Persistent Sequential、Ephemeral、Ephemeral Sequential
- 2 种扩展类型(3.5.3+):Container、TTL
如果只回答 4 种会丢分,面试官可能追问"知道 Container 节点和 TTL 节点吗?"。同时需要说明 Container 在最后一个子节点删除后自动清理、TTL 基于绝对到期时间自动删除的实现区别。
Q:Container 节点和 Persistent 节点的区别?
A:两者都是持久节点(Session 断开后保留),但 Container 在最后一个子节点删除后会被 Leader 异步清理,而普通 Persistent 节点永远保留直到显式删除。Container 适合"一批中间结果的临时目录"场景。
Q:Ephemeral 节点和 Persistent 节点的核心区别?
A:三个维度:
- 生命周期:Ephemeral 绑定 Session,Session 结束自动删除;Persistent 永久存在
- 子节点:Ephemeral 不能有子节点,Persistent 可以
- 使用场景:Ephemeral 表达"存活"语义(服务注册、分布式锁),Persistent 表达"持久状态"(配置中心)
Q:为什么 Ephemeral 节点不能有子节点?
A:保证删除语义的简单性。如果 Ephemeral 可以有子节点,Session 过期时需要递归删除整棵子树,增加了实现复杂度和删除过程中的不一致窗口。
Q:Sequential 节点在分布式锁中的作用?
A:多个竞争者各自创建 EPHEMERAL_SEQUENTIAL 节点,序号最小的获取锁。其他竞争者 Watch 前一个节点,形成公平的排队机制,避免惊群效应(Herd Effect)。
小结
| 要点 | 说明 |
|---|---|
| 4 种组合类型 | Persistent / Ephemeral × 普通 / Sequential |
| Ephemeral | 绑定 Session,适合服务注册 |
| Sequential | 自动递增编号,适合分布式锁和 ID 生成 |
| Ephemeral+Sequential | 分布式锁的最佳组合 |
| Ephemeral 不可有子节点 | 硬约束 |
| Container/TTL | 3.5.3+ 引入,增强自动清理能力 |
理解了节点类型,接下来深入 ACL 权限控制,学习如何保护 ZNode 的安全性。