MingJunDuan的博客
热爱可抵一切,探索未知之境
全站访问量

Dubbo3 服务级注册与发现深度剖析 —— 从接口到应用的架构变革

一个应用有 100 个接口,部署 100 个实例,ZooKeeper 上会有多少个注册节点?

Dubbo 2 的答案是 10,000 个。Dubbo 3 的答案是 100 个。这不是魔法,是注册粒度的根本性变革——从”接口级”升级为”应用级”。

本文从源码层面,拆解 Dubbo 3.2 服务级注册的完整实现,以及 Consumer 如何通过 Mapping、Instance、Metadata 三步发现 Provider 的 Facade。


一、问题背景:Dubbo 2 接口级注册的三大痛点

1.1 ZK 节点数量爆炸

在 Dubbo 2 中,注册的粒度是接口(Interface)。Provider 每暴露一个接口,就在 ZK 上创建一个独立的注册节点。Consumer 每订阅一个接口,就注册一个 Watcher。节点总数 = 接口数 × 实例数

┌─────────────────────────────────────────────────────────────────────┐
│                        一个中型应用的注册数据量                          │
│                                                                     │
│   接口数: 100                                                        │
│   实例数: 100                                                        │
│                                                                     │
│   Dubbo 2(接口级):                                                  │
│     Provider 节点: 100接口 × 100实例 = 10,000 个                    │
│     Consumer 节点: 100接口 × 100实例 = 10,000 个                    │
│     总计: ~20,000 个 ZK 节点                                        │
│                                                                     │
│   每次有一个实例上下线:                                               │
│     ZK 需要推送 10,000 次 Watcher 事件                               │
│     Consumer 需要刷新 100 个接口的 Invoker 列表                       │
└─────────────────────────────────────────────────────────────────────┘

ZK 本质上是为协调设计的,不是为海量小数据存储设计的。随着微服务拆分越来越细、实例数越来越多,ZK 的负载直线上升。ZooKeeper 3.4.x 版本中,单个 ZNode 默认最大 1MB 数据,但 ZK 不适合存储大量节点,因为它的写操作是串行化的。

1.2 推送风暴

Provider 实例重启(这在 K8s 环境下几乎是家常便饭),会触发一连串连锁反应:

┌──────────────────────────────────────────────────────────────────────┐
│                       一次 Provider 重启的连锁反应                      │
│                                                                      │
│  Provider 重启                                                        │
│    │                                                                  │
│    ▼                                                                  │
│  临时节点删除 (Session 过期)                                           │
│    │                                                                  │
│    ▼                                                                  │
│  ZK 触发 Watcher 回调 ──► 所有订阅该接口的 Consumer                    │
│    │                      通知数目 = 接口数 × Consumer实例数           │
│    ▼                                                                  │
│  Consumer 收到通知,全量拉取 /dubbo/{interface}/providers              │
│    │                                                                  │
│    ▼                                                                  │
│  Consumer 刷新 Invoker 列表、重建负载均衡、重建集群容错                  │
│                                                                      │
│  如果 Provider 是滚动重启(K8s 中常见):                               │
│    100 个实例重启 = 100 次推送风暴,每次都重刷全部 Invoker              │
└──────────────────────────────────────────────────────────────────────┘

ZK 的 Watcher 机制本身就是一次性的——触发后需要重新注册。在接口级模型下,每个 Consumer 需要为每个订阅的接口注册 Watcher,数量大、管理复杂。

1.3 接口级粒度与云原生生态割裂

Dubbo 2 的 URL 格式是这样的:

dubbo://192.168.1.100:20880/com.example.OrderService?
  version=1.0.0&
  group=demo&
  timeout=3000&
  methods=getOrder,createOrder&
  ...

这个 URL 里混合了:

  • 服务实例信息(IP、端口)
  • 接口维度信息(接口名、方法、参数)
  • 治理规则(超时、重试、负载均衡策略)

在 Spring Cloud / Kubernetes 的世界里,服务发现的标准模型是 “应用 → 实例列表”,接口级别的信息放在元数据里。Dubbo 2 的模型与云原生体系(Istio、Consul、Eureka/Nacos)天然不兼容,阻碍了 Dubbo 向云原生的演进。


二、Dubbo 3 核心变革:从”接口”到”应用”

2.1 一句话概括

Dubbo 2:注册的是”哪个接口在哪个机器上”。 Dubbo 3:注册的是”哪个应用在哪个机器上”,接口信息作为元数据单独存储。

2.2 架构全景对比

┌──────────────────────────────────────────────────────────────────────────┐
│                          Dubbo 2(接口级注册)                             │
│                                                                            │
│   Provider-A (192.168.1.100)                     ZooKeeper                │
│   ┌───────────────────┐                     ┌──────────────────────┐      │
│   │ OrderService      │──注册──────────────►│ /dubbo/              │      │
│   │ UserService       │──注册──────────────►│   OrderService/      │      │
│   │ ProductService    │──注册──────────────►│     providers/       │      │
│   │ ... × 100 接口    │──注册──────────────►│       dubbo://....   │      │
│   └───────────────────┘                     │   UserService/       │      │
│                                              │     providers/       │      │
│   Provider-B (192.168.1.101)                 │       dubbo://....   │      │
│   ┌───────────────────┐                     │   ProductService/    │      │
│   │ OrderService      │──注册──────────────►│     providers/       │      │
│   │ UserService       │──注册──────────────►│       dubbo://....   │      │
│   │ ProductService    │──注册──────────────►│   ... × 100 接口     │      │
│   │ ... × 100 接口    │──注册──────────────►│                      │      │
│   └───────────────────┘                     └──────────────────────┘      │
│                                                                            │
│   注册节点数 = Protider数 × 接口数 = 100 × 100 = 10,000                    │
└──────────────────────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────────────────────┐
│                          Dubbo 3(应用级注册)                             │
│                                                                            │
│   Provider-A (192.168.1.100)                   ZooKeeper                  │
│   ┌───────────────────┐                   ┌────────────────────────┐     │
│   │ application:      │──注册────────────►│ /services/              │     │
│   │   my-provider     │                   │   my-provider/          │     │
│   │                   │                   │     192.168.1.100:20880 │     │
│   │ 接口元数据:         │──上报────────────►│ /dubbo/metadata/       │     │
│   │ {OrderService,    │                   │   my-provider/          │     │
│   │  UserService,     │                   │     abc123(revision)/   │     │
│   │  ProductService,  │                   │        {json元数据}     │     │
│   │  ...}             │                   │                         │     │
│   └───────────────────┘                   │ /dubbo/mapping/         │     │
│                                            │   OrderService → my-provider│
│   Provider-B (192.168.1.101)              │   UserService → my-provider │
│   ┌───────────────────┐                   └────────────────────────┘     │
│   │ application:      │──注册────────────►                              │
│   │   my-provider     │                                                  │
│   └───────────────────┘                                                  │
│                                                                            │
│   注册节点数 = 实例数 = 100(与接口数解耦!)                                │
└──────────────────────────────────────────────────────────────────────────┘

2.3 ZK 路径结构的根本变化

这是变化最直观的体现。Dubbo 3 在 ZK 上使用了三个独立的路径空间,各自承担不同的职责:

