Dobbo3.x在原有的接口级模型之外引入了应用级服务发现。
1.思维导图

2.两种发现模型
2.1 Dubbo 2.x:接口级服务发现
每个服务接口独立注册到注册中心(以 ZK 为例):
ini
ZooKeeper
├── /dubbo/org.apache.dubbo.demo.DemoService/providers
│ ├── dubbo://192.168.1.1:20880/DemoService?group=&version=1.0.0
│ └── dubbo://192.168.1.2:20880/DemoService?group=&version=1.0.0
│
├── /dubbo/org.apache.dubbo.demo.GreetingService/providers
│ ├── dubbo://192.168.1.1:20880/GreetingService?group=&version=1.0.0
│ └── dubbo://192.168.1.3:20880/GreetingService?group=&version=1.0.0
│
└── ...(每个接口一个目录)
一个暴露了 50 个接口的应用程序在每个实例上会生成 50 个注册条目,随着接口和实例的增加,注册中心的数据将呈平方级增长。
2.2 Dubbo 3.x:应用级服务发现
每个应用只注册一次 到注册中心,并在该单一条目中携带其所有暴露接口的元数据。消费者通过 ServiceNameMapping 将接口名映射到应用名来发现实例,然后订阅实例级别的变更事件。:
bash
ZooKeeper(或 Nacos)
├── /services/UserService
│ └── {"name":"UserService", "id":"192.168.1.1:20880", "metadata":{"dubbo.endpoints":[...]}}
│
├── /services/OrderService
│ └── {"name":"OrderService", "id":"192.168.1.2:20880", "metadata":{...}}
│
└── ...(每个应用一个节点)
接口和应用的映射关系存在元数据中心(如 Nacos、ZK 的另一个路径):
css
元数据中心
├── DemoService → [UserService, OrderService]
└── GreetingService → [UserService]
3.三步迁移
迁移被建模为由 MigrationStep 枚举定义的严格三步阶梯,代表对应用级发现的逐步推进:
| 步骤 | 行为 | 风险等级 | 使用场景 |
|---|---|---|---|
| FORCE_INTERFACE | 仅接口级 Invoker 处于活跃状态;服务发现 Invoker 被销毁 | 最低 | 回滚或仅存留旧版环境 |
| APPLICATION_FIRST | 同时维护两个 Invoker;按 proportion 和 threshold 分配流量 |
中等 | 通过双通道验证进行渐进式迁移 |
| FORCE_APPLICATION | 仅服务发现 Invoker 处于活跃状态;接口级 Invoker 被销毁 | 最高 | 完成迁移的服务 |
3.1 安全门控:threshold(阈值)
DefaultMigrationAddressComparator.shouldMigrate() 决定"应用级地址够不够":
arduino
return ((float) newAddressSize / (float) oldAddressSize) >= threshold;
计算公式:应用级地址数 / 接口级地址数 >= threshold
| threshold | 含义 |
|---|---|
| 0.0(默认) | 只要有 1 个应用级地址就迁移 |
| 0.5 | 应用级地址数至少是接口级的 50% |
| 1.0 | 应用级地址数必须 >= 接口级(完全覆盖才迁移) |
3.2 流量比例:proportion
APPLICATION_FIRST 模式下,proportion=60 表示 60% 的流量走应用级,40% 走接口级。通过 ThreadLocalRandom 实现随机分配。
3.3 强制标志:force
force=true 时跳过阈值检查,强制切换。用于紧急回滚场景。
4. 完整对象关系图
yaml
MigrationInvoker
│
├── invoker (接口级 ClusterInvoker)
│ │
│ │ 创建:RegistryFactory SPI → ZookeeperRegistryFactory → ZookeeperRegistry
│ │ 订阅:ZK /dubbo/DemoService/providers
│ │ 回调:Provider 列表变了 → directory.notify() → 更新 Invoker 列表
│ │
│ └── FailoverClusterInvoker
│ └── RegistryDirectory
│ ├── registry: ZookeeperRegistry(直接操作 ZK)
│ └── urlInvokerMap: {url → DubboInvoker}
│
├── serviceDiscoveryInvoker (应用级 ClusterInvoker)
│ │
│ │ 创建:RegistryFactory SPI → ServiceDiscoveryRegistryFactory
│ │ → ServiceDiscoveryRegistry(内部用 ServiceDiscovery)
│ │ 订阅:Nacos /services/UserService
│ │ 回调:实例列表变了 → directory.notify() → 更新 Invoker 列表
│ │
│ └── FailoverClusterInvoker
│ └── ServiceDiscoveryRegistryDirectory
│ ├── registry: ServiceDiscoveryRegistry
│ │ └── serviceDiscovery: NacosServiceDiscovery
│ └── urlInvokerMap: {url → DubboInvoker}
│
└── currentAvailableInvoker → 指向上面其中一个,根据 MigrationRule 切换
5.核心类详解
1. MigrationInvoker --- 核心切换器
职责: 持有接口级和应用级两个 ClusterInvoker,根据规则决定当前用哪个。
属性详解
csharp
public class MigrationInvoker<T> implements MigrationClusterInvoker<T> {
注入的依赖(构造函数传入,不可变)
| 属性 | 类型 | 为什么需要 |
|---|---|---|
registryProtocol |
RegistryProtocol |
需要调用它的 getInvoker() 和 getServiceDiscoveryInvoker() 来创建接口级/应用级 invoker。它是"invoker 工厂" |
cluster |
Cluster |
创建 ClusterInvoker 时需要。比如 FailoverCluster、FailfastCluster 等容错策略 |
registry |
Registry |
注册中心实例(ZookeeperRegistry / NacosRegistry)。创建 Directory 时需要设置 registry |
type |
Class<T> |
服务接口类型,比如 DemoService.class。创建 Directory 和 Invoker 时必须知道接口类型 |
url |
URL |
registry:// 开头的 URL,包含注册中心地址和参数。用于创建 invoker 时传递注册中心信息 |
consumerUrl |
URL |
consumer:// 开头的 URL,包含消费者参数(interface、methods、timeout 等)。是消费者的"身份证" |
consumerModel |
ConsumerModel |
消费者模型,包含服务元数据。用于在 destroy 时清理注册表中的引用 |
reportService |
FrameworkStatusReportService |
状态上报服务。迁移成功/失败时上报状态,用于监控 |
核心状态(运行时可变,volatile)
| 属性 | 类型 | 为什么需要 |
|---|---|---|
invoker |
ClusterInvoker<T> |
接口级 invoker 。由 RegistryDirectory + Cluster 组成。订阅 ZK 的 /dubbo/InterfaceName/providers 节点。当 Consumer 还是 Dubbo 2.x 模式时,用这个 |
serviceDiscoveryInvoker |
ClusterInvoker<T> |
应用级 invoker 。由 ServiceDiscoveryRegistryDirectory + Cluster 组成。订阅应用级注册中心。当 Consumer 升级到 Dubbo 3.x 模式时,用这个 |
currentAvailableInvoker |
ClusterInvoker<T> |
当前实际在用的 invoker 。invoke() 方法直接调用它的 invoke()。根据 step 的不同,它可能指向 invoker 或 serviceDiscoveryInvoker |
step |
MigrationStep |
当前迁移阶段:FORCE_INTERFACE / APPLICATION_FIRST / FORCE_APPLICATION。决定 currentAvailableInvoker 指向谁 |
rule |
MigrationRule |
当前生效的规则。保存在这里供 invoke() 时使用(比如 APPLICATION_FIRST 模式下需要重新评估) |
promotion |
int |
流量比例(0-100)。APPLICATION_FIRST 模式下,promotion=60 表示 60% 的流量走应用级,40% 走接口级 |
辅助属性
| 属性 | 类型 | 为什么需要 |
|---|---|---|
migrationRuleListener |
MigrationRuleListener |
反向引用。destroy 时需要通知 listener 从 handlers map 中移除自己 |
核心方法
invoke(Invocation) --- 每次 RPC 调用都走这里
scss
public Result invoke(Invocation invocation) {
if (currentAvailableInvoker != null) {
if (step == APPLICATION_FIRST) {
// 流量比例控制:promotion=60 → 60% 走应用级,40% 回退接口级
if (promotion < 100 && ThreadLocalRandom.current().nextDouble(100) > promotion) {
return invoker.invoke(invocation); // 回退到接口级
}
// 检查应用级是否可用,不可用则回退
return decideInvoker().invoke(invocation);
}
return currentAvailableInvoker.invoke(invocation);
}
// 首次调用:根据 step 初始化 currentAvailableInvoker
...
}
为什么有 promotion 流量比例?
假设你有 100 个 Provider,其中 60 个升级到了 Dubbo 3.x。设置 promotion=60 意味着 60% 的请求走应用级发现(能访问到 60 个 Provider),40% 走接口级发现(能访问到全部 100 个 Provider)。这样即使应用级有问题,也有 40% 的流量是安全的。
migrateToForceInterfaceInvoker(rule) --- 切到接口级
kotlin
public boolean migrateToForceInterfaceInvoker(MigrationRule newRule) {
// 1. 创建/刷新接口级 invoker
refreshInterfaceInvoker(latch);
// 2. 如果应用级 invoker 不存在,直接切
if (serviceDiscoveryInvoker == null) {
this.currentAvailableInvoker = invoker;
return true;
}
// 3. 等待地址通知
waitAddressNotify(newRule, latch);
// 4. 如果 force=true,跳过阈值检查,强制切换
if (newRule.getForce(consumerUrl)) {
this.currentAvailableInvoker = invoker;
destroyServiceDiscoveryInvoker(); // 销毁应用级 invoker
return true;
}
// 5. 阈值检查:接口级地址 >= 阈值比例 × 应用级地址?
// 注意:这里是反向检查 --- 确保切回接口级后地址够用
if (detectors.allMatch(c -> c.shouldMigrate(invoker, serviceDiscoveryInvoker, newRule))) {
this.currentAvailableInvoker = invoker;
destroyServiceDiscoveryInvoker();
return true;
}
// 6. 阈值不够,不切换
return false;
}
为什么要 waitAddressNotify?
刚创建 invoker 时,ZK 的 Watcher 还没回调,地址列表可能是空的。等一下让地址通知到达,才能做准确的阈值比较。
为什么有 force 参数?
紧急情况下(比如应用级发现出了 bug),运维想立刻切回接口级,不想等阈值检查。force=true 跳过所有安全检查。
refreshInterfaceInvoker(latch) --- 创建/刷新接口级 invoker
scss
protected void refreshInterfaceInvoker(CountDownLatch latch) {
clearListener(invoker); // 清除旧的监听器
if (needRefresh(invoker)) { // invoker == null || 已销毁 || 没有地址
if (invoker != null) invoker.destroy();
// 调用 RegistryProtocol 创建新的接口级 invoker
invoker = registryProtocol.getInvoker(cluster, registry, type, url);
}
// 设置地址变更监听器
setListener(invoker, () -> {
latch.countDown(); // 通知等待方"地址到了"
if (step == APPLICATION_FIRST) {
calcPreferredInvoker(rule); // 重新评估用哪个 invoker
}
});
}
为什么用 CountDownLatch?
migrateToForceApplicationInvoker 需要等待地址通知到达后才能做阈值比较。CountDownLatch 是一个"等 N 个事件发生"的同步工具。这里 N=1(等一次地址通知)。
calcPreferredInvoker(rule) --- APPLICATION_FIRST 模式下重新评估
typescript
private synchronized void calcPreferredInvoker(MigrationRule migrationRule) {
if (serviceDiscoveryInvoker == null || invoker == null) return;
Set<MigrationAddressComparator> detectors = ...getSupportedExtensionInstances();
if (detectors.allMatch(c -> c.shouldMigrate(serviceDiscoveryInvoker, invoker, migrationRule))) {
this.currentAvailableInvoker = serviceDiscoveryInvoker; // 应用级够了
} else {
this.currentAvailableInvoker = invoker; // 不够,保持接口级
}
}
为什么是 synchronized?
地址通知可能从多个线程同时到达(接口级和应用级的 Watcher 回调)。synchronized 防止并发修改 currentAvailableInvoker 导致不一致。
2. MigrationRuleListener --- 配置监听器
职责: 监听配置中心的规则变更,管理所有 MigrationInvoker 的 MigrationRuleHandler。
属性详解
kotlin
@Activate
public class MigrationRuleListener implements RegistryProtocolListener, ConfigurationListener {
核心状态
| 属性 | 类型 | 为什么需要 |
|---|---|---|
handlers |
ConcurrentMap<MigrationInvoker<?>, MigrationRuleHandler<?>> |
核心映射表。每个 MigrationInvoker 对应一个 MigrationRuleHandler。规则变更时遍历这个 map,对每个 handler 执行迁移 |
rawRule |
String(volatile) |
当前规则的原始 YAML 字符串。用于去重 --- 如果新规则和当前规则一样,跳过 |
rule |
MigrationRule(volatile) |
当前规则的对象形式。由 rawRule 解析而来 |
ruleKey |
String |
配置中心的 key。格式:应用名.migration,比如 demo-consumer.migration |
configuration |
DynamicConfiguration |
配置中心客户端。用于添加/移除监听器、获取配置 |
并发控制
| 属性 | 类型 | 为什么需要 |
|---|---|---|
ruleQueue |
LinkedBlockingQueue<String> |
规则队列。配置中心可能快速推送多次规则,用队列缓冲,逐个处理 |
executorSubmit |
AtomicBoolean |
确保只有一个线程在消费 ruleQueue。compareAndSet(false, true) 保证只提交一次任务 |
ruleManageExecutor |
ExecutorService(单线程) |
消费 ruleQueue 的线程。单线程保证规则按顺序处理 |
ruleMigrationFuture |
Future<?> |
消费线程的 Future。destroy 时需要 cancel |
localRuleMigrationFuture |
ScheduledFuture<?> |
本地规则延迟加载的 Future。destroy 时需要 cancel |
依赖
| 属性 | 类型 | 为什么需要 |
|---|---|---|
moduleModel |
ModuleModel |
模块模型。用于获取应用名、配置环境、动态配置等 |
核心方法
init() --- 初始化
scss
private void init() {
// 1. 构造配置 key
this.ruleKey = moduleModel.getApplicationModel().getApplicationName() + ".migration";
// 2. 获取配置中心客户端
this.configuration = moduleModel.modelEnvironment().getDynamicConfiguration().orElse(null);
if (this.configuration != null) {
// 3. 添加监听器
configuration.addListener(ruleKey, "DUBBO_SERVICEDISCOVERY_MIGRATION", this);
// 4. 读取当前规则
String rawRule = configuration.getConfig(ruleKey, "DUBBO_SERVICEDISCOVERY_MIGRATION");
if (StringUtils.isEmpty(rawRule)) rawRule = INIT;
setRawRule(rawRule);
}
// 5. 如果有本地规则文件,延迟加载(等配置中心的规则,60秒没来才用本地的)
String localRawRule = moduleModel.modelEnvironment().getLocalMigrationRule();
if (!StringUtils.isEmpty(localRawRule)) {
localRuleMigrationFuture = schedule(() -> {
if (this.rawRule.equals(INIT)) {
this.process(new ConfigChangedEvent(null, null, localRawRule));
}
}, getDelay(), TimeUnit.MILLISECONDS);
}
}
为什么有本地规则延迟加载?
场景:Consumer 启动时配置中心还没准备好(网络问题、配置中心重启)。等 60 秒(默认)后如果还没收到配置中心的规则,就用本地的规则文件。这保证了即使配置中心不可用,迁移也能正常进行。
process(ConfigChangedEvent) --- 配置变更回调
scss
public synchronized void process(ConfigChangedEvent event) {
String rawRule = event.getContent();
if (StringUtils.isEmpty(rawRule)) rawRule = INIT;
// 1. 放入队列
ruleQueue.put(rawRule);
// 2. 确保消费线程在运行
if (executorSubmit.compareAndSet(false, true)) {
ruleMigrationFuture = ruleManageExecutor.submit(() -> {
while (true) {
String rule = ruleQueue.take(); // 阻塞等待
// 3. 去重
if (Objects.equals(this.rawRule, rule)) continue;
// 4. 更新规则
setRawRule(rule);
// 5. 并行执行所有 handler 的迁移
ExecutorService executor = Executors.newFixedThreadPool(
Math.min(handlers.size(), 100));
List<Future<?>> futures = new ArrayList<>();
for (MigrationRuleHandler<?> handler : handlers.values()) {
futures.add(executor.submit(() -> handler.doMigrate(this.rule)));
}
// 6. 等待所有迁移完成
for (Future<?> future : futures) {
future.get();
}
executor.shutdown();
}
});
}
}
为什么用队列 + 单线程消费,而不是直接在回调里处理?
配置中心的回调可能从多个线程并发触发(Nacos 的长轮询线程、ZK 的 Watcher 线程)。用队列串行化处理,避免:
- 规则乱序(先到的规则后处理)
- 并发迁移(两个线程同时对同一个 invoker 执行迁移)
为什么迁移用线程池并行执行?
一个 Consumer 可能订阅了 100 个接口,每个接口有一个 MigrationInvoker。规则变更时需要对这 100 个 invoker 都执行迁移。串行执行太慢,并行执行可以大幅缩短迁移时间。
onRefer() --- Consumer 引用时调用
swift
public void onRefer(RegistryProtocol registryProtocol, ClusterInvoker<?> invoker,
URL consumerUrl, URL registryURL) {
// 为这个 MigrationInvoker 创建 Handler
MigrationRuleHandler<?> handler = ConcurrentHashMapUtils.computeIfAbsent(
handlers, (MigrationInvoker<?>) invoker,
_key -> {
((MigrationInvoker<?>) invoker).setMigrationRuleListener(this);
return new MigrationRuleHandler<>((MigrationInvoker<?>) invoker, consumerUrl);
});
// 立即用当前规则执行一次迁移
migrationRuleHandler.doMigrate(rule);
}
为什么在 onRefer 时就执行迁移?
Consumer 启动时,每个接口的 ReferenceConfig 都会调用 RegistryProtocol.refer(),创建 MigrationInvoker。interceptInvoker() 会触发 MigrationRuleListener.onRefer()。这时需要用当前已有的规则(可能从配置中心读到的,也可能是默认规则)立即初始化 invoker 的状态。
3.ServiceDiscoveryRegistry-是关键的连接器
职责: 它实现了经典的 Registry 接口,但在内部委托给 ServiceDiscovery 实例,将接口级的订阅/注册调用转换为应用级的等价操作。
它解决的问题
php
Registry 接口, ServiceDiscovery 接口
ServiceDiscoveryRegistry 桥接了这两者:
左边:实现 Registry 接口(让 Directory 能调用)
右边:内部用 ServiceDiscovery(和 Nacos 通信)
谁干什么
scss
ServiceDiscoveryRegistry(指挥官)
│
│ 1. 调用 serviceNameMapping.getMapping(url)
│ → 查元数据中心:哪个应用提供 DemoService?
│ → 返回 ["UserService"]
│
│ 2. 调用 serviceDiscovery.getInstances("UserService")
│ → 让 ServiceDiscovery 去 Nacos 查实例
│ → 返回 [192.168.1.1:20880, 192.168.1.2:20880]
│
│ 3. 调用 serviceDiscovery.addServiceInstancesChangedListener(listener)
│ → 让 ServiceDiscovery 监听 Nacos 实例变化
│
▼
ServiceDiscovery
│ 负责和 Nacos/ZK 通信
│ getInstances() → 查实例
│ addServiceInstancesChangedListener() → 监听变化
ServiceDiscoveryRegistry 负责编排流程,ServiceDiscovery 负责和注册中心通信。
6.触发机制
6.1 启动时触发(onRefer)
Consumer 启动时,MigrationInvoker 刚创建出来是"空的"(currentAvailableInvoker == null),需要立刻初始化:
scss
Consumer 启动
→ RegistryProtocol.refer()
→ 创建 MigrationInvoker(空的)
→ interceptInvoker()
→ MigrationRuleListener.onRefer()
→ 创建 MigrationRuleHandler
→ handler.doMigrate(rule) ← 立刻用当前规则执行一次迁移
→ 创建接口级/应用级 invoker
→ 设置 currentAvailableInvoker
→ MigrationInvoker 现在有状态了,可以正常工作
为什么要立刻执行? 如果等到第一次 RPC 调用才初始化,第一次调用会很慢(要等创建 invoker + 订阅 ZK + 等待地址通知)。
6.2 规则变更时触发
scss
运维在 Nacos 控制台修改规则
→ Nacos 推送变更通知
→ MigrationRuleListener.process(rawRule)
→ 放入 ruleQueue
→ 单线程消费:遍历所有 handler
→ MigrationRuleHandler.doMigrate(rule)
→ MigrationInvoker.migrateToXxx(rule)
6.3 地址变更时触发
ini
Provider 上下线
→ ZK/Nacos Watcher 触发
→ RegistryDirectory.notify() 或 ServiceDiscoveryRegistryDirectory.notify()
→ 更新 Invoker 列表
→ InvokersChangedListener.onChange()
→ MigrationInvoker 标记 invokersChanged = true
→ APPLICATION_FIRST 模式下重新评估阈值