Dubbo 接口调不通:从注册中心到网络层全链路排查

本文是源码级排障系列的第 1 篇
叙事框架:现象 → 排查过程 → 根因 → 修复 → 预防


问题现象

某日早高峰,订单服务 order-service 突然告警:接口 /order/user/{id} 错误率从 0.5% 飙升到 8.2%,报错信息为 No provider available for the service

告警群响应

该接口通过 Dubbo 调用用户服务 UserService 获取用户信息。测试环境一切正常,上线后逐步暴露。值班小A 和 开发老K 立即拉群排查。

排查过程

第一步:查注册中心 — Provider 是否注册

Dubbo 的调用链路始于注册中心。Provider 启动时会在 ZooKeeper 注册自身地址,Consumer 订阅后拿到 Provider 列表。如果 Provider 没注册上,Consumer 侧就会报 "No provider available"。

登录 consumer 机器,用 zkCli.sh 查看注册信息:

ZooKeeper 注册信息排查

可以看到 Provider 已注册(providers 目录下有一条 URL),Consumer 也已订阅(consumers 目录)。配置路由(configuratorsrouters)均为空,说明没有额外规则干扰。注册中心层面没有问题

源码级视角:Dubbo 的 RegistryDirectory 通过 notify() 接收注册中心推送的 Provider 列表,转为 urlInvokerMap。Consumer 调用时走 doList() 从该 Map 取 Invoker。

RegistryDirectory 源码

// RegistryDirectory.java — 核心:doList 抛 "No provider available"
@Override
public List<Invoker<T>> doList(Invocation invocation) throws RpcException {
    if (urlInvokerMap == null || urlInvokerMap.isEmpty()) {
        throw new RpcException("No provider available for the service "
            + getConsumerUrl().getServiceKey());
    }
    return new ArrayList<>(urlInvokerMap.values());
}

既然 Provider 已经注册了,那说明 Consumer 端已经拿到了地址。下一步看网络。

第二步:查网络层 — TCP 是否通

Dubbo 默认用 Dubbo 协议走 TCP 通信(端口 20880)。即使注册信息正确,如果网络不通或者连接异常,调用也会失败。

netstat -anp | grep 20880
ss -antp | grep 20880
telnet 192.168.1.100 20880
ping -c 3 192.168.1.100

网络连接排查

结果很清晰:Consumer 到 Provider 的 20880 端口有 6 条 ESTABLISHED 状态的 TCP 连接,telnet 和 ping 都正常。网络层过关

第三步:Dubbo QoS 诊断

Dubbo 内置了 QoS(Quality of Service)运维端口 22222,可以通过 telnet 直连,实时查看服务状态:

Dubbo QoS 诊断

dubbo> ls cn.opencao.sourcedebug.dubbofulllink.UserService
PROVIDER:
  1.0.0 online on 192.168.1.100:20880 (weight=100)

dubbo> count cn.opencao.sourcedebug.dubbofulllink.UserService 1.0.0 online
|   method name |   total |   failed |
|   getUserInfo |    1587 |       12 |

dubbo> status -s
  threadPool status: Pool status:OK, max:200, core:200, largest:42, active:3, task:1587
  connection pool status: Clients: 4, idle: 4, active: 0

看到指标:getUserInfo 调用总数 1587,失败 12 次。线程池状态看起来正常(active:3)。但是等一下——failed 计数器有 12,说明确实有调用失败了。日志呢?

第四步:查 Provider 日志 — 发现线程池爆满

Provider 错误日志

2026-06-19 10:23:45.678 ERROR [DubboServerHandler-20880-thread-42] DubboProtocol: Sending request error:
java.util.concurrent.TimeoutException: Waiting server-side execution timeout.
    at DefaultFuture.get(DefaultFuture.java:167)
    at DefaultFuture.get(DefaultFuture.java:137)
    at DubboInvoker.doInvoke(DubboInvoker.java:132)

2026-06-19 10:23:45.681 WARN  [DubboServerHandler-20880-thread-42] DubboProtocol: [DUBBO] The server-side threadpool is exhausted,
  Pool Size: 200 (active: 200, core: 200, max: 200, largest: 200), Task: 45678

线程池 200 个线程全部 active,任务积压 45678! 根源找到了。

DubboInvoker 源码

超时异常来自 DubboInvoker.doInvoke() 调用的 DefaultFuture.get()

DefaultFuture 超时源码

// DefaultFuture.java — 超时判断
if (now >= deadline) {
    FUTURES.remove(id);
    throw new TimeoutException(sent > start
        ? "Waiting server-side response timeout"
        : "Waiting server-side execution timeout");
}

这里有两种超时文案: - Waiting server-side execution timeout:请求还没发出去,说明网络不通或线程池排队超时 - Waiting server-side response timeout:请求已发出去,等不回响应

日志里实际抛的是 execution timeout,结合线程池全满的情况,说明请求在 Provider 端线程池排队等到超时,根本没被执行。

第五步:确认线程池状态

