如何通过 Zookeeper 实现服务注册与发现?
结论先行:提供者在
/服务名/providers下写临时节点,节点名就是可调用的 URL;消费者 watch 该目录的子节点变化,拿到全量列表并缓存在本地。会话超时或主动关闭时临时节点消失,即完成“自动下线”。
一、目录约定
/dubbo/com.demo.UserService
├── providers
│ ├── dubbo://10.0.0.1:20880/com.demo.UserService?timeout=3000&weight=100
│ └── dubbo://10.0.0.2:20880/com.demo.UserService?timeout=3000&weight=100
├── consumers
│ └── consumer://10.0.0.9/com.demo.UserService?application=web
├── routers
│ └── condition://0.0.0.0/com.demo.UserService?category=routers&rule=...
└── configurators
└── override://0.0.0.0/com.demo.UserService?timeout=500
把“参数”放在节点路径而不是节点数据里,好处是 ls 一次就能拿到全部信息,不用逐个 get。
二、注册与订阅时序
Provider ZK Consumer
│ create EPHEMERAL │ │
│ /providers/dubbo://... │ │
├───────────────────────────────>│ │
│ │<──── getChildren + watch ─┤
│ │──── 返回列表 + 注册 watch ─>│
│ │ │ 本地缓存
│ │<──── 调用(直连,不经 ZK)─┤
│ 心跳(会话保活) │ │
├───────────────────────────────>│ │
│ 进程崩溃 / kill -9 │ │
│ (不再续约会话) │ │
│ │── 会话超时,删临时节点 ───>│
│ │── NodeChildrenChanged ───>│
│ │ │ 重新 getChildren
关键点:调用流量不走 ZK,ZK 只负责名单;因此 ZK 短暂不可用不会立刻中断已有调用。
三、会话与超时的调优
# 服务端
tickTime=2000
maxSessionTimeout=40000
minSessionTimeout=4000
# 客户端(Curator)
sessionTimeoutMs=10000
connectionTimeoutMs=5000
| 参数 | 过小 | 过大 |
|---|---|---|
| sessionTimeout | 网络抖动就误摘除,实例反复上下线 | 宕机后长时间仍有流量打向死实例 |
| connectionTimeout | 重连风暴 | 启动期等待过久 |
经验值:会话超时取 10~30 秒,配合优雅停机主动反注册,避免只依赖超时清理。
四、消费者侧的两个必备兜底
// 1. 本地文件缓存:ZK 不可用时仍能启动并调用
// Dubbo 默认把地址列表写入 ~/.dubbo/dubbo-registry-*.cache
// 2. 列表为空时不要清空可用地址,保留上一次快照并告警
if (invokers.isEmpty()) {
log.warn("empty provider list, keep last snapshot");
return lastSnapshot;
}
常见追问
- 追问:为什么用临时节点而不是持久节点加心跳字段?临时节点由服务端在会话失效时自动清理,不依赖客户端自觉,避免“僵尸节点”长期占位。
- 追问:ZK 挂了新实例还能注册吗?不能。新实例无法被发现,但老实例之间的调用不受影响,这就是“降级可用”。
- 追问:和 Nacos 的差别?Nacos 用长连接推送 + 健康检查,注册数据可按应用聚合,且支持配置管理;ZK 是纯目录 + watch,节点数随“接口 × 实例”膨胀。
小结
- 临时节点 + 会话 = 自动上下线;watch + 本地缓存 = 动态名单。
- 路径携带参数,一次
ls拿全量,减少往返。 - 调用直连,ZK 只做名单,所以 ZK 抖动不等于业务中断。
- 会话超时是双刃剑,配合优雅停机才能既快又准。