Dubbo注册发现全过程:注册、订阅、Directory刷新与故障恢复
Dubbo 注册发现解决的是“Consumer 怎样持续获得可以调用的 Provider 地址和服务元数据”。注册中心不是网关,不在 RPC 数据面中转请求;业务调用阶段通常由 Consumer 依据本地目录直接连接 Provider。
只记住“Provider 注册、Consumer 订阅”还不够。生产系统真正困难的是:注册的是接口还是应用实例、地址变化怎样原子替换、本地连接能否复用、通知丢失怎么办、ZooKeeper Session 过期有什么窗口、注册中心宕机后新旧进程分别怎样表现、下线时为什么还可能有流量。
一、学习目标
学完本页,应能做到:
- 区分注册中心、元数据中心、配置中心和 RPC Provider。
- 区分 Dubbo 2.x 常见接口级发现与 Dubbo 3 应用级服务发现。
- 画出 Provider 暴露、注册、元数据发布和 Consumer 订阅全过程。
- 解释
RegistryDirectory如何把地址快照转换为可调用Invoker快照。 - 解释新增、修改、删除地址时连接如何复用和销毁。
- 解释 ZooKeeper 临时节点、Session、Watcher 和失效窗口。
- 分析注册中心启动失败、运行期断连、缓存陈旧和空地址通知。
- 使用日志、注册数据、Consumer 目录和连接证据排查
No provider available。
二、先分清控制面与数据面
flowchart TD
A["Provider启动"] --> B["注册中心写入地址或实例"]
C["Consumer启动"] --> D["订阅地址和元数据"]
B --> D
D --> E["Consumer本地Directory"]
E --> F["Router过滤候选"]
F --> G["LoadBalance选择Invoker"]
G --> H["Consumer直连Provider"]| 平面 | 承载内容 | 是否承载业务请求 |
|---|---|---|
| 注册发现控制面 | 应用、接口、实例地址、版本、分组、协议、变更通知 | 否 |
| 配置治理控制面 | 路由、动态配置、标签、限流等规则 | 否 |
| RPC 数据面 | 请求帧、参数、附件、响应、异常 | 是 |
注册中心不可用不一定让已有 RPC 立刻全部中断;Provider 网络不可达则会直接影响数据面。反过来,Provider 端口可达也不代表服务可发现,Consumer 可能因为接口、版本、分组、协议或路由不匹配而没有候选地址。
三、注册中心到底保存什么,不保存什么
注册数据通常描述:
- 应用名或服务接口。
- Provider IP、端口和协议。
- 接口、group、version。
- 序列化、方法和能力元数据,具体存放位置依版本而异。
- 实例健康或可用状态。
- 路由、覆盖配置的引用或规则。
它通常不保存:
- 一次订单请求的参数和响应。
- Provider 正在执行的 JVM 栈。
- 数据库事务最终是否提交。
- Consumer 的业务幂等结果。
因此,注册发现只能回答“候选地址是什么”,不能证明某个地址此刻一定能完成请求,更不能解决业务一致性。
四、服务身份不是只有接口名
Dubbo 服务匹配通常至少涉及:
interface + group + version + protocol + namespace/environmentProvider:
@DubboService(group = "trade", version = "2.0.0")
public class StockServiceImpl implements StockService {
}Consumer:
@DubboReference(group = "trade", version = "2.0.0")
private StockService stockService;| Provider | Consumer | 结果 |
|---|---|---|
trade/2.0.0 | trade/2.0.0 | 身份维度匹配,继续检查协议和路由 |
trade/2.0.0 | trade/1.0.0 | 通常不会成为候选 |
asset/2.0.0 | trade/2.0.0 | group 不匹配 |
| Triple Provider | Consumer 不支持该协议 | 地址存在也可能不可用 |
端口探测成功只能说明 TCP/HTTP 端口可达,不能证明 serviceKey 能在 Provider 的 Exporter 映射中找到。
五、Dubbo 2.x接口级发现与Dubbo 3应用级发现
5.1 接口级服务发现
Dubbo 2.x 传统模型常为每个服务接口注册完整 Provider URL。以 ZooKeeper 为例,简化目录可能是:
/dubbo
/com.example.stock.api.StockService
/providers
dubbo%3A%2F%2F10.0.0.11%3A20880%2F...
dubbo%3A%2F%2F10.0.0.12%3A20880%2F...
/consumers
/routers
/configurators优点是接口和地址直接对应,模型容易理解;代价是一个应用暴露几百个接口时,会产生大量重复实例信息、节点和推送数据,注册中心压力随“实例数 × 接口数”增长。
5.2 应用级服务发现
Dubbo 3 面向云原生的主线是应用级服务发现:注册中心主要登记应用实例,接口到应用的映射和详细服务定义由服务元数据体系配合完成。多个接口共享同一应用实例信息,减少注册数据冗余。
flowchart TD
A["Provider应用实例"] --> B["注册应用名、地址、端口和修订信息"]
A --> C["发布服务定义与元数据"]
D["Consumer引用接口"] --> E["解析接口到应用的映射"]
E --> F["订阅目标应用实例"]
F --> G["结合实例与服务元数据形成Invoker"]应用级发现不等于“以后没有接口元数据”。RPC 仍需知道接口、方法、参数、group、version 和协议;变化的是注册中心中实例与接口信息的组织、映射和发布方式。
5.3 双模型迁移为什么存在
存量系统可能仍使用接口级发现,新服务逐步使用应用级发现。Dubbo 3 不同版本提供迁移规则或双注册、双订阅能力,常见思想包括:
- 强制接口级发现。
- 应用级优先,必要时回退接口级。
- 强制应用级发现。
- 同时注册接口与应用实例用于过渡。
配置名和默认值会随 Dubbo 3 小版本变化,必须以项目锁定版本的官方配置参考为准,不能从其他版本复制参数后假设已经生效。迁移验收应观察 Consumer 最终使用了哪一种 Directory 和地址来源。
六、注册中心、元数据中心和配置中心的区别
| 组件 | 核心职责 | 数据示例 |
|---|---|---|
| 注册中心 | Provider/应用实例发现 | IP、端口、应用、健康状态 |
| 元数据中心 | 服务定义和映射 | 接口、方法、参数、应用映射、revision |
| 配置中心 | 治理规则动态下发 | 路由、标签、动态覆盖、迁移规则 |
在某些部署中三种职责可能由同一种产品承载,例如 ZooKeeper 或 Nacos,但逻辑职责仍应分开。产品相同不等于数据模型和失败影响相同。
元数据 revision 可以理解为一份服务定义的内容指纹。多个实例服务定义相同,可以引用同一修订;服务定义改变时产生新修订,Consumer 才知道需要读取不同元数据。具体计算和存储实现依 Dubbo 版本而异。
七、Provider从启动到可发现的完整过程
Provider 服务暴露的对象链详见服务暴露全过程。这里聚焦注册发现部分:
flowchart TD
A["解析ServiceConfig"] --> B["把实现包装为Provider Invoker"]
B --> C["Protocol.export并建立Exporter映射"]
C --> D["启动或复用协议Server"]
D --> E["构造注册地址和服务元数据"]
E --> F["向Registry注册地址或应用实例"]
F --> G["发布元数据或接口映射"]
G --> H["Consumer收到新快照"]7.1 为什么通常先完成本地暴露再注册
如果先把地址公开给 Consumer,而协议端口和 Exporter 尚未准备好,Consumer 可能立即选中这个实例并连接失败。先建立本地可调用能力,再发布发现信息,可以缩小“已发现但未就绪”的窗口。
但“端口已监听”仍不等于业务完全就绪。数据库连接池、缓存预热、规则加载可能还没完成。生产环境应明确 Dubbo 注册时机、应用 readiness 和容器探针之间的关系,必要时通过延迟注册、实例状态或发布编排控制流量。
7.2 注册失败是否让应用启动失败
取决于版本、check、容错注册和部署配置:
- 强校验:注册中心不可用时启动失败,避免产生不可发现的 Provider。
- Failback:本地暴露成功后记录失败任务,后台重试注册。
- 只暴露不注册:仅适合明确的直连或测试场景。
必须通过启动日志和注册数据验证实际策略,不能只看到 Java 进程存在就认为服务已上线。
八、Consumer从引用接口到本地Directory的过程
flowchart TD
A["解析ReferenceConfig"] --> B["确定服务身份和注册中心"]
B --> C["创建RegistryDirectory或服务发现Directory"]
C --> D["订阅Provider、实例和治理规则"]
D --> E["收到第一份地址快照"]
E --> F["按协议和配置生成远程Invoker"]
F --> G["Cluster.join包装多Invoker"]
G --> H["ProxyFactory生成接口代理"]Consumer 注入的是接口代理。代理背后不是固定 Provider,而是 Cluster Invoker;它在调用时从 Directory 读取当前快照。因此 Provider 扩缩容后,业务 Bean 一般不需要重新注入。
8.1 启动时没有Provider怎么办
常见行为受引用的 check、懒初始化、本地缓存和版本配置影响:
- 强校验时,第一份可用地址为空可能导致启动失败。
- 关闭启动检查后,应用可先启动,但真正调用时仍会失败。
- 有合法本地缓存时,部分版本可先使用缓存地址并继续恢复订阅。
关闭 check 不是让服务变得可用,只是把失败从启动阶段推迟到调用阶段。核心同步依赖是否允许这样做,应由启动依赖策略决定。
九、RegistryDirectory如何对账地址快照
注册中心通知不是“拿到一个 IP 就直接覆盖字段”。Consumer 需要把 Registry URL、Provider URL、Consumer 参数和动态覆盖规则合并,筛选协议,再生成可调用对象。
一次地址更新可以抽象为:
flowchart TD
A["收到新URL或实例快照"] --> B["校验类别、协议和服务身份"]
B --> C["合并Consumer参数与动态配置"]
C --> D["以规范化地址构建新Map"]
D --> E["复用未变化的Invoker"]
E --> F["为新增或实质变化地址创建Invoker"]
F --> G["原子发布不可变Invoker快照"]
G --> H["延后销毁已移除Invoker和连接"]9.1 新增、保留、变化、删除
| 类型 | 处理 | 为什么 |
|---|---|---|
| 新增地址 | Protocol.refer 创建远程 Invoker | 后续调用可选择新实例 |
| 未变化地址 | 复用旧 Invoker 和底层连接 | 避免每次通知重建长连接 |
| 参数变化 | 视有效 URL 变化重建或更新 | timeout、weight、serialization 可能影响调用 |
| 删除地址 | 新快照不再包含,旧 Invoker 延后销毁 | 防止新请求继续选中,同时保护并发读快照 |
判断“是否变化”不能只比较 IP。协议、端口、服务身份、参数和治理规则都可能影响 Invoker 行为。
9.2 为什么发布不可变快照
调用线程可能正在遍历旧候选,通知线程同时刷新地址。如果直接原地修改共享 List,可能出现:
- 遍历时结构变化。
- 新旧参数混合。
- Invoker 已销毁但仍被新调用选中。
- 短暂空列表或半成品。
常见设计是先在局部构建完整新 Map/List,再通过引用替换对调用线程可见;旧快照中的资源等并发调用离开后再安全销毁。具体字段和锁实现依 Dubbo 版本而异,但“先构建、后发布、再回收”是理解 Directory 刷新的关键。
9.3 空地址通知与空保护
“注册中心通知没有 Provider”可能代表真实全量下线,也可能来自注册中心短暂异常、错误 namespace 或不完整推送。传统 Dubbo 注册协议中可用特殊 empty URL 表达某类地址为空;较新版本还可能有空地址保护能力。
两种策略都有代价:
| 策略 | 优点 | 风险 |
|---|---|---|
| 接受空快照 | 能快速反映真实全量下线 | 瞬时错误会清空可用目录 |
| 保留旧快照 | 控制面抖动时继续调用 | 真实下线后继续访问陈旧地址 |
不能笼统说“永远忽略空通知”。应结合注册中心协议、通知版本、业务风险和实例健康证据,并监控当前快照年龄。
十、本地缓存和Failback恢复
传统 Dubbo 注册客户端可把最后一次注册/订阅信息持久化到本地缓存文件,具体默认路径、文件名和开关随版本及配置而异。缓存的作用是帮助启动或控制面恢复,不是永久替代注册中心。
10.1 本地缓存能解决什么
- 注册中心短暂不可用时提供上次已知地址。
- 客户端重启时减少完全无地址窗口。
- 为故障排查保留最后一次发现信息。
10.2 本地缓存不能保证什么
- 缓存地址仍然存活。
- 新扩容实例能够被发现。
- 已下线实例一定被移除。
- 路由和动态配置仍然最新。
使用缓存时应记录来源和年龄。长时间使用旧快照应告警,而不是把“RPC还能通”当作注册中心已经恢复。
10.3 Failback任务
注册、取消注册、订阅、取消订阅或通知处理失败时,Failback Registry 类实现通常会记录失败任务并后台重试。重试必须具备幂等性:重复注册或重复订阅不应创建无界重复状态。
十一、ZooKeeper作为Dubbo注册中心的真实语义
11.1 临时节点与Session所有权
传统接口级 Provider 常使用临时节点。临时节点属于 ZooKeeper Session,不是属于某条 TCP 连接:
- 短暂断线但 Session 未过期,节点仍存在。
- 客户端在超时内重连到其他 ZooKeeper Server,可继续原 Session。
- Session 最终过期后,服务端删除该 Session 的临时节点。
- 客户端必须建立新 Session 并重新注册、重新订阅。
所以“网线一断节点立刻消失”是错误理解。
11.2 Watcher是变化提示,不是可靠消息队列
传统 ZooKeeper Watch 通常是一次触发语义。Dubbo 客户端或 Curator 等封装会在读取数据后重新设置 Watch,但不能把它解释为每次变化都形成一条永久、可重放的消息。
正确消费模式是:
flowchart TD
A["收到Watcher变化提示"] --> B["重新读取目标节点完整当前状态"]
B --> C["重新注册Watcher或由缓存封装维护"]
C --> D["把当前状态与本地快照对账"]
D --> E["发布新Directory快照"]即使中间发生多次增删,最终也应通过重新读取当前状态收敛,而不是依赖“每个事件都不丢”。
11.3 Session失效窗口
Provider 崩溃后,在 Session 超时前临时节点仍可能存在,Consumer 仍可能选中旧地址。Consumer 必须依赖连接失败、调用超时、Cluster 容错、熔断和健康治理止损,不能把注册中心剔除当成毫秒级故障检测器。
Session 超时过短会让网络抖动、Stop-The-World GC 或调度停顿导致误过期;过长会延长死实例残留时间。该值需要结合网络、GC、部署环境和故障恢复目标设定。
十二、ZooKeeper、Nacos、Consul、etcd和Kubernetes的关系
| 产品 | 协调基础 | Dubbo使用关注点 |
|---|---|---|
| ZooKeeper | ZAB、Session、临时节点、Watch | 传统Dubbo成熟;关注Session与Watcher语义 |
| Nacos | 服务发现与配置能力,内部协议随版本和数据类型不同 | 关注临时/持久实例、客户端与服务端版本,不可粗暴概括为全局AP或CP |
| Consul | Server侧Raft、Agent、Catalog、健康检查 | 关注Agent拓扑、健康状态和阻塞查询 |
| etcd | Raft、MVCC revision、Watch、Lease | 关注Lease、压缩后的Watch恢复和线性一致读边界 |
| Kubernetes | Service、EndpointSlice、DNS、readiness | 关注端点传播、Headless Service和平台数据面 |
注册中心产品改变不会改变 Dubbo 数据面的基本事实:Consumer 最终要形成地址/Invoker 快照,并直接或经平台数据面访问 Provider。不同产品的健康、租约、一致性和通知语义不能互相套用。
十三、Provider优雅下线为什么仍有窗口
理想流程:
flowchart TD
A["实例标记不再接收新流量"] --> B["从注册中心取消注册或置为不可用"]
B --> C["等待Consumer收到并发布新快照"]
C --> D["停止接收新RPC请求"]
D --> E["等待存量请求完成"]
E --> F["关闭Server、连接和进程"]现实中的窗口包括:
- 注册中心传播延迟。
- Consumer 与注册中心断连,仍持有旧快照。
- Consumer 调用前已经选中该 Invoker。
- TCP 连接仍存在。
- 容器终止宽限期短于 Dubbo shutdown wait。
- 强制
kill -9没有执行注销和排空。
因此优雅下线需要注册中心、Dubbo、Kubernetes readiness、PreStop、termination grace period 和业务请求最大耗时共同设计。
十四、故障矩阵:启动期和运行期必须分开
| 故障 | 已运行Consumer | 新启动Consumer | Provider注册 |
|---|---|---|---|
| 注册中心短暂不可用 | 可能继续用内存快照 | 取决于本地缓存和check | 新注册可能失败或进入重试 |
| Consumer到注册中心断网 | 数据面可能继续 | 不适用 | Provider侧不一定受影响 |
| Consumer到Provider断网 | 该调用失败/超时 | 地址仍可能发现 | 注册数据可能仍存在 |
| Provider进程崩溃 | Session窗口内可能选中旧地址 | 可能先看到陈旧地址 | Session过期后节点删除 |
| 元数据中心不可用 | 已构建Invoker可能继续 | 应用级发现初始化可能失败 | 元数据发布受影响 |
| 配置中心不可用 | 使用旧路由/配置 | 规则初始化可能受影响 | 注册本身可能正常 |
“注册中心挂了还能不能调用”的准确答案必须先问:哪个客户端、启动还是运行期、是否已有快照、Provider网络是否可达、依赖哪种发现模型。
十五、JDK 8 Demo:地址快照对账与连接复用
下面的无依赖 Demo 模拟 Directory 刷新。它展示三个原则:以稳定 key 对账、复用未变化 Invoker、构建完成后一次性发布新快照。
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
public class DirectoryReconcileDemo {
static final class Invoker {
private final String key;
private boolean destroyed;
Invoker(String key) {
this.key = key;
System.out.println("create=" + key);
}
void destroy() {
destroyed = true;
System.out.println("destroy=" + key);
}
}
static final class Directory {
private volatile Map<String, Invoker> snapshot = Collections.emptyMap();
synchronized void notifyAddresses(List<String> newKeys) {
Map<String, Invoker> old = snapshot;
Map<String, Invoker> next = new LinkedHashMap<String, Invoker>();
for (String key : newKeys) {
Invoker reused = old.get(key);
next.put(key, reused != null ? reused : new Invoker(key));
}
snapshot = Collections.unmodifiableMap(next);
for (Map.Entry<String, Invoker> entry : old.entrySet()) {
if (!next.containsKey(entry.getKey())) {
entry.getValue().destroy();
}
}
}
List<String> currentKeys() {
return new ArrayList<String>(snapshot.keySet());
}
}
public static void main(String[] args) {
Directory directory = new Directory();
directory.notifyAddresses(java.util.Arrays.asList("10.0.0.11:20880", "10.0.0.12:20880"));
directory.notifyAddresses(java.util.Arrays.asList("10.0.0.12:20880", "10.0.0.13:20880"));
System.out.println("snapshot=" + directory.currentKeys());
}
}预期输出:
create=10.0.0.11:20880
create=10.0.0.12:20880
create=10.0.0.13:20880
destroy=10.0.0.11:20880
snapshot=[10.0.0.12:20880, 10.0.0.13:20880]10.0.0.12 没有再次创建,表示可复用原有连接。真实 Dubbo 还要处理 URL 合并、协议、路由、动态配置、引用计数和并发调用生命周期。
十六、商业订单系统场景
订单服务调用库存、优惠、支付三个 Dubbo 服务:
- 库存属于核心同步依赖,启动时要求至少发现可用 Provider。
- 优惠推荐允许降级,可关闭强启动依赖,但调用时必须有明确 fallback。
- 支付服务按
group/version隔离新旧协议,不能使用模糊匹配直接全量迁移。 - Provider 上线先通过 readiness,再进入注册发现;下线先摘流量再排空。
- Consumer 监控每个接口的当前候选数、快照年龄、通知失败和地址来源。
- 注册中心恢复后必须确认目录已收敛,不能只看注册中心端口恢复。
十七、生产排查Runbook
17.1 No provider available
按证据顺序排查:
- 从异常确定接口、group、version、Consumer 应用和时间。
- 在 Provider 日志确认服务完成本地 export,而不只是 Spring Boot 启动完成。
- 确认 Provider 实际注册地址、端口、协议和 namespace。
- 在注册中心查询的是接口级数据还是应用级实例。
- 检查元数据和接口到应用映射。
- 查看 Consumer 收到的原始通知和当前 Directory 候选数。
- 检查 tag、condition、mesh 等 Router 是否把候选过滤为空。
- 检查协议、序列化和 Consumer 支持能力。
- 最后才检查负载均衡;候选为零时负载算法根本没有执行。
17.2 Consumer仍调用已下线地址
检查:
- Provider 是正常注销还是强制退出。
- ZooKeeper Session 是否尚未过期。
- Consumer 是否与注册中心断连。
- 最后通知时间与本地快照年龄。
- Directory 是否拒绝了空通知。
- 旧 Invoker 是否仍被并发调用持有。
- Kubernetes 终止窗口是否足够。
17.3 注册中心恢复但服务仍不可用
注册中心进程恢复只是第一步,还要确认:
- Provider 重新注册完成。
- 应用级元数据重新发布或可读取。
- Consumer 重新订阅成功。
- Directory 候选数恢复。
- 路由规则正确。
- Consumer 到 Provider 数据面网络可达。
17.4 注册IP错误
多网卡、Docker、Kubernetes、VPN 环境中常见。检查 Provider 实际监听地址、注册地址、Consumer 网络视角、NAT 和 NetworkPolicy。不能只在 Provider 本机 localhost 测通。
十八、应该监控哪些指标
- 每个服务引用的 Provider 候选数。
- 地址快照版本和年龄。
- 注册、订阅、通知和重试失败次数。
- 注册中心连接状态和 Session 状态。
- 新增/删除 Invoker 数量。
- 当前连接数和连接建立失败。
- 路由前后候选数量。
No provider次数。- Provider 注销到 Consumer 收敛耗时。
只有注册中心 Server 健康指标,没有 Consumer 本地目录指标,无法判断真正调用面是否已经恢复。
十九、常见错误及后果
| 错误 | 为什么错 | 后果 |
|---|---|---|
| 注册中心转发RPC | 混淆控制面和数据面 | 排查方向错误 |
| Watcher保证每次事件不丢 | Watch是变化提示,最终应重读状态 | 丢失或合并事件时目录不收敛 |
| Provider崩溃节点立刻删除 | 临时节点由Session超时决定 | 低估陈旧地址窗口 |
| 地址不变就无需刷新 | timeout、weight、协议参数可能变化 | 治理规则不生效 |
| 注册中心恢复就代表调用恢复 | Consumer订阅和Directory可能尚未收敛 | 误判恢复完成 |
| 忽略空通知就绝对安全 | 会保留真实下线的旧地址 | 持续调用僵尸实例 |
| Boot进程启动等于Dubbo已注册 | export、register、metadata可能失败 | 发布系统误放流量 |
二十、面试标准回答
Dubbo 注册中心属于控制面,只保存和推送地址、实例或服务元数据,不转发业务请求。Provider 先完成本地服务暴露,再注册接口 URL 或应用实例并发布元数据;Consumer 创建 Directory,订阅地址和治理规则,把通知对账成远程 Invoker 快照,再由 Cluster、Router 和 LoadBalance完成每次调用选择。Dubbo 2.x 常见接口级发现,Dubbo 3 主线是应用级发现并配合元数据,迁移期可能双订阅。ZooKeeper 临时节点绑定 Session,短暂断线不会立即删除,Session 过期后客户端需要重新注册和订阅;Watcher只是变化提示,收到后应重读完整状态。注册中心故障时已有 Consumer 可能继续用本地快照,但新注册、变更感知和新进程启动会受影响,因此还必须监控目录年龄、Failback重试、陈旧地址和优雅下线传播窗口。
二十一、关联知识点
- Dubbo服务暴露、引用与Invoker调用链
- Dubbo负载均衡、容错与治理
- ZooKeeper Watcher、Session与失效窗口
- ZooKeeper ZAB一致性与恢复
- Dubbo面试题
- Spring Cloud注册发现原理
本章小结
Dubbo 服务发现不是“查一次 IP”,而是一条持续收敛链:Provider 本地暴露并发布地址/实例与元数据,Consumer 订阅并把控制面状态对账成不可变 Invoker 快照,调用线程再从快照中路由和选址。真正掌握注册发现,必须能解释接口级与应用级模型、Directory Diff、连接复用、Session窗口、本地缓存、空通知、Failback和优雅下线,而不是只会说“Provider注册、Consumer订阅”。