Provider 线程池全满

$ jstack 24589 | grep 'DubboServerHandler' | head -5   ← 200 个 DubboServerHandler 线程
$ top -H -p 24589                                        ← 大量线程在 RUNNABLE 状态

200 个线程全部繁忙。谁吃掉了所有线程?

根因分析

排查代码发现 UserServiceImpl.getUserInfo() 中存在一个慢查询场景:当 userId=999 时,会 sleep 6 秒。

@DubboService(version = "1.0.0", group = "online", timeout = 5000)
public class UserServiceImpl implements UserService {

    @Override
    public String getUserInfo(Long userId) {
        // ... 参数校验 ...
        if (userId == 999L) {
            TimeUnit.MILLISECONDS.sleep(6000); // ← 遗留的测试代码!
        }
        return "user-" + userId + "-name:Alice";
    }
}

连锁反应链条如下:

某个上游重试触发 userId=999 请求 → getUserInfo sleep 6s
  → 占用线程池 1 个线程 6 秒
  → 6 秒内更多请求涌入,线程池迅速占满
  → 后续请求在 DefaultFuture 等待超时(3s)
  → 消费端收到 TimeoutException,触发重试
  → 重试又涌入,线程池进一步恶化 → 恶性循环

Consumer 端配置的超时是 3000ms,而服务端有个请求要跑 6000ms+,单次就把线程池堵死。Dubbo 的 ThreadPoolFixedThreadPool(固定大小 200),不支持排队——所有任务必须在线程池里等待。

修复方案

两件事:移除慢查询测试代码 + 加入熔断保护

修复代码

private static final Set<Long> BLACKLIST = Set.of(999L);

@Override
public String getUserInfo(Long userId) {
    if (userId == null || userId <= 0) {
        throw new IllegalArgumentException("invalid userId: " + userId);
    }
    if (BLACKLIST.contains(userId)) {
        return "blocked-user-" + userId;  // 直接返回,不执行耗时逻辑
    }
    return "user-" + userId + "-name:Alice";
}

此外,还需要: 1. 配置 Sentinel 或 Resilience4j 熔断:当 getUserInfo 错误率 > 50% 时自动熔断,防止线程池被占满 2. 调整 Dubbo 线程池策略:将 FixedThreadPool 改为 CachedThreadPool,或调大 threads 参数并增加队列 3. 在 Consumer 端配置 retries=0:防止重试加剧雪崩

验证结果

修完重新发布后:

指标 修复前 修复后
线程池 active 数 200(全满) 3~12
超时异常率 12/1587(0.76%) 0
任务积压 45678 0
接口 P99 响应时间 超时 45ms

避坑建议

1. Dubbo 调用排查四层检查法

Dubbo 接口调不通,不要盲目怀疑某一层。从注册中心开始,逐层确认:

① 注册中心层 → ② 消费端路由层 → ③ 网络 TCP 层 → ④ Provider 线程池层

2. 线上代码准入规则

  • 禁止在 RPC 接口实现中写 sleep / Thread.wait() / 耗时循环
  • 所有 Dubbo 服务接口必须配置明确的 timeout
  • 高并发路径禁止使用 retries > 0

3. 线程池隔离

  • 关键 Dubbo 接口考虑独立线程池隔离(executor 属性)
  • 配置 Provider 端的 executes 限制(最大并发数)

4. 监控告警

  • 配置 Dubbo 线程池使用率监控(active / max 比例)
  • 设置 Provider 端 TIMEOUT 日志告警

附:完整命令清单

注册中心排查

# 查看所有 Provider
zkCli.sh -server 127.0.0.1:2181 ls /dubbo/com.example.UserService/providers

# 查看 Provider 注册详情(URL 参数)
zkCli.sh -server 127.0.0.1:2181 get /dubbo/com.example.UserService/providers/dubbo%3A%2F%...

# 查看所有 Consumer
zkCli.sh -server 127.0.0.1:2181 ls /dubbo/com.example.UserService/consumers

# 查看路由/配置规则
zkCli.sh -server 127.0.0.1:2181 ls /dubbo/com.example.UserService/routers
zkCli.sh -server 127.0.0.1:2181 ls /dubbo/com.example.UserService/configurators

网络层排查

netstat -anp | grep 20880 | grep ESTABLISHED
ss -antp | grep 20880
telnet <provider-ip> 20880
ping -c 3 <provider-ip>

Dubbo QoS 诊断

telnet 127.0.0.1 22222
ls                                    # 列出所有服务
ls com.example.UserService            # 查看服务详情
count com.example.UserService 1.0.0 group  # 查看方法调用统计
status -s                             # 查看线程池/连接池状态

Provider 线程池排查

jstack <pid> | grep 'DubboServerHandler' | wc -l          # 统计线程数
jstack <pid> | grep 'DubboServerHandler' | head -5        # 查看线程状态
top -H -p <pid>                                           # 查看线程 CPU
cat /proc/<pid>/status | grep Threads                     # 进程总线程数