┌──────────────────────────────────────────────────────────────────────────┐
│                         ZK 节点结构对比                                    │
│                                                                          │
│  Dubbo 2(一套路径,接口粒度):                                            │
│                                                                          │
│    /dubbo                          ← 根路径(默认 "dubbo",可配 group)     │
│      └── /{interface}              ← 每个接口一个节点                      │
│            ├── /providers          ← Provider URL 列表(临时节点)         │
│            │     ├── dubbo%3A%2F%2F192.168.1.100%3A20880%2F...          │
│            │     └── dubbo%3A%2F%2F192.168.1.101%3A20880%2F...          │
│            ├── /consumers          ← Consumer URL 列表(临时节点)         │
│            ├── /routers            ← 路由规则                             │
│            └── /configurators      ← 动态配置                             │
│                                                                          │
│  Dubbo 3(三套路径,职责分离):                                            │
│                                                                          │
│    📍 /services                    ← 服务实例路径(应用级)                │
│      └── /{applicationName}        ← 应用名                               │
│            └── /{ip:port}          ← 实例地址(临时节点)                  │
│            └── /{ip:port}          ← value: ServiceInstance JSON         │
│                                                                          │
│    📍 /dubbo/mapping               ← 接口→应用映射路径                     │
│      └── /{serviceInterface}       ← 接口全限定名                         │
│            └── value = "provider-app1,provider-app2"  ← 逗号分隔应用名    │
│                                                                          │
│    📍 /dubbo/metadata              ← 元数据路径                           │
│      └── /{applicationName}        ← 应用名                               │
│            └── /{revision}         ← 元数据版本号(MD5)                   │
│                  └── value = "{...JSON...}"  ← 接口定义元数据              │
│                                                                          │
│    📍 /dubbo/{interface}/providers ← 兼容路径(仅 register-mode=all 时)   │
│      (与 Dubbo 2 完全相同,用于兼容老版本 Consumer)                       │
└──────────────────────────────────────────────────────────────────────────┘

2.4 数据量定量对比

场景 Dubbo 2(接口级) Dubbo 3(应用级) 削减比例
1 应用, 10 接口, 10 实例 100 节点 10 节点 90%
1 应用, 50 接口, 20 实例 1,000 节点 20 节点 98%
1 应用, 100 接口, 100 实例 10,000 节点 100 节点 99%
10 应用, 100 接口, 100 实例 100,000 节点 1,000 节点 99%

核心洞察:Dubbo 3 的注册节点数从 O(接口数 × 实例数) 降低到 O(实例数),与接口数完全解耦。这意味着无论你拆多少接口,ZK 的存储压力只随实例数量线性增长。


三、Provider 端:服务级注册全流程(源码级)

3.1 注册入口:从 ServiceConfig 开始

Dubbo 3 保持了与 Dubbo 2 相同的启动入口——ServiceConfig.export(),但底层的注册链路发生了本质变化。

// 源码: org.apache.dubbo.config.ServiceConfig
// 路径: dubbo-config/dubbo-config-api/src/main/java/org/apache/dubbo/config/ServiceConfig.java

public synchronized void export() {
    // ... 检查、构建 ServiceModel ...
    doExport();
}

protected synchronized void doExport() {
    // ... 初始化 ...
    doExportUrls();  // ← 核心
}

private void doExportUrls() {
    // 遍历所有注册中心 URL
    List<URL> registryURLs = ConfigValidationUtils.loadRegistries(this);
    for (ProtocolConfig protocolConfig : protocols) {
        // 构建服务 URL
        String pathKey = URL.buildKey(interfaceName, group, version);
        // 对每个协议(dubbo/rest/tri...),向每个注册中心注册
        doExportUrlsFor1Protocol(protocolConfig, registryURLs);
    }
}

doExportUrlsFor1Protocol 会走到 RegistryProtocol.export(),这里开始分岔——Dubbo 2 和 Dubbo 3 的路径不再相同。

3.2 双注册机制

Dubbo 3 默认的 register-mode=all,意味着提供者会同时做接口级注册应用级注册,确保 Dubbo 2 的 Consumer 也能发现新上线的 Dubbo 3 Provider。

┌──────────────────────────────────────────────────────────────────────────┐
│                        Provider 双注册策略                                │
│                                                                          │
│   register-mode = all (默认)                                              │
│   ┌─────────────────────────────────────────────────────────────────┐   │
│   │                                                                  │   │
│   │   Provider 启动                                                   │   │
│   │     │                                                            │   │
│   │     ├──► 接口级注册 (ZookeeperRegistry)                           │   │
│   │     │      │                                                      │   │
│   │     │      ├── 路径: /dubbo/{interface}/providers/               │   │
│   │     │      ├── 节点: dubbo://192.168.1.100:20880/{interface}     │   │
│   │     │      └── 兼容: Dubbo 2 Consumer 可以发现                   │   │
│   │     │                                                            │   │
│   │     └──► 应用级注册 (ServiceDiscoveryRegistry → ZookeeperServiceDiscovery) │
│   │            │                                                      │   │
│   │            ├── 路径: /services/{app}/192.168.1.100:20880         │   │
│   │            ├── 节点值: ServiceInstance JSON                       │   │
│   │            └── 发现: Dubbo 3 Consumer 可以发现                    │   │
│   │                                                                  │   │
│   └─────────────────────────────────────────────────────────────────┘   │
│                                                                          │
│   register-mode = instance (纯应用级, 不兼容 Dubbo 2)                     │
│   ┌─────────────────────────────────────────────────────────────────┐   │
│   │   只做应用级注册,ZK 节点数 = 实例数                               │   │
│   │   适用于全部 Consumer 已升级到 Dubbo 3 的场景                     │   │
│   └─────────────────────────────────────────────────────────────────┘   │
└──────────────────────────────────────────────────────────────────────────┘

配置方式

# 双注册(默认),兼容 Dubbo 2 Consumer
dubbo.application.register-mode=all

# 仅应用级注册,不兼容 Dubbo 2 Consumer
dubbo.application.register-mode=instance

# 仅接口级注册(降级为 Dubbo 2 行为)
dubbo.application.register-mode=interface

3.3 应用级注册的调用链

Provider 端应用级注册的完整调用链如下:

ServiceConfig.export()
  └── ServiceConfig.doExportUrls()
        └── RegistryProtocol.export()
              └── AbstractRegistryFactory.getRegistry() → 获取 ZookeeperRegistry(兼容)
              └── ServiceDiscoveryRegistry.register()     → 应用级注册
                    └── AbstractServiceDiscovery.register()
                          └── ZookeeperServiceDiscovery.doRegister()

ZookeeperServiceDiscovery.doRegister() 的核心实现:

// 源码: org.apache.dubbo.registry.zookeeper.ZookeeperServiceDiscovery
// 路径: dubbo-registry/dubbo-registry-zookeeper/
//       src/main/java/org/apache/dubbo/registry/zookeeper/ZookeeperServiceDiscovery.java

@Override
public void doRegister(ServiceInstance serviceInstance) {
    try {
        // 1. 构建 ZK 路径: /services/{appName}/{ip:port}
        String path = toPath(serviceInstance);

        // 2. 将 ServiceInstance 序列化为 JSON
        String content = serviceDefinition.getServiceDefinition(serviceInstance);

        // 3. 在 ZK 上创建临时节点
        //    第三个参数 true = ephemeral(临时节点,Session 过期自动删除)
        zkClient.create(path, content, true);
    } catch (Throwable e) {
        throw new RpcException("Failed to register service instance to zookeeper: "
                + serviceInstance, e);
    }
}

private String toPath(ServiceInstance serviceInstance) {
    // 路径格式: /{group}/{appName}/{host}:{port}
    // 例如: /services/my-provider/192.168.1.100:20880
    return buildPath(serviceInstance);
}

对比 Dubbo 2 的接口级注册路径:

// Dubbo 2: ZookeeperRegistry.doRegister()
// 路径: /dubbo/{interface}/providers/dubbo%3A%2F%2F...
@Override
public void doRegister(URL url) {
    zkClient.create(
        toUrlPath(url),                            // /dubbo/{interface}/providers/{encodedUrl}
        url.getParameter(DYNAMIC_KEY, true),       // 是否临时节点
        true                                       // 是否创建父节点
    );
}

关键区别

  • Dubbo 2:路径包含接口名,一个实例为每个接口各建一个节点
  • Dubbo 3:路径只包含应用名 + IP:Port,一个实例不管暴露多少接口,只建一个节点

