超时:三层时间都要设
只设一个「总超时」往往不够。通常分三层:连接超时(1–3 秒,用于建立 TCP/TLS)、读取超时(按接口 P99 设定)、整体重试预算(所有重试累计不超过上限)。没有预算约束时,一次抖动可能引发数分钟的重复请求,反而把下游压垮。
重试:只重试「可能成功」的失败
| 失败类型 | 是否重试 | 建议 |
|---|---|---|
| 连接超时、502 / 503 / 504 | 适合 | 指数退避 + 抖动 |
| 429(限流) | 谨慎 | 遵循 Retry-After,并限制次数 |
| 408 请求超时 | 视场景 | 必须配合幂等键 |
| 400 / 401 / 403 / 404 / 422 | 不要 | 请求本身有问题,重试无意义 |
| 500 | 谨慎 | 仅在确认是瞬时故障时 |
指数退避与抖动
// 第 n 次重试的等待时间(毫秒),带随机抖动以避免重试风暴
const base = 200, cap = 10000;
const wait = Math.min(cap, base * 2 ** attempt);
const jitter = Math.random() * wait * 0.3;
await sleep(wait + jitter);
没有抖动的退避会让所有客户端在同一时刻重试,形成重试风暴;抖动把重试分散到一个时间窗口内,显著降低峰值。
幂等:让重试变得安全
幂等指同一请求执行多次与执行一次的结果相同。缺少幂等保证时,重试可能造成重复扣款、重复下单或重复发消息。常见做法:
- 客户端为每次业务操作生成幂等键(如 UUID),随请求一起发送;
- 服务端记录「幂等键 → 首次结果」,重复请求直接返回该结果而不重复执行;
- 幂等键设置有效期(如 24 小时),避免存储无限增长;
- 写接口尽量设计为「设置某值」而非「增加某量」(例如 setBalance 优于 addBalance)。
四个常见坑
- 重试放大流量:没有次数上限与整体预算,一次抖动会被放大成雪崩;
- 多层同时重试:网关层重试 3 次、应用层再重试 3 次,实际请求变成 9 次;
- 对非幂等接口重试:直接造成资金或消息重复;
- 超时设置过长:连接被占满、线程池耗尽,故障扩散得更快。
可观测性与排查
为每次调用带上请求 ID(可用 UUID 生成),并记录尝试次数、耗时与最终状态;出问题时先看「重试次数分布」与「耗时分布」,通常能立刻区分是网络抖动、下游变慢,还是重试策略本身不合理。
动手试试:UUID 生成(生成请求 ID 与幂等键)
超时预算与重试预算
分布式调用中,超时与重试必须当作预算来统一分配,而不是每个服务各拍一个数字。
- 自下而上计算超时:先确定最外层可接受的响应时间,再逐层分配,保证上游超时大于下游超时之和,否则上游会在下游仍在处理时就放弃并重试,放大负载。
- 重试次数要有限且递减:重试次数与退避时间应随层数递减,越靠近入口重试次数越少,避免一次抖动被多层放大成历史级流量。
- 只重试可重试的错误:参数错误、鉴权失败等确定性失败重试没有意义,反而增加负载;只有连接失败、超时与部分服务端错误才值得重试。
- 区分读与写的重试策略:读请求可自动重试;写请求必须依赖幂等键,否则重试会产生重复副作用。
- 记录重试指标:重试率、重试成功率与重试引入的额外延迟都应上报。重试率长期偏高说明依赖本身不稳定,需要从根因解决。
幂等键的设计细节
幂等键应由客户端生成并携带业务语义,服务端在有效期内保存处理结果,重复请求直接返回首次结果。需要注意键的存储成本与过期策略,以及并发情况下同一键的请求要串行化处理,避免两次都执行到一半。