排查 3 小时,问题竟在 Dubbo 路由规则:一个 force 的坑
场景:排查 3 小时,问题竟在 Dubbo 路由规则:一个 force 的坑 路径:
DynamicDirectory.doList()→RouterChain.route()→ConditionRouter.route()→matchThen()
【遗迹】异常堆栈:Provider 明明在线,Consumer 却说找不到
上篇讲了 Dubbo 异步调用回调未执行的源码追踪,这篇来看另一个同样反直觉的问题。
Dubbo(2.7.23)报错 No provider available——但注册中心 Provider 明明在线。翻到 DynamicDirectory.doList() 的源码才发现——路由规则把 Provider 全过滤了。
凌晨 2 点,线上报警:Consumer 调用远程服务超时。查看日志,堆栈里躺着一条干巴巴的异常:
Caused by: org.apache.dubbo.rpc.RpcException: No provider available from registry
127.0.0.1:2181 for service com.example.GreetingService ...
第一步反应:查注册中心。
zkServer.sh status → 正常。
ls /dubbo/com.example.GreetingService/providers → Provider 列表完整:
provider://10.0.2.10:20880/...side=provider
provider://10.0.2.11:20880/...side=provider
provider://10.0.2.12:20880/...side=provider
第二步反应:会不会是注册中心没同步彻底?
重启 Consumer,不行。重启 Provider,不行。清空本地缓存、重连——全没用。
折腾 3 小时后,负责灰度发布的同事问了一句:"Admin 上那条路由规则你们看了吗?"
翻 Dubbo Admin → 服务治理 → 条件路由,果然躺着一条:
```yaml
scope: service
force: true
conditions:
- host = 10.0.1.* =>
这条规则的意思是"只允许调用 10.0.1.x 网段的 Provider"。但这次灰度发布的机器全在 10.0.2.x——加上 force: true,Consumer 直接拿不到任何 Provider。
【直觉 vs 现实】:大多数人看到"No provider available"第一反应是注册中心出问题或网络不通。但如果是路由规则把 Provider 全过滤了,堆栈里不会有任何路由相关的提示——就是一条干巴巴的 No provider available。你花半小时查网络,它只需要一个 Ctrl+F → force。

【发掘】源码追踪:RouterChain 如何过滤 Provider
追根溯源:Consumer 发起 RPC 调用时,DynamicDirectory.doList() 从注册中心拿到全部 Provider 地址后,交给 RouterChain 做过滤(Dubbo 2.7.23 DynamicDirectory.java#L172-194):
public List<Invoker<T>> doList(Invocation invocation) {
if (forbidden && shouldFailFast) {
throw new RpcException(..., "No provider available from registry ...");
}
List<Invoker<T>> invokers = null;
try {
invokers = routerChain.route(getConsumerUrl(), invocation);
} catch (Throwable t) { ... }
return invokers == null ? Collections.emptyList() : invokers;
}
看到这里可能会有疑问:routerChain.route() 返回空列表时,doList 不做任何兜底处理,直接返回空。
RouterChain 把当前注册的所有 Router 串起来逐个执行(RouterChain.java#L98-L104):
public List<Invoker<T>> route(URL url, Invocation invocation) {
List<Invoker<T>> finalInvokers = invokers;
for (Router router : routers) {
finalInvokers = router.route(finalInvokers, url, invocation);
}
return finalInvokers;
}
Router 是动态插入的——RegistryDirectory 订阅注册中心的配置变更时,通过 ListenableRouter.process() 监听路由规则变化(ListenableRouter.java#L59-L77):
public synchronized void process(ConfigChangedEvent event) {
if (event.getChangeType().equals(ConfigChangeType.DELETED)) {
routerRule = null;
conditionRouters = Collections.emptyList();
} else {
routerRule = ConditionRuleParser.parse(event.getContent());
generateConditions(routerRule);
}
}
Admin 上 YAML 格式的路由规则 → 配置中心(Zookeeper/Nacos)→ ConditionRuleParser.parse() 解析为 ConditionRouterRule 对象 → generateConditions() 生成 ConditionRouter 实例列表。
这就是路由规则的完整注入路径。当 process() 收到一条新规则时,generateConditions() 内部会逐个创建 ConditionRouter:
private void generateConditions(ConditionRouterRule rule) {
if (rule != null && rule.isValid()) {
this.conditionRouters = rule.getConditions()
.stream()
.map(condition -> new ConditionRouter(condition,
rule.isForce(), rule.isEnabled()))
.collect(Collectors.toList());
}
}
关键就在这里:rule.isForce() 在创建 ConditionRouter 时就传入构造方法,这个参数决定了后续匹配为空时的行为。
那么 ConditionRouter.route() 到底做了什么?(ConditionRouter.java#L179-L212)
public <T> List<Invoker<T>> route(List<Invoker<T>> invokers, URL url, Invocation invocation) {
if (!enabled) return invokers;
if (!matchWhen(url, invocation)) return invokers; // ← 当前请求不匹配 when 条件,跳过
List<Invoker<T>> result = new ArrayList<>();
for (Invoker<T> invoker : invokers) {
if (matchThen(invoker.getUrl(), url)) { // ← 逐个检查 provider 是否匹配 then 条件
result.add(invoker);
}
}
if (!result.isEmpty()) return result;
else if (force) return result; // ← force=true + 空结果 = 无 Provider!
return invokers; // ← force=false 时回退到原始列表
}
执行路径是这样的:Consumer 携带 host = 10.0.1.* 条件进入 matchWhen()——匹配成功(when 条件是应用于 Consumer 自身的),于是进入 then 条件匹配阶段。But matchThen() 逐一检查每个 Provider 的 host——3 个 Provider 全是 10.0.2.x,没有一个匹配 10.0.1.*。result 为空。
到这里就分叉了:
- force=false:return invokers——回退到全部 Provider,功能正常,路由规则形同虚设
- force=true:return result——返回空列表,Consumer 拿不到任何 Provider,服务不可用
这行代码(第 79 行的 else if (force) return result)就是整条排障线索的终点。
为什么 No provider available 的堆栈指向 DynamicDirectory.doList() 而不是 ConditionRouter.route()?因为异常是在 doList() 中抛出的——它从 routerChain.route() 拿到空列表后,发现 forbidden && shouldFailFast 都满足,直接抛了 RpcException。而 ConditionRouter.route() 只是默默返回空列表,它不抛异常。堆栈指向哪里 ≠ 问题出在哪里——它只告诉你最后一站,你要自己往回追。

【路径】🔍 IDE 到达路径:三步入桩排障法
下次遇到同样的问题,不需要 Google——三步定位。
第一步:验证是否是路由问题
IDE: Ctrl+Shift+N → 输入 DynamicDirectory → 回车
→ Ctrl+F → 输入 doList → 回车
→ 看第 188 行:routerChain.route(getConsumerUrl(), invocation)
文件: dubbo-cluster/src/main/java/org/apache/dubbo/
rpc/cluster/directory/AbstractDirectory.java → 继承类 DynamicDirectory
设断点在这一行。
用 Consumer URL 的 serviceKey 查看 routerChain 中包含哪些 Router。
第二步:检查路由规则是否匹配
IDE: Ctrl+Shift+N → 输入 ConditionRouter → 回车
→ Ctrl+F → 输入 route → 回车
→ 跳到第 179 行
文件: dubbo-cluster/src/main/java/org/apache/dubbo/
rpc/cluster/router/condition/ConditionRouter.java
在 matchWhen 和 matchThen 处设断点。
观察 invocation 参数和 matchThen 对每个 invoker URL 的返回值。
第三步:确认 force 值
IDE: ConditionRouter 构造方法设断点(第 71-77 行):
public ConditionRouter(String rule, boolean force, boolean enabled)
文件: dubbo-cluster/src/main/java/org/apache/dubbo/
rpc/cluster/router/condition/ConditionRouter.java:71
观察 force 参数的来源——它来自路由规则 YAML 中的 force 字段。
如果 force=true 且 matchThen 全部返回 false → 100% 是这个问题。

调试技巧
当 force=true 且路由结果为空时,Consumer 日志会输出一条 WARN:
WARN ... ConditionRouter - The route result is empty and force execute.
consumer: 192.168.1.10, service: com.example.GreetingService,
router: host = 10.0.1.* =>
这条 WARN 日志比 No provider available 异常早了大约 10 毫秒——因为它是在 doList 抛出异常之前由 ConditionRouter.route() 打出的。下次排查时先搜这条 WARN,能省 90% 的时间。
【解读】为什么这么设计
force=true:硬约束还是定时炸弹?
force 参数的存在不是 Bug,是有意为之的设计取舍。
没有 force 会怎样? Consumer 请求来了,路由过滤出空列表——自动 fallback 到全部 Provider。这就是"软路由":路由规则表现为建议而非约束。灰度场景下,明明路由规则配了"只走灰度分组",但因为灰度分组没机器,流量自动走到了线上分组——灰度变成了空谈。
有 force 的代价:路由条件只要写错一笔,Consumer 就完全不可用。框架把"路由规则"当成"路由契约"来执行——匹配不到就是匹配不到,不会自作主张给你 fallback。
横向对比:其他框架怎么处理空路由?
Dubbo 的设计在主流 RPC/网关框架里算是最激进的:
| 框架 | 空匹配行为 | 哲学 |
|---|---|---|
| Dubbo force=true | 返回空列表,Consumer 报错 | 契约优先:路由是硬约束 |
| Dubbo force=false | 回退到全部 Provider | 兼容模式:路由是建议 |
| Spring Cloud Gateway | 找不到路由 → 404,但可通过 spring.cloud.gateway.default-filters 配置 fallback |
网关默认 fail-fast,但允许全局兜底 |
| Sentinel | 降级规则匹配不到 → 走正常调用 | 熔断降级是附加保护,不影响主流程 |
| Nginx upstream | 找不到 upstream → 502,但可用 backup 配置备用服务器 |
主备分离,显式声明 |
Dubbo 的 force=true 和 Nginx 的 no backup 最接近——都是"配置错了就死"的策略。不同的是:Nginx 配置错了 reload 会失败,能在发布前发现问题;Dubbo 路由规则通过 Admin 动态下发,改了立即可见,没有预检机制。这才是真正的风险。
执行顺序:为什么路由必须在前
路由规则在负载均衡之前执行(RouterChain.route() → LoadBalance.select())。这条顺序保证了路由是一个过滤操作而非选择操作:
- 路由:从 N 个 Provider 中选出符合规则的 M 个(M ≤ N,M 可以是 0)
- 负载均衡:从 M 个中选 1 个(前提 M > 0)
如果路由在后、负载均衡在前,那路由就失去了意义——每次调用先挑一个 Provider 发过去,然后路由说"这个 Provider 不符合规则"——为时已晚。
框架的作者不是随机写这行代码的——force 参数是用来保护灰度策略不被绕过的,但它不保护写错条件的你。

【收获】排查锚点
下次看到 No provider available + 注册中心显示 Provider 在线,别再查网络了。
force 决策矩阵
| 场景 | force 取值 | 原因 |
|---|---|---|
| 灰度发布:流量只走指定分组 | true |
防止流量误入线上分组 |
| 黑名单:禁止某些 Consumer 调用 | true |
硬拒绝,没有回退 |
| 兼容阶段:大部分流量走正常,小部分引流测试 | false |
测试分组挂了不影响主流程 |
| 路由规则刚上线,不确定条件是否正确 | false |
即使配错也不影响现网 |
| 全链路压测标记透传 | true |
压测流量必须走压测分组 |
两步验证法
Step 1: curl dubbo-admin 查路由规则
curl -s http://dubbo-admin:8080/governance/routes | \
jq '.data[] | select(.service=="com.example.GreetingService")'
关注 force 和 conditions 字段
Step 2: grep Consumer 日志确认
grep "route result is empty and force execute" app.log
如果匹配到 → 路由过滤全部 Provider,无需再看其他
排查锚点速查
| 异常现象 | 入口类 | 关键方法 | 排查优先级 |
|---|---|---|---|
| No provider available | DynamicDirectory |
doList() L188 |
1️⃣ 先查注册中心 Provider 是否存在 |
| 路由结果与预期不符 | ConditionRouter |
route() L202-L206 |
2️⃣ 再查路由规则 |
| force 行为判断 | ConditionRouter |
force 字段 L204 |
3️⃣ 最后确认 force |
| WARN 日志路由为空 | ConditionRouter |
route() matchThen |
🔑 最快线索:先搜这条 WARN |
自检清单
写完路由规则后,一条一条过:
□ force 取值有意识:不是默认 true,也不是默认 false
□ conditions 中的 host/application 参数名与注册中心 URL 中的字段名完全一致
□ when 条件的 scope 正确(service vs application 级别)
□ 规则已用 force=false 预验证过匹配结果
□ 预验证通过后才改为 force=true
□ 同一条路由规则配置了多条件时,条件之间是 AND 还是 OR,确认过源码行为
这条规则可以帮你节省数小时的网络排查时间——一个异常堆栈 = 一个类的某个方法出了问题。

下篇我们聊 Dubbo 集群负载均衡策略选型不当导致的服务响应不均。