3.4 ServiceInstance 的数据结构

应用级注册时,ZooKeeper 临时节点存储的不是一个简单的 URL 字符串,而是一个完整的 ServiceInstance JSON 对象:

{
  "id": "192.168.1.100:20880",
  "serviceName": "my-provider",
  "host": "192.168.1.100",
  "port": 20880,
  "healthy": true,
  "enabled": true,
  "metadata": {
    "dubbo.endpoints": "[{\"port\":20880,\"protocol\":\"dubbo\"},{\"port\":50051,\"protocol\":\"tri\"}]",
    "dubbo.metadata-service.url-params": "{\"version\":\"1.0.0\",\"dubbo\":\"2.0.2\",\"release\":\"3.2.0\",\"port\":20880}",
    "dubbo.metadata.revision": "da3be833baa2088c5f6776fb7ab1a436",
    "timestamp": "1721030400000"
  }
}

重点字段说明

  • serviceName:应用名,Consumer 发现的关键
  • metadata["dubbo.metadata.revision"]:元数据版本号,Consumer 用它去元数据中心查接口定义
  • metadata["dubbo.endpoints"]:该实例暴露的协议端口(可能同时暴露 dubbo:20880 和 tri:50051)

3.5 元数据上报全流程

应用级注册解决的是”哪个应用在哪个机器上”的问题。但 Consumer 还需要知道 “这个应用提供了哪些接口?每个接口有哪些方法?超时多少?用哪个序列化协议?” ——这就是元数据的职责。

3.5.1 MetadataInfo 构建与 Revision 计算

// 源码: org.apache.dubbo.metadata.MetadataInfo
// 路径: dubbo-common/src/main/java/org/apache/dubbo/metadata/MetadataInfo.java

public class MetadataInfo {
    private String app;              // 应用名
    private String revision;         // 元数据版本号(MD5)
    private Map<String, ServiceInfo> services;  // key: serviceKey, value: 接口信息

    // ServiceInfo: 一个接口的元数据
    public static class ServiceInfo {
        private String name;         // 接口全限定名
        private String protocol;     // dubbo / tri / rest
        private String path;         // 服务路径
        private Map<String, String> params;  // timeout, retries, methods, ...
    }
}

Revision 的计算逻辑

// revision = MD5(接口名 + 方法签名 + 参数配置)
// 相同的接口定义 → 相同的 revision → 多个实例共享同一份元数据
String revision = MD5Utils.md5(
    serviceDefinitions.stream()
        .map(ServiceDefinition::toCanonicalString)
        .sorted()
        .collect(Collectors.joining())
);

Revision 的妙处在于:同一应用的不同实例,只要接口定义一样(代码版本相同),revision 就相同,它们共享同一份元数据。这意味着:

  • 100 个实例 → 不需要 100 份元数据
  • ZK 元数据路径下只有一份,revision 作为节点名
  • 实例重启但代码不变 → revision 不变 → 不需要重新上报元数据
  • 上线新版本代码 → revision 变化 → 写入新版本元数据

3.5.2 Remote 模式:元数据上报到 ZK

# 配置 remote 模式
dubbo.application.metadata-type=remote
dubbo.metadata-report.address=zookeeper://127.0.0.1:2181

上报过程:

// 源码: org.apache.dubbo.metadata.report.support.AbstractMetadataReport
// 路径: dubbo-metadata-report/dubbo-metadata-report-api/

@Override
public void publishProvider(MetadataInfo metadataInfo) {
    // 1. 构建 ZK 路径: /dubbo/metadata/{app}/{revision}
    String path = buildMetadataPath(metadataInfo.getApp(), metadataInfo.getRevision());

    // 2. 检查 revision 是否已存在(幂等上报)
    //    多个实例相同 revision 时只上报一次
    if (metadataCenter.exist(path)) {
        return;  // 已有相同版本元数据,无需重复上报
    }

    // 3. 将 MetadataInfo 序列化为 JSON 写入 ZK
    //    value = {"app":"my-provider","revision":"abc123","services":{...}}
    metadataCenter.put(path, metadataInfo.toJsonString());
}

ZK 上元数据节点的实际内容:

{
    "app": "my-provider",
    "revision": "da3be833baa2088c5f6776fb7ab1a436",
    "services": {
        "com.example.OrderService:dubbo": {
            "name": "com.example.OrderService",
            "protocol": "dubbo",
            "path": "com.example.OrderService",
            "params": {
                "side": "provider",
                "interface": "com.example.OrderService",
                "group": "demo",
                "version": "1.0.0",
                "methods": "getOrder,createOrder,cancelOrder",
                "timeout": "3000",
                "retries": "2",
                "loadbalance": "random",
                "serialization": "hessian2"
            }
        },
        "com.example.UserService:dubbo": {
            "name": "com.example.UserService",
            "protocol": "dubbo",
            "path": "com.example.UserService",
            "params": {
                "methods": "getUser,updateUser,deleteUser",
                "timeout": "5000",
                "retries": "1"
            }
        }
    }
}

3.5.3 Local 模式:Provider 暴露 MetadataService

Local 模式(默认)不把元数据上报到 ZK,而是 Provider 自身启动一个 MetadataService 的 Dubbo 服务,Consumer 直接远程调用它来获取接口元数据。

┌──────────────────────────────────────────────────────────────────────────┐
│                        Local 模式的元数据获取                              │
│                                                                          │
│   Consumer                                   Provider                    │
│   ┌─────────────────┐                    ┌──────────────────┐           │
│   │ 需要 OrderService│                    │ 已注册:            │           │
│   │ 的接口定义元数据  │                    │ /services/my-app/ │           │
│   │                 │                    │   192.168.1.100   │           │
│   │                 │                    │                   │           │
│   │  RPC 调用 ────────────►             │ MetadataService  │           │
│   │                 │                    │ 端口: 20880       │           │
│   │                 │                    │                   │           │
│   │                 │   ◄────────────    │ getExportedURLs()│           │
│   │ 返回: [{        │                    │ getMetadataInfo() │           │
│   │  interface:     │                    │ getServiceDef...()│           │
│   │  "OrderService",│                    └──────────────────┘           │
│   │  methods: "...",│                                                   │
│   │  timeout: 3000  │                                                   │
│   │ }]              │                                                   │
│   └─────────────────┘                                                   │
│                                                                          │
│   本质:MetadataService 也是一个 Dubbo 服务                                │
│   接口: org.apache.dubbo.metadata.MetadataService                        │
│   协议: dubbo://{provider-ip}:20880/org.apache.dubbo.metadata.MetadataService │
└──────────────────────────────────────────────────────────────────────────┘

两种模式对比

维度 Remote 模式 Local 模式(默认)
元数据存储 ZK / Nacos / Redis Provider 内存
获取方式 Consumer 读元数据中心 Consumer RPC 调 Provider
网络开销 Consumer ↔ 元数据中心 Consumer → Provider
ZK 存储 有 metadata 节点 无 metadata 节点
Provider 存活依赖 无(元数据中心如果高可用) 必须在 Provider 在线时获取
适用场景 大规模、跨集群 中小规模、默认推荐

四、Consumer 端:发现 Provider Facade 的完整过程(源码级)

这是 Dubbo 3 最核心的变化,也是本文的重头戏。Consumer 只知道接口名(如 com.example.OrderService),它如何一步步找到提供这个接口的 Provider 实例并生成可调用的 Invoker?

4.1 发现入口:从 ReferenceConfig 开始

// 源码: org.apache.dubbo.config.ReferenceConfig
// 路径: dubbo-config/dubbo-config-api/src/main/java/org/apache/dubbo/config/ReferenceConfig.java

public synchronized T get() {
    // ... 初始化 ...
    return createProxy(refProtocol.refer(interfaceClass, urls));
}

RegistryProtocol.refer() 是 Consumer 发现的关键入口:

