深蓝黑背景封面:左侧大标题「四种 znode,各管一件事」,副标题「临时性与顺序性,两个正交的开关」;右侧原创示意图——2×2 网格四张节点卡片:持久、临时、持久顺序、临时顺序,横轴标注「是否带顺序号」、纵轴标注「是否随会话消失」,右下角临时顺序卡片高亮;左上标注「面试经典 · 系统设计」,右上标注「Day 01 · 10-29」 图 1:本文封面,两个开关组合出四种节点。

先说明属性:这是一篇经典题解析,题目出自本地历史面试资料,与任何公司的当前招聘无关。题目问「znode 有哪几种」,标准答案是四种,但比背下四个名字更重要的是看清它们其实是两个独立开关的组合:持久还是临时、无序还是顺序。两个开关各自解决一类协调难题。

第一个开关:临时性——节点跟谁同生共死。

持久节点一旦创建就一直在那里,直到被显式删除。它跟创建者的存亡无关,适合存放配置、元数据这类「长寿命」信息。

临时节点则绑定在创建它的会话上:Zookeeper 官方文档的定义是,临时节点只在创建它的会话存活期间存在,会话结束节点就被删除。这个语义直接把一类老大难问题消灭了——「持锁者宕机了谁来收拾残局」。服务注册就是最典型的用法:实例上线就在注册目录下建一个临时节点,实例宕机、会话超时,节点自动消失,服务列表自动收敛,不需要任何外部清理进程。

临时性还有一个容易忽略的约束:官方文档明确,临时节点不允许有子节点。道理很直白——父节点随时可能随会话蒸发,挂在一朵随时会消失的云下面的子树没法保证一致性。

第二个开关:顺序性——节点名字里带一个全局序号。

请求创建顺序节点时,Zookeeper 会在你给的路径后面追加一个单调递增的计数器后缀,格式是十位的十进制数字(前补零),比如 task-0000000001。这个计数器由父节点维护,在同一个父节点下保证唯一且递增。

顺序性解决的是「谁先谁后」的问题。没有它,十个客户端同时抢一个资源,你只能靠运气;有了它,大家各自拿一个序号,比大小就有了公平的先来后到——这就是分布式锁、分布式队列的排队基础。注意计数器是有符号整型,官方文档提醒,超过 2147483647 会溢出回绕,超长寿命的顺序目录要留意这一点。

两个开关组合,得到四种 znode。

持久节点:两个开关都关,存配置、存元数据。临时节点:只开临时性,做服务注册、探活。持久顺序节点:只开顺序性,生成全局唯一且递增的名字,比如分布式 ID、消息序号。临时顺序节点:两个都开,既有会话绑定的自动清理、又有公平排队的序号——这是分布式锁的标准零件,持锁者挂掉锁自动释放,等待者按序号递补。

顺带一提,3.5.3 版本之后 Zookeeper 又增加了容器节点(最后一个子节点被删除后,容器本身未来会被服务器清理)和 TTL 节点(带超时且期间无子节点修改即过期)两种类型;但面试题问的「四种」指经典的两个开关组合,新类型作为补充了解即可。

用代码把两个开关的语义跑一遍。

下面用一段内存模拟把四种行为演出来:顺序后缀、最小号持锁、临时节点不能有子节点、会话结束自动清理。用 Node.js 22.15.0 直接运行(node znode.js):

'use strict';

