重试风暴把慢接口进一步放大时,怎么区分表象和根因?
最危险的重试风暴,起点往往不是流量突然翻倍,而是某一段等待先变长,随后同一批请求被多打了几遍。把原始请求量、实际调用量、超时边界和重试层级对齐,才能看清是谁先点火,谁把火越吹越大。
重试风暴最麻烦的地方,是它会把一场本来还能控住的慢请求事故,改写成“系统突然像被流量打穿了”。
18:22,入口订单确认接口 QPS 只从 1200 涨到 1340,看起来不算夸张;18:26,同一个库存下游的实际调用量却已经冲到 4800。短短 4 分钟里,入口流量没翻几倍,内部调用量先翻了近 4 倍。
当时的关键对照是这张表:
| 时间 | 入口原始请求量 | 库存实际调用量 | 平均每个请求调用次数 | timeout 分布 | 当时现场 |
|---|---|---|---|---|---|
| 18:22 | 1200 qps | 1260 qps | 1.05 | 零散 | 基本正常 |
| 18:24 | 1280 qps | 1960 qps | 1.53 | 800ms 左右开始聚集 | 轻微变慢 |
| 18:25 | 1310 qps | 3180 qps | 2.43 | 800ms、1600ms 两个峰 | 上游重试明显介入 |
| 18:26 | 1340 qps | 4800 qps | 3.58 | 超时、504、拒绝一起抬头 | 已进入放大阶段 |
这张表一出来,就知道不该再问“是不是业务流量突然大了”,而要问两件更具体的事:
- 最开始到底是哪一段先慢了。
- 现在把系统往下拖的,已经有多少是重试本身造成的额外压力。
先确认涨的是“人流”,还是“每个人被多检查了几遍”
很多现场一看到下游 QPS 暴涨,就会自动联想到流量洪峰。重试风暴不是。
它最典型的特征,是入口原始请求量变化不算离谱,但内部实际调用量成倍增长。
所以我第一眼最想看的是两个数字:
- 入口原始请求量。
- 某个关键下游的实际调用量。
如果两者一起涨,而且倍数差不大,更像真实业务上涨;如果入口只涨了 10% 到 20%,内部调用量却翻倍甚至翻几倍,那大概率是重试、代理重放、SDK 自动重试已经一起上来了。
找“第一下掉速”比找“最后哪里最红”更重要
重试风暴经常让最后的监控长相非常吓人:
- 线程池满。
- 连接池紧。
- 504 上升。
- 超时率飙高。
- 下游调用量爆炸。
但这里面不全是起点。
那次库存事故里,最早的分叉其实发生在 18:23 左右:库存接口 p95 从 90ms 升到 420ms,还没到全面 timeout,只是慢了。真正把局面拖坏的,是后面这条链:
- 下游开始变慢,接近调用方 800ms 阈值。
- 上游 SDK 触发一次同步重试。
- 同一个请求变成 2 次甚至 3 次调用。
- 下游线程、连接、事务占用时间变长。
- 原本只是“有点慢”的下游,被额外调用量压成全面超时。
所以重试风暴里最关键的判断,从来不是“有没有重试”,而是:最开始那段慢链在前,还是重试已经先成了主压力量。
哪些迹象说明“根因在前,重试在后”
更像这种局面的信号通常有:
- 在调用量明显放大之前,下游 RT、锁等待、慢 SQL 已经先抬头。
- timeout 先集中在某个真实慢链附近,而不是一开始就大面积失败。
- 降低重试后,调用量立刻回落,但下游 RT 仍然偏高,只是没那么夸张了。
这说明重试很重要,但它更像放大器。真正那一下起火,还是前面已有一段慢链先出现了。
哪些迹象说明“重试已经从放大器变成主要压力”
也有一些现场,后面真正把系统压垮的,已经不是最初那一下慢,而是重试本身:
- 入口请求量变化不大,内部调用量却持续倍增。
- timeout 明显聚集在 500ms、800ms、1s、2s 这类统一阈值边界。
- 降低重试次数、关闭代理重试后,线程池、连接池、504 很快整体回落。
- 同一条请求在 tracing 里出现多段几乎重复的调用链。
到了这一步,重试已经不只是“陪衬”,而是当前主要额外压力来源。你继续只盯最初那一下慢链,系统未必撑得住。
现场里最值钱的四份证据
1. 原始请求量 vs 实际调用量
没有这个对照,根本判断不了是不是“每个请求被打了更多遍”。
2. timeout 的分布形状
如果超时大量卡在统一阈值附近,说明重试和 timeout 配置正在主导现场长相,而不是纯粹自然抖动。
3. 重试层级
要看清是不是多层叠加:
- 业务代码自己重试
- HTTP / RPC client 自动重试
- 网关 / 代理重放
- 消息补偿或任务补偿
一层两次、另一层两次,看起来都不大,叠起来就可能把一次请求放成四五次。
4. 资源池回落顺序
降低重试后,如果先掉的是调用量、线程池队列、连接池 pending,说明重试确实在制造主要额外压力;如果这些回落有限,而下游 RT 仍然极高,那说明根部的慢链还在。
止血动作最好按“减放大”来做,不要一刀切
重试问题最容易走两个极端:
- 完全不敢动重试,怕成功率更差。
- 所有重试一刀切全关,结果把可恢复的抖动也一起放弃了。
更稳的顺序通常是:
- 先找出最贵、最同步、最会占住线程的那层重试。
- 先降这一层的次数或并发,而不是所有层一起砍。
- 保留真正有意义的幂等重试,把最会形成堆叠的同步重试压下来。
- 同步观察调用量倍率、线程池队列、连接池
pending、下游 RT 的回落顺序。
比如那次库存事故里,我们先关掉了网关侧一次自动重放,再把 SDK 从“超时后再试 1 次”改成“只在连接建立失败时重试”。10 分钟内,库存实际调用量从 4800 掉回 1900,线程池队列和 504 也跟着明显回落。
这一步很说明问题:最初那一下慢链还在,但真正把火吹大的风已经被压住了。
重试风暴里,最怕的不是失败,而是误把“放大结果”当“原始故障”
如果你盯着 18:26 那一刻的监控,很容易以为:
- 线程池就是根因。
- 连接池就是根因。
- 流量突然暴涨就是根因。
可把 18:22 到 18:26 的倍率和时间线拉出来后,故事就完全不同:最初只是库存接口变慢;真正把局面拖成系统级事故的,是一层层同步重试把每个请求又多打了几遍。
这也是为什么重试风暴总该分成两段看:
- 谁先点火。
- 谁把火越吹越大。
这两段经常不是同一个角色。
如果你现在要往别处接
- 还没分清 timeout 更像落在应用、网络还是下游,去看接口超时增多时,先区分应用、网络还是下游依赖?。
- 现在争论的是客户端超时、网关 504、应用 timeout 谁先报,去看网关 504、应用超时、客户端超时,到底谁先超时?。
- 你已经进入止血和根因分界的事故阶段,也可以直接切去超时风暴那条线。
重试本来是为了给抖动留余地,但一旦它把同一批请求反复压回已经变慢的链路里,保护动作就会变成放大动作。
把“原始请求量、实际调用量、超时边界、重试层级”这四件事对齐之后,你通常就能看清:哪里是第一下慢,哪里是后来把故障吹成风暴的那股力。