// 源码: org.apache.dubbo.registry.integration.RegistryProtocol
@Override
public <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException {
    // 1. 获取注册中心实例
    Registry registry = registryFactory.getRegistry(url);

    // 2. 创建 Directory(Dubbo 3 中是 ServiceDiscoveryRegistryDirectory)
    RegistryDirectory<T> directory = new ServiceDiscoveryRegistryDirectory<>(type, url);
    directory.setRegistry(registry);
    directory.setProtocol(protocol);
    directory.buildRouterChain(subscribeUrl);

    // 3. 订阅 —— 这是核心
    directory.subscribe(subscribeUrl);

    // 4. 将 Directory 中的所有 Invoker 聚合成一个 Cluster Invoker
    return cluster.join(directory, url);
}

4.2 subscribeURLs:三步发现法的核心

Dubbo 2 使用的是 RegistryDirectory,Dubbo 3 使用的是 ServiceDiscoveryRegistryDirectory(继承自 DynamicDirectory),它通过 ServiceDiscoveryRegistry 来订阅。

Consumer 发现的核心调用链

ReferenceConfig.get()
  └── RegistryProtocol.refer()
        └── ServiceDiscoveryRegistryDirectory.subscribe()
              └── ServiceDiscoveryRegistry.doSubscribe()
                    └── subscribeURLs()  ← ★ 核心方法
                          │
                          ├── Step 1: 查 Mapping(接口 → 应用)
                          ├── Step 2: 查 Instance(应用 → IP:Port)
                          └── Step 3: 查 Metadata(接口元数据)
// 源码: org.apache.dubbo.registry.client.ServiceDiscoveryRegistry
//       (subscribeURLs 方法的简化逻辑)

private void subscribeURLs(URL url, NotifyListener listener,
                           Set<String> serviceNames, List<URL> subscribedURLs) {

    // ============ Step 1: 接口 → 应用名映射 ============
    // 拿到接口全限定名
    String serviceInterface = url.getServiceInterface();
    // 例如: "com.example.OrderService"

    // 从 ZK /dubbo/mapping/ 或缓存中获取该接口对应的应用名
    Set<String> appNames = getMappingListener().getServiceAppMapping(serviceInterface);
    // 返回: {"my-provider", "another-provider"}

    // ============ Step 2: 根据应用名获取实例列表 ============
    for (String appName : appNames) {
        // 从 ServiceDiscovery 获取该应用的所有健康实例
        // ZK 路径: /services/{appName}/
        List<ServiceInstance> instances = serviceDiscovery.getInstances(appName);
        // 返回: [{host:"192.168.1.100", port:20880},
        //        {host:"192.168.1.101", port:20880}]

        for (ServiceInstance instance : instances) {
            // ============ Step 3: 获取接口元数据 ============
            // 从 instance 的 metadata 中获取 revision
            String revision = instance.getMetadata()
                .get("dubbo.metadata.revision");

            // 根据 revision 从元数据中心获取接口定义
            MetadataInfo metadataInfo = metadataService
                .getMetadataInfo(appName, revision);

            // 从 MetadataInfo 中提取该接口的完整 dubbo URL
            List<URL> providerURLs = metadataInfo.toURLs(instance, serviceInterface);
            // 输出: dubbo://192.168.1.100:20880/com.example.OrderService?
            //          timeout=3000&retries=2&methods=getOrder,createOrder&...

            subscribedURLs.addAll(providerURLs);
        }
    }

    // 通知 listener,刷新 Invoker 列表
    listener.notify(subscribedURLs);
}

4.3 三步发现法详解

下面将这”三步”展开,每一层都深入到 ZK 操作和源码细节。

4.3.1 Step 1:查 Mapping —— 接口名 → 应用名

这是 Dubbo 3 引入的全新概念。ZK 上的映射路径是 /dubbo/mapping/{interface}

┌──────────────────────────────────────────────────────────────────────────┐
│                     Step 1: 查 Mapping                                    │
│                                                                          │
│   Consumer 持有:                                                          │
│     interface = "com.example.OrderService"                               │
│                                                                          │
│   查询 ZK:                                                                │
│     GET /dubbo/mapping/com.example.OrderService                          │
│                                                                          │
│   返回值:                                                                 │
│     "my-provider,second-provider"                                        │
│                                                                          │
│   含义:                                                                   │
│     OrderService 这个接口由 "my-provider" 和 "second-provider" 两个应用提供 │
│                                                                          │
│   ┌─────────────────────────────────────────────────────────────────┐   │
│   │ 为什么一个接口可能对应多个应用?                                   │   │
│   │                                                                  │   │
│   │ - 多团队各自部署:teamA 的 my-provider 和 teamB 的 second-provider │   │
│   │   都实现了 OrderService                                         │   │
│   │ - 灰度/AB 场景:同一个接口在新旧两个应用中都有                      │   │
│   │ - 应用合并/拆分:拆分时两个应用临时共存                            │   │
│   └─────────────────────────────────────────────────────────────────┘   │
└──────────────────────────────────────────────────────────────────────────┘

Mapping 的注册逻辑(Provider 端在应用级注册时同步写入):

// 源码: org.apache.dubbo.registry.client.mapping.DefaultServiceAppMapping
// Provider 端在 doRegister 后调用:

public void addMapping(String serviceInterface, String appName) {
    // ZK 路径: /dubbo/mapping/{serviceInterface}
    String mappingPath = buildMappingPath(serviceInterface);

    // 读取当前映射值
    String currentValue = zkClient.getContent(mappingPath);
    // currentValue = "existing-provider,another-provider"

    // 合并(去重)
    Set<String> appNames = new LinkedHashSet<>(
        Arrays.asList(currentValue.split(","))
    );
    appNames.add(appName);

    // 写回 ZK(用 version 做 CAS 乐观锁,最多重试 6 次)
    // 使用 ZK 的 setData() 带 version 参数,避免并发覆盖
    String newValue = String.join(",", appNames);
    zkClient.setData(mappingPath, newValue);  // "existing-provider,...,my-provider"
}

Mapping 的缓存与监听

// MappingListener 负责监听 mapping 变更
public class MappingListener {
    // 内存缓存: interface → [appName1, appName2, ...]
    private Map<String, Set<String>> mappingCache = new ConcurrentHashMap<>();

    public Set<String> getServiceAppMapping(String serviceInterface) {
        // 1. 先查内存缓存
        Set<String> cached = mappingCache.get(serviceInterface);
        if (cached != null && !cached.isEmpty()) {
            return cached;
        }

        // 2. 缓存没有,去 ZK 查
        String path = "/dubbo/mapping/" + serviceInterface;
        String content = zkClient.getContent(path);
        Set<String> appNames = parseAppNames(content);

        // 3. 写入缓存并注册 Watcher
        mappingCache.put(serviceInterface, appNames);
        //    当 mapping 数据变更时,Watcher 回调会清除缓存,
        //    触发重新发现 → 重新走 Step 1~3
        addMappingListener(serviceInterface); // 注册 Watcher

        return appNames;
    }
}

4.3.2 Step 2:查 Instance —— 应用名 → IP:Port 列表

拿到应用名后,Consumer 去 /services/{appName} 下查所有实例:

┌──────────────────────────────────────────────────────────────────────────┐
│                     Step 2: 查 Instance                                   │
│                                                                          │
│   Step 1 返回: appName = "my-provider"                                   │
│                                                                          │
│   查询 ZK:                                                                │
│     ls /services/my-provider                                             │
│                                                                          │
│   返回子节点:                                                              │
│     [192.168.1.100:20880]                                                │
│     [192.168.1.101:20880]                                                │
│     [192.168.1.102:20880]                                                │
│                                                                          │
│   每个节点的值为 ServiceInstance JSON:                                     │
│     {                                                                    │
│       "host": "192.168.1.100",                                           │
│       "port": 20880,                                                     │
│       "metadata": {                                                      │
│         "dubbo.metadata.revision": "da3be833...",                        │
│         "dubbo.endpoints": "[{\"port\":20880,\"protocol\":\"dubbo\"}]",  │
│         "dubbo.metadata-service.url-params": "{...}"                     │
│       }                                                                  │
│     }                                                                    │
└──────────────────────────────────────────────────────────────────────────┘