// 迷你 Zookeeper 数据模型:演示四种 znode 的语义开关
class MiniZk {
  constructor() {
    this.nodes = new Map(); // path -> { session, children }
    this.seq = new Map();   // 父路径 -> 下一个顺序号(计数器按父节点维护)
  }
  create(path, { session = null, sequential = false } = {}) {
    if (sequential) {
      const next = (this.seq.get(path) ?? 0) + 1;
      this.seq.set(path, next);
      path = path + String(next).padStart(10, '0'); // %010d 单调递增后缀
    }
    if (this.nodes.has(path)) throw new Error('节点已存在');
    const parent = path.slice(0, path.lastIndexOf('/')) || '/';
    if (path !== '/' && !this.nodes.has(parent)) throw new Error('父节点不存在');
    if (this.nodes.get(parent)?.session) throw new Error('临时节点不能有子节点');
    this.nodes.set(path, { session, parent });
    return path;
  }
  closeSession(session) { // 会话结束:该会话创建的临时节点全部删除
    const gone = [];
    for (const [path, node] of this.nodes) {
      if (node.session === session) { this.nodes.delete(path); gone.push(path); }
    }
    return gone;
  }
  has(path) { return this.nodes.has(path); }
}

const zk = new MiniZk();
zk.create('/');           // 根
zk.create('/config');     // 持久节点:配置数据,与谁创建它无关
zk.create('/locks');      // 锁目录
const lock1 = zk.create('/locks/task-', { session: 'S1', sequential: true });
const lock2 = zk.create('/locks/task-', { session: 'S2', sequential: true });
const lock3 = zk.create('/locks/task-', { session: 'S3', sequential: true });
console.log(`三个临时顺序节点:${lock1} / ${lock2} / ${lock3}`);
console.log(`最小号持锁:${[lock1, lock2, lock3].sort()[0]}`);

try {
  zk.create(lock1 + '/child', { session: 'S1' });
} catch (e) {
  console.log(`给临时节点建子节点:${e.message}`);
}

const gone = zk.closeSession('S2'); // S2 宕机,会话结束
console.log(`S2 会话结束后被自动清理:${gone}`);
console.log(`锁位次递补:${[lock1, lock3].filter((p) => zk.has(p)).sort()[0]} 成为最小号`);
console.log(`持久节点 /config 仍在:${zk.has('/config')}`);

实测输出:

三个临时顺序节点:/locks/task-0000000001 / /locks/task-0000000002 / /locks/task-0000000003
最小号持锁:/locks/task-0000000001
给临时节点建子节点:临时节点不能有子节点
S2 会话结束后被自动清理:/locks/task-0000000002
锁位次递补:/locks/task-0000000001 成为最小号
持久节点 /config 仍在:true

模拟里最值得细看的是最后三行:S2 宕机,它名下的 002 号节点被自动清掉,不需要任何人发删除命令;锁的位次自动递补,而挂在树上的持久节点 /config 纹丝不动。临时性回答「谁来兜底清理」,顺序性回答「谁先谁后」——两个开关的分工一目了然。

上半部分语义矩阵:四种 znode 由「持久/临时」「无序/顺序」两个维度组合而成——持久节点适合配置数据,临时节点随会话结束自动删除适合服务注册探活,持久顺序节点自动生成单调递增后缀适合全局序号,临时顺序节点两者兼得。下半部分两个场景示意:左「会话绑定」——客户端与临时节点用一条会话线相连,会话断开后节点被打叉自动清除;右「公平排队」——同一目录下三个临时顺序节点带 0001/0002/0003 后缀排成一列,最小号持锁,其余各自监听前驱。底部总结:临时性回答「谁来兜底清理」,顺序性回答「谁先谁后」 图 2:两个开关、四种组合、两类场景。

工程里的选型对应。

模型注册、任务认领、租约续约这类协调需求,最后都落在「节点类型选哪种」上。选错的代价很具体:该用临时节点却用了持久节点,实例宕机后注册表里全是尸体,要额外写清理任务;该用顺序节点却自己生成名字,公平性就得自己造轮子。做 AI 平台时,推理实例的注册与下线就是临时节点的教科书场景。

回到面试现场,建议的答题结构:先给两个正交开关,再组合出四种类型并各配一个场景,然后补临时节点不能有子节点、顺序计数器会溢出两个细节,最后点一句新版本的容器与 TTL 节点。定义答全、语义答透、细节兜底,这道题就满了。你在用 Zookeeper 或同类协调组件时,选错过节点类型吗?欢迎说说后果。