排查 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

异常堆栈 + 注册中心查到的 providers

【发掘】源码追踪: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=falsereturn invokers——回退到全部 Provider,功能正常,路由规则形同虚设 - force=truereturn result——返回空列表,Consumer 拿不到任何 Provider,服务不可用

这行代码(第 79 行的 else if (force) return result)就是整条排障线索的终点。

为什么 No provider available 的堆栈指向 DynamicDirectory.doList() 而不是 ConditionRouter.route()?因为异常是在 doList() 中抛出的——它从 routerChain.route() 拿到空列表后,发现 forbidden && shouldFailFast 都满足,直接抛了 RpcException。而 ConditionRouter.route() 只是默默返回空列表,它不抛异常。堆栈指向哪里 ≠ 问题出在哪里——它只告诉你最后一站,你要自己往回追。

ConditionRouter.route() 核心分支 + RouterChain 全貌 RouterChain 类结构

【路径】🔍 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% 是这个问题。

IDE 搜索路径 + 断点设置

调试技巧

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 集群负载均衡策略选型不当导致的服务响应不均。