重试不是万能药:一次线上故障里被放大的三倍流量

举报
yd_261373198 发表于 2026/11/02 09:51:04 2026/11/02
【摘要】 从一次数据库主从切换事故出发,讲清重试放大的乘性效应、瞬时/永久/不确定三类失败的区分、幂等实现的三种手段,以及退避抖动与时间预算的工程细节。

重试不是万能药:一次线上故障里被放大的三倍流量

## 结论先行

重试(retry)是分布式系统里最容易被滥用的"可靠性手段"。它看起来只是把失败的操作再执行一次,代价极低。但在真实故障中,重试往往不是缓解问题,而是把一次局部抖动放大成一次全局雪崩。判断该不该重试,关键不在"操作失败了没有",而在两个问题:这个操作是否幂等,以及失败的原因是否可能在几百毫秒内自行消失。

## 故障是怎么被放大的

一次典型的链路是这样的:数据库主节点做了一次 12 秒的故障切换,所有连接同时报错。上层服务的客户端 SDK 默认配置是"失败重试 3 次、退避 100 毫秒"。一万个并发请求,在切换窗口内每个都尝试 4 次,瞬时写入量变成原来的 4 倍。

主从切换完成后,新的主节点在几十秒内被这波积压流量打满 CPU,慢查询排队,于是又触发了新一批超时——新的超时又触发新的重试。系统进入自激振荡:故障本身只持续了 12 秒,服务恢复却花了 20 分钟。

这里有两个细节值得记住:

第一,重试的放大不是线性的,是乘性的。客户端重试、网关重试、服务内部 RPC 重试、消息队列消费重试,四层各重试 3 次,最坏情况是 81 倍的请求量。每一层都"只重试了三次",看起来都很克制。

第二,重试会掩盖真实故障率。上线后监控里看到的成功率是 99.5%,实际底层资源的失败率可能是 40%。运维据此判断"服务还健康",把真正的容量问题拖到下一次大促才集中爆发。

## 先分清三类失败

不是所有失败都值得重试。我的划分是三档:

- **瞬时失败**:连接被重置、连接池瞬时耗尽、Leader 切换中的 503。这类失败的特征是原因会自行消失,且通常耗时在毫秒到秒级。可以重试,但必须带退避和抖动。
- **永久失败**:参数校验不通过、账号无权限、资源已被删除。重试一万次结果都一样,只会额外浪费对端资源。
- **不确定失败**:请求发出去了,但响应超时,不知道对方执行了没有。这一类最危险——它决定了重试是否会造成重复扣款、重复发货、重复建单。

真正需要工程投入的是第三类。前两类只需要正确分类错误码:只有网关层的 502/503/504、连接异常、以及明确的"请稍后重试"才进重试分支,业务错误码一律不重试。

## 幂等是重试的前提

对不确定失败做重试,必须有幂等保证。工程上可落地的手段有三种:

1. **业务唯一键 + 唯一索引**。下单请求带客户端生成的 requestId,服务端在事务里插入一张去重表,唯一索引冲突就直接返回上一次的结果。这是最可靠的一种,因为它把幂等性交给了数据库而不是应用逻辑。
2. **状态机版本的乐观锁**。更新时带上 `WHERE status = 'PENDING'`,受影响行数为 0 说明已被别人处理,直接幂等返回。
3. **幂等令牌**。网关在接收请求时生成令牌并缓存结果,同一令牌在有效期内直接回放缓存结果。

反过来,如果做不到幂等,那么唯一正确的做法是不重试,把失败暴露给调用方,由业务决定补偿——比如人工对账、T+1 冲正。这比偷偷重试然后留下脏数据好得多。

## 退避与抖动:细节决定成败

指数退避是标配,但很多人漏掉了抖动(jitter)。如果所有客户端都按 100ms、200ms、400ms 的固定节奏重试,它们会在同一时刻同时醒来,形成周期性的尖峰,这就是所谓的重试风暴(thundering herd)。给退避加一个随机因子——比如在 `[0.5, 1.5]` 倍之间随机——能把尖峰摊平成一条平缓的曲线。

另一个常被忽视的参数是重试的时间预算。重试的总耗时应该被一个 deadline 约束:如果一次调用整体预算 2 秒,那么第一次失败后剩余时间不足 300 毫秒时,再次重试只会让上游等得更久,最终双方都超时。正确做法是把截止时间沿调用链向下传递,每一层都按剩余预算决定是否还值得重试。

## 什么时候该"重试"之外的方案

如果某个操作的失败率长期偏高,说明重试只是在掩盖系统性缺陷。这时候应该看三个地方:

- **连接池与超时配置**。大量"连接超时"往往不是网络问题,而是连接池太小、单次查询太慢。把连接池从 20 调到 100 比加重试有效得多。
- **对端容量**。失败集中在某个分片或某台机器时,多半是单点过载,重试会把它压得更死,需要的是负载均衡和限流。
- **重试的观测**。必须把"首次失败率"和"重试后成功率"分开打点。只看后者,你永远不知道系统里藏了多少被掩盖的失败。

## 一份可以照抄的检查清单

上线任何带重试的代码前,我会问自己五个问题:

1. 这个错误码属于瞬时失败吗?不是就绝不重试。
2. 这个操作幂等吗?不幂等且有副作用,就不重试,走补偿。
3. 退避有抖动吗?总量有截止时间约束吗?
4. 重试次数是几层叠加的?把链路里的每一层都数一遍,算最坏的放大倍数。
5. 首次失败率有没有单独监控?告警是挂在首次失败还是最终失败上?

这五条里最容易被跳过的是第四条。真正的容量事故很少来自某一层写了夸张的重试次数,而是来自五层各自"就重试一下"的默契。系统地数一遍,往往是排查性能问题最快的入口。

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

评论(0)

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

全部回复

上滑加载中

设置昵称

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

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

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