本章定位:命名服务是 ZooKeeper 在分布式系统中的另一大典型应用,涵盖全局唯一 ID 生成和服务注册发现两大场景。
定义与作用
命名服务(Naming Service)是分布式系统中将名称映射到资源的机制。ZooKeeper 通过 ZNode 路径和 Sequential 节点提供两种核心能力:
| 能力 | 机制 | 典型应用 |
|---|---|---|
| 全局唯一 ID 生成 | Persistent Sequential 节点 | 订单号、流水号、消息 ID |
| 服务注册与发现 | Ephemeral 节点 + getChildren | 微服务注册中心 |
核心原理
服务注册发现流程
服务提供者创建 Ephemeral 节点注册自己,消费者 Watch 父节点获取在线服务列表。提供者宕机后 Ephemeral 节点自动删除,消费者收到通知并更新列表。
ID 生成架构
多个 ID 生成器实例各自创建 Persistent Sequential 节点,Leader 原子分配序号,保证全局唯一。没有单点故障(Snowflake 依赖机器时钟)。
完整示例
示例一:服务注册与发现
场景说明:订单服务 3 个实例注册,网关动态发现。
# 创建命名空间
zkCli.sh -server 127.0.0.1:2181
[zkshell: 0] create /services ""
Created /services
[zkshell: 1] create /services/order ""
Created /services/order
# 订单服务实例 1 注册(Terminal 1)
zkCli.sh -server 127.0.0.1:2181
[zkshell: 0] create -e /services/order/192.168.1.10:8080 "weight=10"
Created /services/order/192.168.1.10:8080
# 实例 2 注册(Terminal 2)
[zkshell: 0] create -e /services/order/192.168.1.11:8080 "weight=5"
# 实例 3 注册(Terminal 3)
[zkshell: 0] create -e /services/order/192.168.1.12:8080 "weight=10"
# 网关发现服务(Terminal 4)
zkCli.sh -server 127.0.0.1:2181
[zkshell: 0] ls -w /services/order
[192.168.1.10:8080, 192.168.1.11:8080, 192.168.1.12:8080]
# 实例 2 宕机(关闭 Terminal 2)
# 等待 Session Timeout 后...
# 网关自动收到通知
[zkshell: 1] ls -w /services/order
[192.168.1.10:8080, 192.168.1.12:8080]
# 192.168.1.11:8080 已自动移除
操作前后对比:
| 阶段 | 在线实例 | 消费者视图 |
|---|---|---|
| 3 实例全部在线 | 10:8080, 11:8080, 12:8080 | 3 个实例 |
| 实例 2 宕机 | 10:8080, 12:8080 | 2 个实例(自动更新) |
示例二:Java 服务注册实现
场景说明:服务启动时注册,关闭时主动注销。
public class ServiceRegistry {
private ZooKeeper zk;
private String servicePath;
private String instancePath;
public ServiceRegistry(String zkAddr, String serviceName,
String host, int port) throws Exception {
this.zk = new ZooKeeper(zkAddr, 5000, event -> {});
this.servicePath = "/services/" + serviceName;
// 确保父节点存在
if (zk.exists(servicePath, false) == null) {
zk.create(servicePath, null,
ZooDefs.Ids.OPEN_ACL_UNSAFE,
CreateMode.PERSISTENT);
}
// 注册 Ephemeral 节点
String address = host + ":" + port;
this.instancePath = zk.create(
servicePath + "/" + address,
("status=UP").getBytes(),
ZooDefs.Ids.OPEN_ACL_UNSAFE,
CreateMode.EPHEMERAL);
System.out.println("Registered: " + instancePath);
}
public void shutdown() throws Exception {
if (instancePath != null) {
zk.delete(instancePath, -1);
System.out.println("Deregistered: " + instancePath);
}
zk.close();
}
}
// 使用
ServiceRegistry registry = new ServiceRegistry(
"127.0.0.1:2181", "order-service", "192.168.1.10", 8080);
// 应用关闭时
Runtime.getRuntime().addShutdownHook(
new Thread(() -> registry.shutdown()));
操作前后对比:
| 事件 | /services/order-service 节点 | 原因 |
|---|---|---|
| 应用启动 | 新增 192.168.1.10:8080 | create Ephemeral |
| 应用正常关闭 | 删除 192.168.1.10:8080 | delete 主动注销 |
| 应用异常宕机 | 自动删除(~Session Timeout 后) | Ephemeral 自动清理 |
示例三:全局唯一订单号生成
场景说明:分布式订单系统生成唯一订单号。
public class OrderIdGenerator {
private ZooKeeper zk;
private static final String ID_PATH = "/order-ids/ORD-";
public OrderIdGenerator(String zkAddr) throws Exception {
this.zk = new ZooKeeper(zkAddr, 5000, event -> {});
if (zk.exists("/order-ids", false) == null) {
zk.create("/order-ids", null,
ZooDefs.Ids.OPEN_ACL_UNSAFE,
CreateMode.PERSISTENT);
}
}
public String nextId() throws Exception {
String path = zk.create(ID_PATH, null,
ZooDefs.Ids.OPEN_ACL_UNSAFE,
CreateMode.PERSISTENT_SEQUENTIAL);
// path = "/order-ids/ORD-0000000001"
return path.substring(ID_PATH.length());
}
}
// 使用
OrderIdGenerator gen = new OrderIdGenerator("127.0.0.1:2181");
System.out.println(gen.nextId()); // ORD-0000000001
System.out.println(gen.nextId()); // ORD-0000000002
System.out.println(gen.nextId()); // ORD-0000000003
操作前后对比:
| 调用 | 返回 | /order-ids 下节点数 |
|---|---|---|
| nextId() #1 | ORD-0000000001 | 1 |
| nextId() #2 | ORD-0000000002 | 2 |
| nextId() #3 | ORD-0000000003 | 3 |
注意:生成的 Sequential 节点应定期清理(可异步删除或 TTL 自动清理),避免节点数量无限增长。
易错场景与面试考点
易错场景
1. 服务消费者本地缓存与实际不一致
消费者 getChildren 后本地缓存服务列表。如果 Watcher 通知丢失(网络抖动),消费者可能一直使用过时的列表。解决方案:
- 定时轮询(如每 30s 重新 getChildren)
- 调用失败后立即刷新列表
2. 服务注册信息过于庞大
将服务的完整元数据(如健康检查 URL、版本号、权重、标签)全部写在 ZNode 数据中可能超出 1MB 限制。建议在 ZNode 中只存储关键信息(地址 + 权重),详细元数据通过独立 API 提供。
3. Sequential 节点未清理
ID 生成器创建的 Persistent Sequential 节点永久存在,数量积累后:
ls命令变慢- 内存占用增加
- ID 解析时的排序开销增大
应在 ID 消费后异步删除节点。
面试高频题
Q:ZooKeeper 做服务发现与 Eureka 有何区别?
A:
- ZooKeeper(CP):保证一致性,网络分区时少数派不可用
- Eureka(AP):保证可用性,网络分区时各分区独立服务
- 选型:ZooKeeper 适合要求强一致性的场景(如金融服务);Eureka 适合要求高可用的互联网场景
Q:如何实现带权重的负载均衡?
A:在 ZNode 数据中存储权重信息:
create -e /services/order/192.168.1.10:8080 "weight=10"
create -e /services/order/192.168.1.11:8080 "weight=5"
消费者读取子节点列表 + 各自数据 → 按权重加权随机或轮询。
Q:如何处理服务上下线的短暂不一致?
A:依赖 ZooKeeper 的顺序一致性保证——同一个客户端看到的事件顺序与全局顺序一致。结合客户端重试 + 服务端优雅下线(先标记 status=DOWN,等待一段时间后再断开 Session),可大幅减少不一致窗口。
小结
| 要点 | 说明 |
|---|---|
| 服务注册 | Ephemeral 节点,宕机自动清除 |
| 服务发现 | getChildren + Watcher 监听子节点变化 |
| ID 生成 | Persistent Sequential 节点,Leader 原子分配 |
| 负载均衡 | 在节点数据中存储 weight,客户端加权选择 |
| 缓存策略 | Watcher + 定时轮询兜底 |
| 清理 | Sequential 节点需定期清理 |
命名服务解决了"在哪里"和"叫什么"的问题。下一章进入客户端编程,学习如何用 Java 原生 API 与 ZooKeeper 交互。