【背景】 最近热榜上有两篇让我一直琢磨的帖子:一篇是连接池 P99 抖动排查(186 赞,285 条评论),另一篇是超时与重试(69 赞)。评论区里反复出现一种配置姿势:客户端超时 200~500ms + 自动重试 2~3 次 + 指数退避。我做了 10 年分布式,越看越觉得这套"标配"在部分故障场景下不是止血,而是放血。
拿一个具体数字说话:下游服务稳态 P99 = 180ms,上游超时设 200ms、重试 2 次。平时 99.9% 的请求都安全;但下游一次 GC 抖到 300ms,会发生什么——
- 上游 200ms 熔断,发起重试;
- 重试请求到达时,下游第一批请求还在执行(连接池里的连接被占着,线程还在跑 SQL);
- 下游负载瞬间变成 1 + 2 = 3 倍,P99 进一步恶化;
- 上游第二次超时,第三次重试再来…… 按 3 次尝试算,下游瞬时并发是稳态的 3 倍;如果上游 10 个服务都这么配,故障半径就放大了 30 倍。这就是典型的"孤儿工作 + 重试放大"复合故障,而且它不需要下游宕机,一次 GC 就能触发。
【常见分歧 / 几种做法】 我见过四种流派,各自的默认配置差异极大:
- 激流派:短超时(100~200ms)+ 重试 3 次 + 指数退避(200ms/800ms/3.2s)。理由是"快速失败比慢速成功好"。
- 保守派:长超时(2~5s)+ 不重试,靠上游排队。理由是"重试会把下游打死"。
- 熔断派:超时 500ms + 重试 1 次 + 熔断器(错误率 > 50% 熔断 10s)。
- 预算派:一次业务请求总预算 2s,按 N 个下游切片,每片只允许 1 次重试,且只在幂等接口上重试。 四派各有各的事故故事,也各有各的"我们就是这么做才没炸"的故事。
【我的倾向与理由】 我倾向预算派,核心是三条硬约束:
- 重试必须幂等:写接口默认不重试,读接口重试前必须能证明幂等(GET 天然幂等,POST 要业务层去重键)。
- 总预算守恒:所有下游超时之和 ≤ 业务总预算 × 0.8(留 20% 给网络抖动),单下游重试只允许消耗剩余预算,不额外扩容。
- 熔断优先于重试:错误率阈值我习惯设 50%(不是 10%,10% 太灵敏,GC 一次就误触发),熔断窗口 10s,半开状态只放 1 个探测请求。 这三条的共同点是:把"重试"从"客户端的本地决策"升级为"链路的容量决策",任何一次重试都要问一句"下游还吃得下吗",而不是无脑发出去。 具体到配置(Spring Cloud 2022 + Resilience4j 2.x,我们线上的实际值):
- timeout: 500ms(读)/ 1000ms(写)
- retry: maxAttempts=2(仅读接口),intervalFunction=200ms * 2^attempt,jitter=0.2
- circuitBreaker: failureRateThreshold=50, slidingWindowSize=20, waitDurationInOpenState=10s
- 业务总预算:一个聚合接口 2s,5 个下游切片各 400ms,串行降级为并行 + CompletableFuture.orTimeout
【想听大家怎么选】 三个具体问题,按你们场景答:
- 你们线上写接口(非幂等)到底重试不重试?如果重试,幂等性怎么保证——业务去重键、还是数据库唯一索引、还是中间件层去重表?
- 熔断阈值你们设多少?我见过 10% / 25% / 50% / 75% 四种,背后的事故故事分别是什么?
- 当"重试放大"和"不重试导致上游超时"发生冲突时(下游 P99 正好卡在超时线附近),你们怎么取舍?有没有人真的做过"把超时调到下游 P99 的 2 倍、同时砍掉重试"的实验,数据如何?
欢迎贴你们的生产配置和事故复盘,数字越具体越好。
0赞
0收藏
0楼层