实例变更监听

Consumer 不会只查一次。它会注册一个 ServiceDiscoveryListener,监听 /services/my-provider/ 的子节点变化:

// 伪代码示意
serviceDiscovery.addServiceInstancesChangedListener(appName, event -> {
    // 实例上线/下线/修改时触发
    switch (event.getType()) {
        case ADDED:      // 新实例上线 → 构建新 Invoker
        case REMOVED:    // 实例下线 → 移除 Invoker
        case MODIFIED:   // 实例元数据变化 → 重建 Invoker
    }
    // 重新走 Step 3,刷新 Invoker 列表
    refreshInvokerList();
});

关键设计:Consumer 不需要为每个接口注册 Watcher。它只需要监听一个路径 /services/{appName}。无论该应用暴露了多少接口,实例上下线时 Consumer 只收到一次通知,然后一次性刷新所有接口的 Invoker。

4.3.3 Step 3:查 Metadata —— 接口元数据获取

这是”Consumer 如何发现 Provider 的 Facade”的核心。有了 IP:Port,Consumer 还需要知道:

  • 用哪个协议连接?(dubbo / tri / rest)
  • 有哪些方法可以调用?(方法签名、参数类型、返回值类型)
  • 超时多少?重试几次?用哪个序列化协议?

元数据获取有两条路径,取决于 Provider 的 metadata-type 配置:

┌──────────────────────────────────────────────────────────────────────────┐
│                     Step 3: 查 Metadata                                   │
│                                                                          │
│  ┌─── Remote 模式 ───────────────────────────────────────────────────┐  │
│  │                                                                     │  │
│  │  Consumer                                     ZK / 元数据中心       │  │
│  │  ┌──────────────┐                         ┌──────────────────┐    │  │
│  │  │ 拿到 appName │                         │ /dubbo/metadata/  │    │  │
│  │  │ 和 revision   │── GET metadata ──────►│   my-provider/    │    │  │
│  │  │              │                         │     abc123/       │    │  │
│  │  │              │  ◄──── JSON ────────────│   { JSON 元数据 }  │    │  │
│  │  └──────────────┘                         └──────────────────┘    │  │
│  │                                                                     │  │
│  │  特点: 不依赖 Provider 存活,元数据中心独立存储                       │  │
│  │  适合: 大规模集群,跨机房场景                                        │  │
│  └─────────────────────────────────────────────────────────────────────┘  │
│                                                                          │
│  ┌─── Local 模式(默认)────────────────────────────────────────────────┐  │
│  │                                                                     │  │
│  │  Consumer                                     Provider              │  │
│  │  ┌──────────────┐                         ┌──────────────────┐    │  │
│  │  │ 拿到 IP:Port │                         │ MetadataService  │    │  │
│  │  │ 192.168.1.100│── RPC 调用 ──────────►│ 端口: 20880       │    │  │
│  │  │   :20880     │                         │                  │    │  │
│  │  │              │  ◄── MetadataInfo ─────│ getExportedURLs()│    │  │
│  │  │              │    {services: {...}}    │ getMetadataInfo() │    │  │
│  │  └──────────────┘                         └──────────────────┘    │  │
│  │                                                                     │  │
│  │  特点: 元数据不占 ZK,不占外部存储,通过 RPC 实时获取                │  │
│  │  注意: Provider 必须在线才能获取,Provider 下线后新 Consumer 无法获取│  │
│  └─────────────────────────────────────────────────────────────────────┘  │
└──────────────────────────────────────────────────────────────────────────┘

Local 模式下的实际 RPC 调用

Consumer 在拿到 Provider 实例的 IP:Port 后,会构建一个指向 MetadataService 的 Dubbo Invoker,发起一次真实的 RPC 调用来获取元数据:

// Consumer 端获取元数据的源码逻辑(简化)

// 1. 构建 MetadataService 的 URL
URL metadataServiceURL = URL.valueOf(
    "dubbo://" + instance.getHost() + ":" + instance.getPort() +
    "/org.apache.dubbo.metadata.MetadataService" +
    "?application=" + appName +
    "&version=1.0.0" +
    "&group=" + appName   // group 默认等于 appName
);

// 2. 创建针对 MetadataService 的 Invoker
Invoker<MetadataService> metadataInvoker = protocol.refer(
    MetadataService.class, metadataServiceURL
);

// 3. 发起 RPC 调用获取元数据
MetadataService metadataService = proxyFactory.getProxy(metadataInvoker);

// 4. 调用方法获取接口级别的元数据
// 获取所有暴露服务的 URL 列表
SortedSet<String> exportedURLs = metadataService.getExportedURLs(
    "com.example.OrderService",  // 目标接口
    "demo",                       // group
    "1.0.0",                      // version
    "dubbo"                       // protocol
);

// 返回:
// [
//   "dubbo://192.168.1.100:20880/com.example.OrderService?
//     timeout=3000&retries=2&methods=getOrder,createOrder&serialization=hessian2&...",
//   "dubbo://192.168.1.100:50051/com.example.OrderService?
//     timeout=3000&methods=getOrder,createOrder&protocol=tri&..."
// ]

元数据缓存机制

Consumer 不会每次调用都去查元数据。它有一层缓存:

// 内存缓存
Map<String, MetadataInfo> metadataCache = new ConcurrentHashMap<>();
// key = "appName:revision"
// value = MetadataInfo

// 文件缓存(进程重启后可用)
// ~/.dubbo/dubbo-metadata-{appName}.cache

4.4 MappingListener 的监听改变触发全链路刷新

Dubbo 3 中 Consumer 注册了三种监听器,它们之间形成了级联刷新

┌──────────────────────────────────────────────────────────────────────────┐
│                    Consumer 端的三层监听机制                               │
│                                                                          │
│  /dubbo/mapping/{interface}       /services/{app}       元数据(revision)  │
│  ┌──────────────────────┐  ┌──────────────────────┐  ┌────────────────┐  │
│  │ MappingListener      │  │ ServiceInstances     │  │ 元数据 revision │  │
│  │                      │  │ ChangedListener      │  │ 变化监听        │  │
│  │ 变更通知:             │  │                      │  │                │  │
│  │ "应用B也提供这个接口"  │  │ 变更通知:             │  │ 变更通知:       │  │
│  │                      │  │ "新实例上线/下线"      │  │ "接口定义变化"   │  │
│  └──────────┬───────────┘  └──────────┬───────────┘  └───────┬────────┘  │
│             │                         │                      │           │
│             ▼                         ▼                      ▼           │
│  ┌──────────────────────────────────────────────────────────────────┐   │
│  │                     触发全链路刷新                                │   │
│  │                                                                  │   │
│  │  1. 清缓存 → 2. 重新 Mapping → 3. 重新 Instance → 4. 重新 Metadata│   │
│  │  5. 重建 Invoker → 6. 重建 Cluster → 7. 重建 LoadBalance         │   │
│  └──────────────────────────────────────────────────────────────────┘   │
└──────────────────────────────────────────────────────────────────────────┘

对比 Dubbo 2:

维度 Dubbo 2(接口级) Dubbo 3(应用级)
监听路径 /dubbo/{interface}/providers(每个接口一个) /services/{app}(每个应用一个)
监听次数 接口数 × Consumer实例数 应用数 × Consumer实例数
事件粒度 某个接口的 Provider 变更 某个应用的实例变更
Watcher 开销

4.5 Consumer 端 Invoker 构建全链路

