Skip to content

Dubbo注册发现全过程:注册、订阅、Directory刷新与故障恢复

Dubbo 注册发现解决的是“Consumer 怎样持续获得可以调用的 Provider 地址和服务元数据”。注册中心不是网关,不在 RPC 数据面中转请求;业务调用阶段通常由 Consumer 依据本地目录直接连接 Provider。

只记住“Provider 注册、Consumer 订阅”还不够。生产系统真正困难的是:注册的是接口还是应用实例、地址变化怎样原子替换、本地连接能否复用、通知丢失怎么办、ZooKeeper Session 过期有什么窗口、注册中心宕机后新旧进程分别怎样表现、下线时为什么还可能有流量。

一、学习目标

学完本页,应能做到:

  1. 区分注册中心、元数据中心、配置中心和 RPC Provider。
  2. 区分 Dubbo 2.x 常见接口级发现与 Dubbo 3 应用级服务发现。
  3. 画出 Provider 暴露、注册、元数据发布和 Consumer 订阅全过程。
  4. 解释 RegistryDirectory 如何把地址快照转换为可调用 Invoker 快照。
  5. 解释新增、修改、删除地址时连接如何复用和销毁。
  6. 解释 ZooKeeper 临时节点、Session、Watcher 和失效窗口。
  7. 分析注册中心启动失败、运行期断连、缓存陈旧和空地址通知。
  8. 使用日志、注册数据、Consumer 目录和连接证据排查 No provider available

二、先分清控制面与数据面

mermaid
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 服务匹配通常至少涉及:

text
interface + group + version + protocol + namespace/environment

Provider:

java
@DubboService(group = "trade", version = "2.0.0")
public class StockServiceImpl implements StockService {
}

Consumer:

java
@DubboReference(group = "trade", version = "2.0.0")
private StockService stockService;
ProviderConsumer结果
trade/2.0.0trade/2.0.0身份维度匹配,继续检查协议和路由
trade/2.0.0trade/1.0.0通常不会成为候选
asset/2.0.0trade/2.0.0group 不匹配
Triple ProviderConsumer 不支持该协议地址存在也可能不可用

端口探测成功只能说明 TCP/HTTP 端口可达,不能证明 serviceKey 能在 Provider 的 Exporter 映射中找到。

五、Dubbo 2.x接口级发现与Dubbo 3应用级发现

5.1 接口级服务发现

Dubbo 2.x 传统模型常为每个服务接口注册完整 Provider URL。以 ZooKeeper 为例,简化目录可能是:

text
/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 面向云原生的主线是应用级服务发现:注册中心主要登记应用实例,接口到应用的映射和详细服务定义由服务元数据体系配合完成。多个接口共享同一应用实例信息,减少注册数据冗余。

mermaid
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 服务暴露的对象链详见服务暴露全过程。这里聚焦注册发现部分:

mermaid
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的过程

mermaid
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 参数和动态覆盖规则合并,筛选协议,再生成可调用对象。

一次地址更新可以抽象为:

mermaid
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 连接:

  1. 短暂断线但 Session 未过期,节点仍存在。
  2. 客户端在超时内重连到其他 ZooKeeper Server,可继续原 Session。
  3. Session 最终过期后,服务端删除该 Session 的临时节点。
  4. 客户端必须建立新 Session 并重新注册、重新订阅。

所以“网线一断节点立刻消失”是错误理解。

11.2 Watcher是变化提示,不是可靠消息队列

传统 ZooKeeper Watch 通常是一次触发语义。Dubbo 客户端或 Curator 等封装会在读取数据后重新设置 Watch,但不能把它解释为每次变化都形成一条永久、可重放的消息。

正确消费模式是:

mermaid
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使用关注点
ZooKeeperZAB、Session、临时节点、Watch传统Dubbo成熟;关注Session与Watcher语义
Nacos服务发现与配置能力,内部协议随版本和数据类型不同关注临时/持久实例、客户端与服务端版本,不可粗暴概括为全局AP或CP
ConsulServer侧Raft、Agent、Catalog、健康检查关注Agent拓扑、健康状态和阻塞查询
etcdRaft、MVCC revision、Watch、Lease关注Lease、压缩后的Watch恢复和线性一致读边界
KubernetesService、EndpointSlice、DNS、readiness关注端点传播、Headless Service和平台数据面

注册中心产品改变不会改变 Dubbo 数据面的基本事实:Consumer 最终要形成地址/Invoker 快照,并直接或经平台数据面访问 Provider。不同产品的健康、租约、一致性和通知语义不能互相套用。

十三、Provider优雅下线为什么仍有窗口

理想流程:

mermaid
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新启动ConsumerProvider注册
注册中心短暂不可用可能继续用内存快照取决于本地缓存和check新注册可能失败或进入重试
Consumer到注册中心断网数据面可能继续不适用Provider侧不一定受影响
Consumer到Provider断网该调用失败/超时地址仍可能发现注册数据可能仍存在
Provider进程崩溃Session窗口内可能选中旧地址可能先看到陈旧地址Session过期后节点删除
元数据中心不可用已构建Invoker可能继续应用级发现初始化可能失败元数据发布受影响
配置中心不可用使用旧路由/配置规则初始化可能受影响注册本身可能正常

“注册中心挂了还能不能调用”的准确答案必须先问:哪个客户端、启动还是运行期、是否已有快照、Provider网络是否可达、依赖哪种发现模型。

十五、JDK 8 Demo:地址快照对账与连接复用

下面的无依赖 Demo 模拟 Directory 刷新。它展示三个原则:以稳定 key 对账、复用未变化 Invoker、构建完成后一次性发布新快照。

java
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());
    }
}

预期输出:

text
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

按证据顺序排查:

  1. 从异常确定接口、group、version、Consumer 应用和时间。
  2. 在 Provider 日志确认服务完成本地 export,而不只是 Spring Boot 启动完成。
  3. 确认 Provider 实际注册地址、端口、协议和 namespace。
  4. 在注册中心查询的是接口级数据还是应用级实例。
  5. 检查元数据和接口到应用映射。
  6. 查看 Consumer 收到的原始通知和当前 Directory 候选数。
  7. 检查 tag、condition、mesh 等 Router 是否把候选过滤为空。
  8. 检查协议、序列化和 Consumer 支持能力。
  9. 最后才检查负载均衡;候选为零时负载算法根本没有执行。

17.2 Consumer仍调用已下线地址

检查:

  • Provider 是正常注销还是强制退出。
  • ZooKeeper Session 是否尚未过期。
  • Consumer 是否与注册中心断连。
  • 最后通知时间与本地快照年龄。
  • Directory 是否拒绝了空通知。
  • 旧 Invoker 是否仍被并发调用持有。
  • Kubernetes 终止窗口是否足够。

17.3 注册中心恢复但服务仍不可用

注册中心进程恢复只是第一步,还要确认:

  1. Provider 重新注册完成。
  2. 应用级元数据重新发布或可读取。
  3. Consumer 重新订阅成功。
  4. Directory 候选数恢复。
  5. 路由规则正确。
  6. 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 服务发现不是“查一次 IP”,而是一条持续收敛链:Provider 本地暴露并发布地址/实例与元数据,Consumer 订阅并把控制面状态对账成不可变 Invoker 快照,调用线程再从快照中路由和选址。真正掌握注册发现,必须能解释接口级与应用级模型、Directory Diff、连接复用、Session窗口、本地缓存、空通知、Failback和优雅下线,而不是只会说“Provider注册、Consumer订阅”。