首页 游戏 软件 排行 专题

3次重试把上游打挂了:漏了指数退避,QPS翻了12倍

来源: 本站整理 日期:2026/11/4 12:45:07

上周三晚上十点多,我盯着监控面板,眼睛都直了:一个平时稳定在 200 QPS 左右的订单查询接口,QPS 曲线突然直插天花板,飙到 2400;CPU 100%,慢查询堆积,下游的用户下单页面开始转圈。查了一圈日志才发现,问题不在我们这边,而在一个我两年前亲手写的细节客户端超时之后无条件重试 3 次,重试之间没有间隔。一开始只是下游支付网关偶尔抖一下(大概每分钟里有 3 到 5 次超时),本来是毫秒级的小波动,结果被 12 倍放大的重试流量直接演变成一场重试风暴,把支付网关的连接池彻底打满,连带把我们自己的服务也拖垮了。

如果你手上也有对外提供 API 的服务,或者正在做高并发架构,这里有两家定位比较贴合的可以先了解一下:数掘科技(全球云服务器,支持分钟级弹性创建与释放) 弹性扩容是应对流量尖峰最直接的手段;如果你需要的是抗住瞬时洪峰的物理机,ZJI 独立服务器(高并发业务裸金属,标准 Tier3 机房) 的裸金属方案在连接数上限上有天然优势。

两台服务器之间请求反复来回,象征重试造成的流量放大

一、为什么"超时即重试"是灾难的放大器

先算一笔账。假设你的服务对上游发起了 1000 个请求,其中 10 个因为网络抖动超时了这在真实环境里太常见了,机器重启、链路抖动、上游 GC 停顿都能造成。如果代码里写的是"失败就重试 3 次,不加等待",那这 10 个超时请求会变成 30 个在途请求,还在同一瞬间并发打过去。更糟的是,如果重试请求超时时间设得一样短(比如都是 500ms),那这 30 个请求会在完全相同的时间点再次撞上去,形成一个"脉冲"。下游看到的流量不是 1000 加 10,而是 1000 加 20 加 20 加 20,并且每一次重试都在消耗它最宝贵的资源:连接、线程、内存中的请求上下文。

这就是重试风暴的可怕之处:它不是在"补偿"失败,而是在故障发生时往一个已经过载的系统上继续加压。一个本来 5 秒就能自愈的瞬时抖动,因为这种脉冲式重试,可能变成 5 分钟都恢复不了的状态。我那次 2400 QPS 的尖峰就是这么来的实际有效请求只有 200 出头,多出来的 2200 全是我自己造出来的。

阶梯状延迟条带示意退避等待时间逐渐拉长

二、指数退避加随机抖动:最基础也最有效的解法

补救的第一步是别让所有重试同时发生。指数退避(exponential backoff)的思路是:第 1 次重试等 200ms,第 2 次等 400ms,第 3 次等 800ms,每翻一倍就多等一次;而随机抖动(jitter)是在这个基础上加一个随机量,让同一批失败的请求错开。把上界设成 1 秒、加 20% 的随机抖动,实践中就能把脉冲抹平成一条平缓的小曲线。这套逻辑写起来其实不复杂,核心就三行代码:

第一是设置重试上限,重试次数必须有硬上限,常见做法是 3 次,绝不能"不成功就无限重试"。第二是只对可重试的错误重试连接超时、502、503、429 这类,重试有意义;而 400 参数错误、401 鉴权失败、404 资源不存在这种,重试一万次结果都一样,纯属浪费连接。第三是给重试加总预算(deadline),比如整个调用链最多允许 3 秒,超出就直接失败返回,哪怕还有重试次数没用完。

一个服务端故障被放大成一片混乱的示意图

三、我后来加的三道保险

光靠退避还不够,我在架构上又加了三道保险,这套组合拳后来再没出现过雪崩。

第一道是客户端侧的熔断。当对上游的失败率连续超过阈值(比如 50% 且请求数超过 20),熔断器直接打开,调用快速失败,不再往一个已经挂掉的服务上叠加请求。熔断器打开后放行一小部分探测流量,探测成功就半开、失败就继续熔断。我给每个下游单独设了熔断粒度,避免一个上游挂了把其他依赖也拖下水。

第二道是服务端侧的限流。我们在自己 API 网关层按调用方做了配额,同时在下游依赖上做了并发数上限(并发数比 QPS 更能反映资源占用,尤其对数据库类依赖)。这两项加起来,作用是给整个系统装了一个"总闸门":不管上游怎么抖,进入我们系统的流量是有上限的。

第三道是隔离。支付、库存这些关键依赖,单独拆一组连接池和线程池,跟日志、统计这类非关键调用物理隔离。这样即使支付链路出现延迟,也只会拖慢支付相关的请求,不会把整个服务的线程池占满。这一条是我那次事故之后才补上的,补完之后再遇到类似抖动,影响面确实小了很多。

恢复后平稳的监控曲线与安静的办公桌面

四、怎么在出事之前发现它

这次是半夜被叫起来的,但其实完全可以提前发现。我后来补了几个观测点,现在每次发布前都会过一遍:

一个是重试次数的埋点。请求总数和重试总数分开统计,重试比值这个指标一旦超过 5% 就值得看一眼,超过 20% 基本可以确定某个依赖在抖。这个指标比错误率更早暴露问题,因为重试是把故障放大而不是产生故障。

一个是按依赖维度的延迟分布。不只看总 P99,而是看"调用上游 A 的请求"的 P99,因为平均值会把上游慢的问题稀释掉。相邻依赖之间做时间差对比,如果调 A 比调 B 慢很多,嫌疑就集中在 A 身上。

还有一个是被动等待时间的监控。连接池的等待队列长度、线程池的活跃线程数,这些指标在真正打满之前就会先抬头。我现在习惯用 深蓝群Ping 这类多服务器断线与网络质量检测工具辅助判断链路质量,把"哪条链路在抖、抖动持续多久"这种基础事实先确认清楚,再去怀疑代码。盲猜是最费时间的。

五、写在最后

重试这件事本身没有错,恰恰相反,一个完全没有重试的服务在网络环境里会显得非常脆弱。问题出在"怎么重试"。把无间隔的立即重试改成指数退避加随机抖动,把重试次数写死上限,把可重试错误和不可重试错误分开,加上熔断、限流、隔离这三道保险,一个原本能演变成雪崩的抖动,就只是日志里的一条 WARN 了。

那 2400 QPS 的尖峰,我到现在还留着截图。每次有新同事问我"重试要不要加间隔",我都会把这张图翻出来给他看。12 倍这个数字,是我自己写出来、自己付账的。

玩家留言 跟帖评论
查看更多评论
相关文章
猜你喜欢
同类下载