把三步骤串起来,完整的 Consumer Invoker 构建过程如下:

┌──────────────────────────────────────────────────────────────────────────┐
│               Consumer 端 Invoker 构建完整时序                             │
│                                                                          │
│  ReferenceConfig               ZK                    Provider            │
│  ───────┬───────                ──                    ────────            │
│         │                                                                 │
│  ① get()                                                                  │
│         │                                                                 │
│         ├──► Step 1: 查 /dubbo/mapping/{interface} ──► ZK                │
│         │         返回: ["my-provider"]                                   │
│         │                                                                 │
│         ├──► Step 2: 查 /services/my-provider ────────► ZK                │
│         │         返回: [192.168.1.100:20880,                             │
│         │                 192.168.1.101:20880]                             │
│         │                                                                 │
│         ├──► Step 3a (remote): GET /dubbo/metadata/                      │
│         │     my-provider/abc123 ──────────────────────► ZK               │
│         │         返回: {services:{OrderService:{...}}}                   │
│         │                                                                 │
│         ├──► Step 3b (local): RPC 调 MetadataService ───► 192.168.1.100  │
│         │         MetadataService.getExportedURLs(...)                    │
│         │         返回: 接口的完整 dubbo URL 列表                          │
│         │                                                                 │
│         ├──► 拼装 Provider URL:                                          │
│         │     dubbo://192.168.1.100:20880/OrderService?                  │
│         │       timeout=3000&retries=2&methods=getOrder&...              │
│         │                                                                 │
│         ├──► 创建 Invoker:                                                │
│         │     DubboInvoker{url=dubbo://192.168.1.100:20880/...}         │
│         │     DubboInvoker{url=dubbo://192.168.1.101:20880/...}         │
│         │                                                                 │
│         ├──► 注册 Watcher:                                               │
│         │     watch /services/my-provider (实例变更)                      │
│         │     watch /dubbo/mapping/OrderService (映射变更)                │
│         │                                                                 │
│         └──► 返回 Cluster Invoker:                                       │
│               FailoverClusterInvoker{                                     │
│                 directory=ServiceDiscoveryRegistryDirectory{              │
│                   invokers=[DubboInvoker@100, DubboInvoker@101]           │
│                 },                                                        │
│                 loadbalance=RandomLoadBalance{}                            │
│               }                                                           │
│                                                                            │
│  ② Consumer 调用 userService.getOrder(123)                                │
│         │                                                                 │
│         ├──► ClusterInvoker.invoke()                                     │
│         ├──► LoadBalance.select() → 192.168.1.101                        │
│         └──► DubboInvoker.doInvoke() ────────────────► Provider          │
└──────────────────────────────────────────────────────────────────────────┘

五、平滑迁移:三种订阅规则

从 Dubbo 2 到 Dubbo 3 不是一夜之间的事。Dubbo 3 通过三种订阅规则动态迁移机制,支持灰度切换。

5.1 三种迁移状态

# 规则一:仅接口级订阅(降级回 Dubbo 2 模式)
dubbo.application.service-discovery.migration=FORCE_INTERFACE

# 规则二:双订阅,应用级优先(默认)
dubbo.application.service-discovery.migration=APPLICATION_FIRST

# 规则三:仅应用级订阅(纯 Dubbo 3 模式)
dubbo.application.service-discovery.migration=FORCE_APPLICATION
┌──────────────────────────────────────────────────────────────────────────┐
│                    三种订阅规则的行为差异                                   │
│                                                                          │
│  FORCE_INTERFACE(退回到 Dubbo 2):                                       │
│  ┌──────────────────────────────────────────────────────────────────┐   │
│  │  Consumer 只走接口级订阅:                                         │   │
│  │    subscribe → /dubbo/{interface}/providers                       │   │
│  │                                                                    │   │
│  │  使用: RegistryDirectory (Dubbo 2 行为)                           │   │
│  │  前提: Provider 必须以 register-mode=all 或 interface 注册        │   │
│  │  场景: 线上出问题需要紧急回滚,或老版本 Consumer 必须调用          │   │
│  └──────────────────────────────────────────────────────────────────┘   │
│                                                                          │
│  APPLICATION_FIRST(默认,双保险):                                        │
│  ┌──────────────────────────────────────────────────────────────────┐   │
│  │  Consumer 同时走两套订阅:                                          │   │
│  │    ┌─ 应用级订阅 → /services/{app}                                │   │
│  │    │   └─ 优先使用,性能更好                                       │   │
│  │    └─ 接口级订阅 → /dubbo/{interface}/providers                   │   │
│  │        └─ 兜底,应用级订阅失败时切过来                             │   │
│  │                                                                    │   │
│  │  两个 ServiceDiscoveryRegistryDirectory 共存                      │   │
│  │  场景: 迁移过渡期,保证不丢流量                                    │   │
│  └──────────────────────────────────────────────────────────────────┘   │
│                                                                          │
│  FORCE_APPLICATION(纯 Dubbo 3):                                         │
│  ┌──────────────────────────────────────────────────────────────────┐   │
│  │  Consumer 只走应用级订阅:                                          │   │
│  │    subscribe → /services/{app}                                    │   │
│  │                                                                    │   │
│  │  ZK 路径完全使用三路径模型                                         │   │
│  │  场景: 全部 Consumer/Provider 升级完成,可直接切换                 │   │
│  └──────────────────────────────────────────────────────────────────┘   │
└──────────────────────────────────────────────────────────────────────────┘

5.2 动态切换与灰度迁移

迁移规则支持动态下发,可以在运行时热切换,无需重启 Consumer:

// 源码: org.apache.dubbo.registry.client.migration.MigrationRuleHandler
// 核心逻辑

// 1. 注册 MigrationRuleListener
// 2. 当规则变更时(比如从配置中心收到新规则),触发:
public void onRuleChanged(MigrationRule newRule) {
    switch (newRule.getStep()) {
        case FORCE_INTERFACE:
            // 切换到接口级 Directory
            // → 废弃应用级 Directory 的 Invoker
            // → 只使用 RegistryDirectory 的 Invoker
            break;
        case APPLICATION_FIRST:
            // 维持双 Directory
            // → 应用级 Directory 作为首选
            // → 接口级 Directory 作为灾备
            break;
        case FORCE_APPLICATION:
            // 切换到应用级 Directory
            // → 废弃接口级 Directory 的 Invoker
            // → 只使用 ServiceDiscoveryRegistryDirectory 的 Invoker
            break;
    }
    // 刷新 Invoker 列表
    refreshServiceDiscoveryInvoker();
}

推荐的灰度迁移路径

┌──────────────────────────────────────────────────────────────────────────┐
│                      灰度迁移四阶段                                       │
│                                                                          │
│  Phase 1: Provider 升级                                                   │
│  ┌──────────────────────────────────────────────────────────────────┐   │
│  │  dubbo.application.register-mode=all                               │   │
│  │  → Provider 双注册,兼容 Dubbo 2 和 Dubbo 3 Consumer              │   │
│  │  → Consumer 不做任何变更                                          │   │
│  │  → 验证: ZK 上同时有 /dubbo/{interface}/providers                 │   │
│  │           和 /services/{app}/{ip:port}                              │   │
│  └──────────────────────────────────────────────────────────────────┘   │
│         │                                                                 │
│         ▼                                                                 │
│  Phase 2: Consumer 开启双订阅                                              │
│  ┌──────────────────────────────────────────────────────────────────┐   │
│  │  dubbo.application.service-discovery.migration=APPLICATION_FIRST   │   │
│  │  → Consumer 优先走应用级发现,失败时 fallback 到接口级             │   │
│  │  → 验证: Consumer 日志中出现 "subscribe service: {app}"          │   │
│  │  → 监控: 应用级发现成功率 > 99.9%                                 │   │
│  └──────────────────────────────────────────────────────────────────┘   │
│         │                                                                 │
│         ▼                                                                 │
│  Phase 3: Consumer 只走应用级                                              │
│  ┌──────────────────────────────────────────────────────────────────┐   │
│  │  dubbo.application.service-discovery.migration=FORCE_APPLICATION   │   │
│  │  → Consumer 仅走应用级订阅                                        │   │
│  │  → 验证: /dubbo/{interface}/providers 的 Watcher 不再新增         │   │
│  │  → 监控: RPC 成功率无变化                                         │   │
│  └──────────────────────────────────────────────────────────────────┘   │
│         │                                                                 │
│         ▼                                                                 │
│  Phase 4: Provider 关闭接口级注册                                          │
│  ┌──────────────────────────────────────────────────────────────────┐   │
│  │  dubbo.application.register-mode=instance                          │   │
│  │  → ZK 上彻底没有 /dubbo/{interface}/providers 路径了              │   │
│  │  → ZK 节点数量大幅下降                                            │   │
│  │  → Dubbo 2 Consumer 无法再发现此 Provider(已全部下线)            │   │
│  └──────────────────────────────────────────────────────────────────┘   │
└──────────────────────────────────────────────────────────────────────────┘

六、核心类图与调用链

6.1 Provider 端核心类关系

┌──────────────────────────────────────────────────────────────────────────┐
│                        Provider 端核心类关系                               │
│                                                                          │
│  ServiceConfig                                                           │
│    │                                                                      │
│    ├──► RegistryProtocol.export()                                       │
│    │      │                                                               │
│    │      ├──► [接口级注册]                                               │
│    │      │      AbstractRegistryFactory.getRegistry()                   │
│    │      │        └── ZookeeperRegistry                                │
│    │      │              └── FailbackRegistry                           │
│    │      │                    └── AbstractRegistry                      │
│    │      │                                                               │
│    │      └──► [应用级注册]                                               │
│    │             ServiceDiscoveryRegistry.register()                    │
│    │               └── AbstractServiceDiscovery                         │
│    │                     └── ZookeeperServiceDiscovery                   │
│    │                           └── ServiceDiscovery (SPI接口)            │
│    │                                                                      │
│    ├──► [元数据上报]                                                      │
│    │      MetadataReportFactory.getMetadataReport()                      │
│    │        └── ZookeeperMetadataReport                                  │
│    │              └── AbstractMetadataReport                             │
│    │                                                                      │
│    └──► [Mapping 注册]                                                   │
│           ServiceAppMapping.addMapping(interface, appName)               │
│             └── ZK: /dubbo/mapping/{interface} = "app1,app2"            │
│                                                                          │
│  核心 SPI 接口:                                                           │
│  ┌────────────────────────────────────────────────────────────────────┐  │
│  │  ServiceDiscovery (service-discovery)                               │  │
│  │    ├── ZookeeperServiceDiscovery                                    │  │
│  │    ├── NacosServiceDiscovery                                        │  │
│  │    └── ConsulServiceDiscovery                                       │  │
│  │                                                                      │  │
│  │  MetadataReport (metadata-report)                                   │  │
│  │    ├── ZookeeperMetadataReport                                      │  │
│  │    ├── NacosMetadataReport                                          │  │
│  │    └── RedisMetadataReport                                          │  │
│  │                                                                      │  │
│  │  ServiceAppMapping (service-discovery)                              │  │
│  │    ├── ZookeeperServiceAppMapping                                   │  │
│  │    └── NacosServiceAppMapping                                       │  │
│  └────────────────────────────────────────────────────────────────────┘  │
└──────────────────────────────────────────────────────────────────────────┘

6.2 Consumer 端核心类关系

┌──────────────────────────────────────────────────────────────────────────┐
│                        Consumer 端核心类关系                               │
│                                                                          │
│  ReferenceConfig                                                         │
│    │                                                                      │
│    └──► RegistryProtocol.refer()                                        │
│           │                                                               │
│           └──► ServiceDiscoveryRegistryDirectory.subscribe()            │
│                  │                                                        │
│                  └──► ServiceDiscoveryRegistry.doSubscribe()             │
│                         │                                                 │
│                         └──► subscribeURLs()                             │
│                                │                                          │
│                                ├──► MappingListener                      │
│                                │      .getServiceAppMapping(interface)    │
│                                │      返回: [appName1, appName2]          │
│                                │                                          │
│                                ├──► ServiceDiscovery                     │
│                                │      .getInstances(appName)              │
│                                │      返回: List<ServiceInstance>         │
│                                │      注册 Watcher on /services/{app}    │
│                                │                                          │
│                                └──► MetadataService (两种途径)            │
│                                       ├── remote: 读 ZK /dubbo/metadata/ │
│                                       └── local:  RPC 调 Provider 的     │
│                                            MetadataService               │
│                                                                          │
│  最终产出:                                                                │
│  ┌────────────────────────────────────────────────────────────────────┐  │
│  │  ClusterInvoker                                                      │  │
│  │    ├── Directory (Invoker 列表)                                      │  │
│  │    │     ├── DubboInvoker{url=dubbo://192.168.1.100:20880/...}      │  │
│  │    │     ├── DubboInvoker{url=dubbo://192.168.1.101:20880/...}      │  │
│  │    │     └── DubboInvoker{url=dubbo://192.168.1.102:20880/...}      │  │
│  │    ├── Router (路由链)                                               │  │
│  │    └── LoadBalance (负载均衡: random/roundrobin/...)                 │  │
│  └────────────────────────────────────────────────────────────────────┘  │
└──────────────────────────────────────────────────────────────────────────┘

6.3 完整调用链时序图

┌──────────────────────────────────────────────────────────────────────────┐
│            Dubbo 3 应用级服务注册与发现:完整调用链时序图                      │
│                                                                          │
│  Provider-1                ZK                     Consumer               │
│  ─────┬─────               ──                     ────┬───               │
│       │                                               │                  │
│  ═══ Provider 启动 ═══                                │                  │
│       │                                               │                  │
│   ① export()                                          │                  │
│       │                                               │                  │
│       ├─ 注册实例 ────────────►                       │                  │
│       │  /services/my-app/                            │                  │
│       │  192.168.1.100:20880                          │                  │
│       │  (临时节点)            │                       │                  │
│       │  ◄─── OK ─────────────                       │                  │
│       │                                               │                  │
│       ├─ 注册 Mapping ───────►                       │                  │
│       │  /dubbo/mapping/                              │                  │
│       │  com.example.OrderService                     │                  │
│       │  = "my-app"          │                       │                  │
│       │  ◄─── OK ─────────────                       │                  │
│       │                                               │                  │
│       ├─ 上报元数据(remote模式)──►                     │                  │
│       │  /dubbo/metadata/                             │                  │
│       │  my-app/abc123                                │                  │
│       │  = {JSON元数据}       │                       │                  │
│       │  ◄─── OK ─────────────                       │                  │
│       │                                               │                  │
│       └─ 暴露 MetadataService(local模式)               │                  │
│          端口: 20880                                  │                  │
│                                                       │                  │
│  ═══ Consumer 启动 ═══════════════════════════════════                   │
│                                                       │                  │
│                                         ② get()                       │
│                                            │                             │
│                                     ③ 查 Mapping ──────►               │
│                                       /dubbo/mapping/                   │
│                                       com.example.OrderService          │
│                                       ◄── "my-app" ────                │
│                                            │                             │
│                                     ④ 查实例 ──────────►               │
│                                       /services/my-app/                 │
│                                       ◄── [192.168.1.100:20880] ────   │
│                                            │             注册 Watcher    │
│                                            │                             │
│                                     ⑤ 获取元数据(remote) ──►            │
│                                       /dubbo/metadata/                  │
│                                       my-app/abc123                     │
│                                       ◄── {JSON} ────────              │
│                                            │                             │
│                                     ⑥ 或: RPC 调 Provider(local) ────► │
│                                       MetadataService.                 │
│                                       getExportedURLs(...)              │
│                                       ◄── URL列表 ───────────────────── │
│                                            │                             │
│                                     ⑦ 构建 Invoker                     │
│                                       {DubboInvoker@100,                │
│                                        DubboInvoker@101}                │
│                                            │                             │
│  ═══ RPC 调用 ═══════════════════════════════════════                    │
│                                                       │                  │
│                                     ⑧ 调用 OrderService.getOrder(123)   │
│                                       ──────────────────────────────►   │
│      收到请求,执行业务逻辑                                               │
│      返回结果                                                             │
│                                       ◄──────────────────────────────   │
│                                                       │                  │
│  ═══ 实例下线 ═══════════════════════════════════════                    │
│       │                                               │                  │
│   ⑨ ZK Session 过期       ──────────►                │                  │
│       节点删除                                        │                  │
│                                     ⑩ Watcher 回调 ──►                  │
│                                       (实例变更通知)  │                  │
│                                            │                             │
│                                     ⑪ 重新拉取实例列表 ──►              │
│                                       ◄── [] ──────────                │
│                                            │                             │
│                                     ⑫ 移除已下线 Invoker                 │
│                                        Invoker 列表更新                  │
│                                        后续请求只走存活实例               │
└──────────────────────────────────────────────────────────────────────────┘

七、Dubbo 2 vs Dubbo 3 全方位对比

7.1 核心差异总览

维度 Dubbo 2(接口级) Dubbo 3(应用级)
注册粒度 接口(Interface) 应用(Application)
ZK 路径 /dubbo/{interface}/providers/{url} /services/{app}/{ip:port}
节点数量 O(接口数 × 实例数) O(实例数)
元数据 嵌入在注册 URL 中 独立存储(元数据中心 或 Provider本地)
Consumer发现方式 直接查 /dubbo/{interface}/providers 三步:Mapping → Instance → Metadata
Watcher 数量 接口数 × 实例数 应用数
注册内容 URL 编码的完整配置 ServiceInstance JSON(含 revision 指针)
接口-应用映射 不需要(接口名直接定位) 需要 /dubbo/mapping/{interface}
接口变更影响 重新注册(URL 更新) revision 变化(共享元数据更新)
实例上下线开销 推送所有接口的 Watcher 只推送应用的 Watcher
云原生兼容 差(接口级模型与 K8s/SpringCloud 模型不一致) 好(应用级模型与 K8s Service 一致)
多协议 URL 中指定协议参数 ServiceInfo 中每个接口独立记录 protocol
多语言 需要实现 Dubbo URL 编解码 标准 JSON 元数据,无需私有编码
Consumer 迁移 N/A FORCE_INTERFACE / APPLICATION_FIRST / FORCE_APPLICATION

7.2 架构演进的意义

┌──────────────────────────────────────────────────────────────────────────┐
│                    从 Dubbo 2 到 Dubbo 3 的架构演进                        │
│                                                                          │
│  Dubbo 2(接口级)                                                        │
│  ┌────────────────────────────────────────────────────────────────────┐  │
│  │                                                                      │  │
│  │      Provider                         Consumer                       │  │
│  │  ┌──────────────┐               ┌──────────────┐                    │  │
│  │  │ 接口A + 配置  │──ZK 节点────►│ 订阅接口A     │                    │  │
│  │  │ 接口B + 配置  │──ZK 节点────►│ 订阅接口B     │                    │  │
│  │  │ 接口C + 配置  │──ZK 节点────►│ 订阅接口C     │                    │  │
│  │  └──────────────┘               └──────────────┘                    │  │
│  │                                                                      │  │
│  │  特点: 注册数据 = 实例信息 + 接口定义 + 治理规则(全耦合)             │  │
│  │  问题: 接口越多,ZK 压力越大;配置变更需要重新注册                      │  │
│  └────────────────────────────────────────────────────────────────────┘  │
│                                                                          │
│  Dubbo 3(应用级)                                                        │
│  ┌────────────────────────────────────────────────────────────────────┐  │
│  │                                                                      │  │
│  │      Provider                         Consumer                       │  │
│  │  ┌──────────────┐               ┌──────────────┐                    │  │
│  │  │ 实例信息      │──ZK 节点────►│ 发现应用实例   │                    │  │
│  │  │ (app+ip:port) │               │              │                     │  │
│  │  │              │               ├──────────────┤                    │  │
│  │  │ 映射信息      │──ZK 节点────►│ 接口→应用映射  │                    │  │
│  │  │ (I→A mapping) │               │              │                     │  │
│  │  │              │               ├──────────────┤                    │  │
│  │  │ 接口元数据     │──ZK/本地────►│ 获取接口定义  │                    │  │
│  │  │ (revision)    │               │              │                     │  │
│  │  └──────────────┘               └──────────────┘                    │  │
│  │                                                                      │  │
│  │  特点: 三者分离,各司其职                                             │  │
│  │  优势: 实例上下线不影响元数据;接口增删不影响实例注册                   │  │
│  └────────────────────────────────────────────────────────────────────┘  │
└──────────────────────────────────────────────────────────────────────────┘

7.3 迁移建议

阶段 Provider 配置 Consumer 配置 预期效果
第 1 步 register-mode=all 不变 Provider 双注册,Consumer 无感知
第 2 步 register-mode=all APPLICATION_FIRST Consumer 优先应用级发现
第 3 步 register-mode=all FORCE_APPLICATION Consumer 仅应用级发现
第 4 步 register-mode=instance FORCE_APPLICATION 全部切换到 Dubbo 3 模型

监控要点

  • ZK 节点数量变化(尤其是 /dubbo/ 路径下的节点数)
  • Consumer 端 subscribeURLs 耗时(应用级发现是否比接口级更快)
  • Provider 注册成功率(双注册是否有部分失败)
  • RPC 调用成功率(迁移前后对比)

八、总结

Dubbo 3 的应用级服务注册与发现,本质上是做了一次注册模型的范式转移

  1. 从”接口”到”应用”:注册粒度从接口级变为应用级,ZK 节点数从 O(N×M) 降到 O(M),与接口数解耦,消除了大规模微服务场景下的 ZK 存储和推送瓶颈。

  2. 从”耦合”到”分离”:实例信息(/services/)、接口映射(/dubbo/mapping/)、接口元数据(/dubbo/metadata/ 或 Provider 本地)三者彻底分开,各司其职。实例上下线不需要触动接口定义,接口变更不需要重新注册实例。

  3. 从”一步”到”三步”:Consumer 发现 Provider 的过程从 Dubbo 2 的”直接查接口的 providers 列表”变为”Mapping → Instance → Metadata”的三步链路,每一步都有缓存和监听,保证实时性的同时最大程度减少 ZK 交互。

  4. 从”硬迁移”到”软切换”:通过双注册 + 三种迁移规则,Dubbo 3 提供了从接口级到应用级的完整灰度路径,可以在不中断业务的情况下逐步完成架构升级。

  5. 与云原生对齐:应用级注册模型与 Kubernetes Service、Spring Cloud Discovery、Istio 等服务网格体系天然兼容,为 Dubbo 拥抱云原生铺平了道路。

注册中心不该存储接口定义,它只需要回答一个问题:“这个应用在哪些机器上?” —— Dubbo 3 终于把这件事做对了。


本文基于 Dubbo 3.2.x 源码分析,核心模块路径:

  • dubbo-registry/dubbo-registry-zookeeper/ — ZK 注册中心实现
  • dubbo-registry/dubbo-registry-api/ — 注册 API(Registry, ServiceDiscovery, ServiceDiscoveryRegistry)
  • dubbo-metadata-report/ — 元数据中心(MetadataReport, MetadataService)
  • dubbo-common/ — 公共模型(ServiceInstance, MetadataInfo, URL)
本文阅读量