从一次线上事故聊起:为什么你的重试机制可能正在放大故障

举报
yd_277754045 发表于 2026/10/28 01:39:12 2026/10/28
【摘要】 从一次线上事故聊起:为什么你的重试机制可能正在放大故障上周三凌晨两点,我被一条告警电话叫醒:核心支付链路错误率飙升到 40%。爬起来看监控,第一反应是某个下游服务挂了。但奇怪的是,下游服务的健康检查全是绿的,QPS 却在几分钟内翻了五倍。最后定位到原因:某个调用方在超时后疯狂重试,而重试又打满了下游的连接池,导致原本健康的请求开始超时,进而触发更多重试。一个原本只影响 1% 请求的偶发超时...

从一次线上事故聊起:为什么你的重试机制可能正在放大故障

上周三凌晨两点,我被一条告警电话叫醒:核心支付链路错误率飙升到 40%。爬起来看监控,第一反应是某个下游服务挂了。但奇怪的是,下游服务的健康检查全是绿的,QPS 却在几分钟内翻了五倍。

最后定位到原因:某个调用方在超时后疯狂重试,而重试又打满了下游的连接池,导致原本健康的请求开始超时,进而触发更多重试。一个原本只影响 1% 请求的偶发超时,被重试机制放大成了一场雪崩。

这件事让我想聊聊重试这个看起来简单、实际上坑很深的话题。

重试的本质:一个被低估的放大器

大多数人对重试的理解是"失败了再试一次,提高成功率"。这个直觉在单请求视角下没错,但在系统视角下,重试是一个流量放大器。

假设下游有 10% 的请求超时,如果每个失败请求重试 3 次,那么下游实际承受的流量会变成:

正常流量 + 失败流量 × 重试次数 = 1 + 0.1 × 3 = 1.3 倍

看起来还好。但如果下游已经开始过载,超时率上升到 50%,同样的重试策略会让流量变成:

1 + 0.5 × 3 = 2.5 倍

这就是正反馈循环的起点:下游越慢 → 超时越多 → 重试越多 → 下游越慢。学术界管这叫"重试风暴"(retry storm),是分布式系统里最经典的故障放大模式之一。

三个必须做对的设计决策

1. 退避策略:别用固定间隔

最糟糕的重试实现是 sleep(1) 然后重试。固定间隔意味着所有失败的请求会在同一时刻再次涌向下游,形成周期性的流量尖峰。

指数退避(exponential backoff)是标配,但很多人只做了指数增长,忽略了抖动(jitter)。没有抖动的指数退避,会让同一批请求的重试时间点高度对齐,退避了个寂寞。

一个简单有效的做法是 full jitter:

def backoff(attempt, base=0.1, cap=10):
    # 指数增长 + 全抖动
    return random.uniform(0, min(cap, base * (2 ** attempt)))

random.uniform(0, ...) 把重试时间点打散到整个区间,这是 AWS 那篇著名的《Exponential Backoff and Jitter》里推荐的做法,实测对削峰效果显著。

2. 重试预算:给重试设一个总闸

单个请求的重试次数上限是不够的。真正需要控制的是全局重试比例。

思路很简单:维护一个滑动窗口,统计最近一段时间内"首次请求"和"重试请求"的数量。当重试请求占比超过阈值(比如 10%),就直接拒绝新的重试,让失败快速暴露给上游。

这就是所谓的 retry budget。它的价值在于:正常情况下重试能提升成功率,异常情况下重试不会成为压垮下游的最后一根稻草。gRPC 的 retryThrottling 和 Envoy 的 retry_budget 都是这个思路。

3. 只重试"值得重试"的错误

不是所有失败都该重试。一个 400 Bad Request 重试一百次还是 400,只会浪费资源。需要区分:

  • 可重试:超时、连接拒绝、502/503/504、限流(429,但要配合 Retry-After)
  • 不可重试:4xx 客户端错误、业务逻辑错误、幂等性无法保证的写操作

最后一点尤其关键。非幂等操作的重试是事故的常见来源。一个"创建订单"的请求超时了,你不知道是没创建成功还是创建成功但响应丢了。盲目重试可能产生重复订单。这类场景要么用幂等键(idempotency key)保证安全,要么干脆不重试、交给上层补偿。

一个容易被忽略的细节:重试的位置

重试发生在哪一层,效果天差地别。

如果 SDK、网关、服务框架、业务代码各自都有一层重试,那么一次请求实际可能被放大 3 × 3 × 3 = 27 倍。这种"重试叠加"在微服务架构里非常隐蔽,因为每一层单独看都配置得很合理。

实践中的建议是:重试尽量收敛到一层,通常在客户端 SDK 或服务网格的出口侧统一处理,业务代码里显式声明的重试要能被观测到。至少要做到:所有重试都打上标记(比如 x-retry-count header),在链路追踪里能一眼看出这次请求被重试了几次。

回到那次事故

我们最后的修复方案其实不复杂:

  1. 把固定间隔重试改成带 jitter 的指数退避
  2. 加了一个全局重试预算,重试占比超过 15% 就熔断重试
  3. 在网关层统一了重试策略,干掉业务代码里散落的三处重试
  4. 给所有重试请求加了 header 标记,接入监控

改完之后,同样的偶发超时再也没能演变成雪崩。下游 QPS 在故障期间的峰值从 5 倍降到了 1.3 倍。

小结

重试是一个典型的"默认直觉会骗人"的设计。它的收益在单请求视角下很直观,但它的风险在系统视角下才会显现。写重试代码的时候,值得多问自己几个问题:

  • 这个操作幂等吗?
  • 失败是暂时的还是永久的?
  • 如果下游已经过载,我的重试会让它更糟吗?
  • 系统里有几层重试在叠加?

想清楚这几个问题,比抄一段重试模板代码重要得多。


如果这篇对你有帮助,欢迎讨论。你们线上遇到过重试相关的事故吗?

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。