每层都设了 3 秒,然后呢
一次调用穿八层服务,每层超时都设 3 秒——听起来很整齐。最坏情况:第一层等 3 秒发现下游没回来,重试一次又是 3 秒,链路末端的理论等待能堆到几十秒;而入口处的用户,三秒前就已经关掉了页面。上游等不起的时候,下游的一切努力都是浪费——超时不是每层独立的事,是全链路的预算问题。
超时预算:层层衰减
把总超时当成一笔预算往下分:入口给 1 秒,网关留 0.9 秒,核心服务留 0.7 秒,最底层的 DB 查询留 0.2 秒——原则只有一条:下游的超时必须小于上游的剩余时间,否则下游做得再精确,上游早就放弃等待了。更精细的做法是超时传递:每一跳把已消耗的时间传下去,让下游拿到的不是静态值,而是本次请求的剩余预算。
| 层级 | 预算 | 说明 |
|---|---|---|
| 用户端 | 1s | 入口总预算 |
| 网关 | 0.9s | 预留 0.1s 给出口 |
| 核心服务 | 0.7s | 重试必须装进预算 |
| 下游 RPC | 0.3s | 连接与读超时分开设 |
| DB | 0.2s | 慢查询直接掐断 |
重试的三个前提
重试是把双刃剑,开之前确认三件事:幂等——只有读操作和幂等的写操作可以直接重试,非幂等写先做幂等化改造;预算——重试必须装进超时预算,读超时 3 秒配重试 3 次等于没预算;退避——立即重试等于撞枪口,指数退避加随机抖动把重试流量摊开。预算用尽就止损,不要让重试风暴沿着调用链向上滚。
// 指数退避 + 随机抖动
long backoff(int attempt) {
long base = 100L; // 基数 100ms
long exp = base << attempt; // 100, 200, 400 ...
long jitter = (long) (exp * 0.2 * Math.random()); // 20% 抖动
return exp - jitter / 2 + random(jitter);
}
// 重试次数 1~2 次封顶,且必须在剩余预算内执行和熔断的先后关系
重试和熔断是搭配使用的:重试解决偶发抖动,熔断解决持续性故障。次序很关键:重试发生在熔断器之内——失败率统计要包含重试失败的请求,下游真挂了熔断器才能基于真实失败率尽快打开;熔断打开期间重试直接短路,半开状态只放少量探测请求。反过来先重试后熔断,故障期流量先被放大一圈才被熔断,下游死得更快。熔断器的三态模型这里不展开,先记结论:先熔断保护,重试才有意义。
一张默认值清单
给一套能直接抄的基线:连接超时 500ms~1s,建连不该超过 1 秒;读超时按接口 P99 的 2~3 倍定,不要拍脑袋统一 3 秒;重试 1~2 次封顶;退避基数 100ms、抖动 20%;所有参数进配置中心,支持运行时调整——超时是调出来的,不是定出来的。下一篇聊流量进来的第一站:API 网关。
评论 (0)