Java

重试风暴把慢接口进一步放大时,怎么区分表象和根因?

最危险的重试风暴,起点往往不是流量突然翻倍,而是某一段等待先变长,随后同一批请求被多打了几遍。把原始请求量、实际调用量、超时边界和重试层级对齐,才能看清是谁先点火,谁把火越吹越大。

  • 重试
  • 接口超时
  • 性能排查
  • 稳定性
  • Java
16 分钟阅读

重试风暴最麻烦的地方,是它会把一场本来还能控住的慢请求事故,改写成“系统突然像被流量打穿了”。

18:22,入口订单确认接口 QPS 只从 1200 涨到 1340,看起来不算夸张;18:26,同一个库存下游的实际调用量却已经冲到 4800。短短 4 分钟里,入口流量没翻几倍,内部调用量先翻了近 4 倍。

当时的关键对照是这张表:

时间入口原始请求量库存实际调用量平均每个请求调用次数timeout 分布当时现场
18:221200 qps1260 qps1.05零散基本正常
18:241280 qps1960 qps1.53800ms 左右开始聚集轻微变慢
18:251310 qps3180 qps2.43800ms、1600ms 两个峰上游重试明显介入
18:261340 qps4800 qps3.58超时、504、拒绝一起抬头已进入放大阶段

这张表一出来,就知道不该再问“是不是业务流量突然大了”,而要问两件更具体的事:

  1. 最开始到底是哪一段先慢了。
  2. 现在把系统往下拖的,已经有多少是重试本身造成的额外压力。

先确认涨的是“人流”,还是“每个人被多检查了几遍”

很多现场一看到下游 QPS 暴涨,就会自动联想到流量洪峰。重试风暴不是。

它最典型的特征,是入口原始请求量变化不算离谱,但内部实际调用量成倍增长。

所以我第一眼最想看的是两个数字:

  • 入口原始请求量。
  • 某个关键下游的实际调用量。

如果两者一起涨,而且倍数差不大,更像真实业务上涨;如果入口只涨了 10% 到 20%,内部调用量却翻倍甚至翻几倍,那大概率是重试、代理重放、SDK 自动重试已经一起上来了。

找“第一下掉速”比找“最后哪里最红”更重要

重试风暴经常让最后的监控长相非常吓人:

  • 线程池满。
  • 连接池紧。
  • 504 上升。
  • 超时率飙高。
  • 下游调用量爆炸。

但这里面不全是起点。

那次库存事故里,最早的分叉其实发生在 18:23 左右:库存接口 p95 从 90ms 升到 420ms,还没到全面 timeout,只是慢了。真正把局面拖坏的,是后面这条链:

  1. 下游开始变慢,接近调用方 800ms 阈值。
  2. 上游 SDK 触发一次同步重试。
  3. 同一个请求变成 2 次甚至 3 次调用。
  4. 下游线程、连接、事务占用时间变长。
  5. 原本只是“有点慢”的下游,被额外调用量压成全面超时。

所以重试风暴里最关键的判断,从来不是“有没有重试”,而是:最开始那段慢链在前,还是重试已经先成了主压力量。

哪些迹象说明“根因在前,重试在后”

更像这种局面的信号通常有:

  • 在调用量明显放大之前,下游 RT、锁等待、慢 SQL 已经先抬头。
  • timeout 先集中在某个真实慢链附近,而不是一开始就大面积失败。
  • 降低重试后,调用量立刻回落,但下游 RT 仍然偏高,只是没那么夸张了。

这说明重试很重要,但它更像放大器。真正那一下起火,还是前面已有一段慢链先出现了。

哪些迹象说明“重试已经从放大器变成主要压力”

也有一些现场,后面真正把系统压垮的,已经不是最初那一下慢,而是重试本身:

  • 入口请求量变化不大,内部调用量却持续倍增。
  • timeout 明显聚集在 500ms、800ms、1s、2s 这类统一阈值边界。
  • 降低重试次数、关闭代理重试后,线程池、连接池、504 很快整体回落。
  • 同一条请求在 tracing 里出现多段几乎重复的调用链。

到了这一步,重试已经不只是“陪衬”,而是当前主要额外压力来源。你继续只盯最初那一下慢链,系统未必撑得住。

现场里最值钱的四份证据

1. 原始请求量 vs 实际调用量

没有这个对照,根本判断不了是不是“每个请求被打了更多遍”。

2. timeout 的分布形状

如果超时大量卡在统一阈值附近,说明重试和 timeout 配置正在主导现场长相,而不是纯粹自然抖动。

3. 重试层级

要看清是不是多层叠加:

  • 业务代码自己重试
  • HTTP / RPC client 自动重试
  • 网关 / 代理重放
  • 消息补偿或任务补偿

一层两次、另一层两次,看起来都不大,叠起来就可能把一次请求放成四五次。

4. 资源池回落顺序

降低重试后,如果先掉的是调用量、线程池队列、连接池 pending,说明重试确实在制造主要额外压力;如果这些回落有限,而下游 RT 仍然极高,那说明根部的慢链还在。

止血动作最好按“减放大”来做,不要一刀切

重试问题最容易走两个极端:

  • 完全不敢动重试,怕成功率更差。
  • 所有重试一刀切全关,结果把可恢复的抖动也一起放弃了。

更稳的顺序通常是:

  1. 先找出最贵、最同步、最会占住线程的那层重试。
  2. 先降这一层的次数或并发,而不是所有层一起砍。
  3. 保留真正有意义的幂等重试,把最会形成堆叠的同步重试压下来。
  4. 同步观察调用量倍率、线程池队列、连接池 pending、下游 RT 的回落顺序。

比如那次库存事故里,我们先关掉了网关侧一次自动重放,再把 SDK 从“超时后再试 1 次”改成“只在连接建立失败时重试”。10 分钟内,库存实际调用量从 4800 掉回 1900,线程池队列和 504 也跟着明显回落。

这一步很说明问题:最初那一下慢链还在,但真正把火吹大的风已经被压住了。

重试风暴里,最怕的不是失败,而是误把“放大结果”当“原始故障”

如果你盯着 18:26 那一刻的监控,很容易以为:

  • 线程池就是根因。
  • 连接池就是根因。
  • 流量突然暴涨就是根因。

可把 18:22 到 18:26 的倍率和时间线拉出来后,故事就完全不同:最初只是库存接口变慢;真正把局面拖成系统级事故的,是一层层同步重试把每个请求又多打了几遍。

这也是为什么重试风暴总该分成两段看:

  • 谁先点火。
  • 谁把火越吹越大。

这两段经常不是同一个角色。

如果你现在要往别处接

重试本来是为了给抖动留余地,但一旦它把同一批请求反复压回已经变慢的链路里,保护动作就会变成放大动作。

把“原始请求量、实际调用量、超时边界、重试层级”这四件事对齐之后,你通常就能看清:哪里是第一下慢,哪里是后来把故障吹成风暴的那股力。