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 的应用级服务注册与发现,本质上是做了一次注册模型的范式转移:
-
从”接口”到”应用”:注册粒度从接口级变为应用级,ZK 节点数从 O(N×M) 降到 O(M),与接口数解耦,消除了大规模微服务场景下的 ZK 存储和推送瓶颈。
-
从”耦合”到”分离”:实例信息(
/services/)、接口映射(/dubbo/mapping/)、接口元数据(/dubbo/metadata/或 Provider 本地)三者彻底分开,各司其职。实例上下线不需要触动接口定义,接口变更不需要重新注册实例。 -
从”一步”到”三步”:Consumer 发现 Provider 的过程从 Dubbo 2 的”直接查接口的 providers 列表”变为”Mapping → Instance → Metadata”的三步链路,每一步都有缓存和监听,保证实时性的同时最大程度减少 ZK 交互。
-
从”硬迁移”到”软切换”:通过双注册 + 三种迁移规则,Dubbo 3 提供了从接口级到应用级的完整灰度路径,可以在不中断业务的情况下逐步完成架构升级。
-
与云原生对齐:应用级注册模型与 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)