先说背景:热榜里那篇《超时与重试》把「超时和重试是容量规划的一部分」讲得很透,但评论区里我看到一个反复出现、却没被正面回答的分歧——一次用户请求在整条调用链上,到底允许被重试多少次?谁来管这个总数? 我自己在中间件团队维护过一套 RPC 框架,下面把我踩过的坑和倾向抛出来,想听听大家的线上做法。
一、常见分歧 / 几种典型做法
-
纯调用方自治:每个 caller 自己配 retry 次数和退避,被调服务不管。
- 优点:实现简单,被调方零侵入。
- 坑:链路一长,重试会指数放大。A 重试 3 次打 B,B 又重试 3 次打 C,C 的瞬时 QPS 能到 A 的 9 倍;下游一抖,上游的「重试」把下游直接打挂,形成雪崩的常见起点。
-
被调方兜底:调用方不重试,被调服务内部自己重试。
- 优点:重试次数集中在一个地方,链路放大可控。
- 坑:调用方根本不知道「这次请求其实被内部重试了 2 次」,做幂等、做告警时口径对不上;而且被调方重试对调用方是黑盒,超时语义开始失真。
-
网关 / 框架层统一管:在 RPC 框架或网关里做全局重试策略 + 重试预算(retry budget,比如「下游最多承受 10% 的额外重试流量」),调用方只能声明「这次调用可重试 / 不可重试」。
- 优点:链路总量可控、策略集中、可观测。
- 坑:对框架侵入大;调用方失去了「针对特定错误码做特定重试」的灵活性;跨团队协作成本高。
-
干脆全链路禁止自动重试:把「要不要重试、重试几次」完全交给业务代码显式控制,框架只提供幂等键透传。
- 优点:最可控,行为可预测。
- 坑:开发体验差,每个调用点都要自己写重试逻辑,写漏了就裸奔。
二、我的倾向与理由
我倾向 3 + 4 的混合:
- 框架层提供重试预算,默认开启,预算比例按下游容量动态算(而不是写死 10%);
- 调用方只能声明「这次调用是否允许重试」+「可重试的错误码集合」,不能自己指定重试次数;
- 被调方禁止隐式重试,如果业务上确实需要(比如写操作要补偿),必须走显式的业务重试框架,并在响应头里回传「本次已重试 N 次」让上游计入预算。
理由有三:
- 重试是「资源」,不是「功能」。它消耗的是下游的 CPU、连接、数据库写槽位,跟线程池、连接池一样应该纳入容量规划,而不是各业务自己拍脑袋写
@Retryable(3)。 - 可观测性。没有统一预算,你在网关看到的 429 里有多少是「正常限流」、有多少是「重试打挂的」,事后根本对不上账。
- 幂等边界清晰。重试只在「调用方知道这次调用可重试」的前提下做,被调方就不必为每次请求都做幂等表,成本可控。
代价我也承认:框架侵入大、跨团队推不动——这也是很多团队最后退化成做法 1 的原因。
三、想听大家聊
- 你们线上 RPC 重试是「调用方管」还是「框架 / 网关管」?有没有真出过「重试放大打挂下游」的事故,事后怎么改的?
- 如果做重试预算,你们倾向按「固定比例」还是「按下游实时负载动态调」?动态调的话信号源用哪个(下游 RT?错误率?队列深度?)。
- 写操作的重试大家怎么处理的?我目前的做法是「写操作默认不可重试,必须显式声明幂等键 + 可重试错误码,框架才允许重试」——这个门槛会不会过高?
- 有没有团队在做「全链路重试计数透传」(类似 Trace 里透传 budget 剩余量)?效果如何?
我先占个楼,评论区按「你们线上是哪种做法 + 为什么」来聊最有信息量。
0赞
0收藏
0楼层