Java

RPC 超时但下游不慢,很多时候是请求根本没稳定到下游

RPC timeout 而下游 RT 看起来正常时,排查最容易被带偏到“下游是不是没暴露出来”。很多真实现场里,请求预算其实消耗在连接池、代理层或回调线程这些中间环节,请求根本没稳定到下游。

  • RPC
  • 接口超时
  • Netty
  • Service Mesh
  • 性能排查
10 分钟阅读

RPC timeout 最让人别扭的一种现场,是调用方已经开始成片超时了,下游却说自己没慢。

这种时候,排障讨论特别容易卡住。

调用方会觉得:

  • 都 timeout 了,下游怎么可能没问题?

下游会觉得:

  • 我收到的请求没那么多,RT 也没炸,为什么锅先到我这?

两边都不算胡说,但如果只围着“下游到底慢没慢”打转,现场很容易越查越僵。

因为这类问题里,一个很常见、也很容易被漏掉的事实是:

请求未必真的稳定到了下游。

也就是说,timeout 不一定发生在下游业务执行阶段,它也可能发生在请求到达下游之前,或者响应回来以后、但还没来得及被调用方正确处理的时候。

我更愿意把这类问题压成一条很短的判断线:

  • 不是先问下游慢不慢
  • 而是先问请求有没有稳定到下游

只要这一步没搞清,现场就会一直在“你说你没慢,我说我都 timeout 了”这种争论里打转。

为什么“下游 RT 正常”并不能说明链路没问题

因为调用方感知到的 RPC 耗时,本来就不只是下游业务处理时间。

一条 RPC 从调用方视角看,前后至少还夹着几段很容易被低估的成本:

  • 借连接
  • 发请求
  • 经过代理或 sidecar 转发
  • 等响应回来
  • 回调线程分发结果

只要这几段里有一段明显变长,调用方看到的就是 timeout。

而下游常说的“我这边 RT 正常”,很多时候只覆盖了它真正收到请求之后的那一小段处理时间。

所以两边看到的不是同一段时间,本来就可能同时成立:

  • 调用方大量 timeout
  • 下游 RT 仍然正常

这不是谁在撒谎,而是预算可能在中间层被吃掉了。

这类现场里,我最先怀疑的通常不是所有中间层,而是三段最常见的位置

原文如果把所有中间层一股脑铺开,很容易又写成一篇知识库总览。真到值班现场,我更常先盯下面三段。

第一段:调用端连接池

这是特别高频、也特别容易被忽略的一层。

如果请求在真正发出去之前,要先拿连接、等可用连接、或者频繁建连,那么 timeout 预算可能在出门前就已经耗掉一截了。

这时现场常会看到:

  • 调用方超时很多
  • 下游收到量却对不上
  • 问题集中在部分实例
  • 高峰时更明显

也就是说,请求不是到了下游以后慢,而是还没稳定发出去。

第二段:代理层或 Sidecar

如果链路里有 Service Mesh、Envoy 或其他代理层,这一段也特别值得优先怀疑。

因为代理层一旦出现局部抖动,会形成一种很迷惑的现象:

  • 调用方 timeout 很明显
  • 下游业务实例并没有同步恶化
  • 问题还可能只集中在部分节点、部分机房

这时候如果你只盯下游应用日志,往往会觉得“什么都没发生”。可实际上请求可能在代理转发、连接复用或局部摘流那里就已经不稳定了。

第三段:回调线程或 EventLoop

还有一种很像“下游不慢但我还是 timeout”的问题,出在响应其实已经回来了,只是调用方没来得及把它处理完。

比如:

  • EventLoop 被阻塞
  • 回调线程池拥堵
  • future / promise 迟迟不能完成

这时从下游看,请求处理完了;从调用方看,却还是等到了 timeout。

所以这类问题最别扭的地方就在这里:不是请求没执行,而是结果没及时回到业务线程眼里。

一个更像现场的判断办法:先对齐“发起量”和“收到量”

如果以后再碰到“RPC timeout,但下游不慢”,我最建议先做的不是翻所有框架细节,而是先对齐一件很朴素的事:

  • 调用方到底发起了多少
  • 下游到底稳定收到了多少

这一步特别值钱。

如果调用方记录的发起量很高,下游实际收到量却明显少一截,那就别再围着“下游执行慢不慢”争了,重点该立刻转向中间层。

因为这已经很像:请求压根没稳定到下游。

只有当两边量能对上,再去比较下游处理期 RT,才更像在同一条线上讨论。

为什么这条线能帮现场少走弯路

因为它把一个很容易失焦的问题重新拉直了。

很多人一看到“RPC timeout + 下游 RT 正常”,就会下意识进入两种无效动作:

  • 不断逼下游再查一遍
  • 或者开始列一大堆可能中间层,结果越列越散

但如果先抓住“请求有没有稳定到下游”这条线,方向会收得很快。

如果没稳定到,下一个重点自然就是:

  • 连接池
  • 代理层
  • 回调线程 / EventLoop

这三段已经能覆盖现场里很大一部分真实问题,而且它们都能解释那个最关键的矛盾:

  • 下游看起来没慢
  • 调用方却真的在 timeout

最后压成一句话

RPC timeout 而下游不慢时,最怕的不是暂时还没定位,而是一开始就默认“请求肯定到了下游,只是下游没承认”。

很多时候,请求预算真正耗掉的地方,在连接池、代理层或回调线程,请求根本没稳定到下游。

先把这件事判断清楚,现场才不会一直在错误的问题上打转。