超时重试的正反馈环路:上游 timeout=3s + retry=2,下游 P99 在 2.5s 附近时重试把 QPS 放大 3 倍 → P99 进一步恶化 → 更多超时,这个「重试放大倍数」你们线上有真实数据吗?怎么打断?
该问题的正文与回答。
背景
我在一个 20 人后端团队做 SRE,核心链路是「网关 → 订单 → 库存 → 支付」,跑在 K8s 1.28 上。上游(订单服务)对下游(库存服务)的 HTTP 调用配置是 timeout=3000ms, maxRetries=2, backoff=固定 200ms。库存服务依赖 MySQL + Redis,稳态 P99 ≈ 2200ms,偶发毛刺到 2800–3200ms(慢 SQL + 连接池排队)。
现象
去年 11 月一次复盘里我拉了 30 分钟的链路日志,发现一个让我很不舒服的模式:
- t=0:库存服务 P99 从 2200ms 爬到 2600ms(一条慢 SQL 开始排队);
- t=200ms:订单服务第一批请求超时(>3000ms),触发 retry #1;
- t=400ms:retry 请求到达库存服务,QPS 从 800 瞬时涨到 ~1200(放大 1.5 倍);
- t=600ms:库存服务连接池排队加剧,P99 从 2600ms 进一步恶化到 3400ms;
- t=800ms:订单服务 retry #2 到达,QPS 冲到 ~1600(放大 2 倍);
- t=1s:库存服务 P99 突破 4s,连 retry 请求本身也开始超时;
- t=1.5s:订单服务线程池被占满,开始拒绝新请求,上游开始 502。
整个事件从"一条慢 SQL"到"上游 502"用了不到 2 秒。事后看,不是下游不够快,是上游的重试把下游推过了临界点。
已尝试的排查与结果
- 加了指数退避(200ms → 400ms → 800ms)+ 随机抖动(±100ms):能缓解,但 t=800ms 时 retry 请求还是集中到达,QPS 峰值从 1600 降到 1300,P99 恶化幅度从 4s 降到 3.2s。有帮助但没根治。
- 把 retry 从"全量重试"改成"只重试幂等接口":砍掉了约 30% 的无效重试,但核心下单接口本身就是幂等的,效果有限。
- 看了热榜上《超时与重试》那篇文章(id=23)里提到的"重试预算"概念:方向对,但文章里没有给出"重试放大倍数"的量化方法。
- 看了《连接池不是越大越好》(id=18)里 Little 定律的分析:确认了"扩池解决不了排队,只会让更多请求一起排队",但没覆盖"重试本身制造了额外排队"这个维度。
我卡住的具体问题
-
量化:你们线上有没有方法实时观测"当前时刻的重试放大倍数"(= 实际到达下游的 QPS / 用户原始 QPS)?我现在的做法是拿下游 Ingress 的 QPS 除以网关入口的 QPS,但这包含了所有来源的流量,不干净。有没有更精确的埋点方案?
-
策略设计:打断正反馈环路,"全局重试预算"(比如"同一用户同一时刻最多 1 个在途请求")和"服务端自适应限流"(下游发现 QPS 异常升高时主动拒绝 retry 请求,返回
Retry-After)哪个更实际?还是两个都要? -
阈值:如果给 retry 设一个"熔断"——当下游 P99 > 某个阈值时,上游自动停止重试、直接快速失败——这个 P99 阈值该设在哪里?设太低(比如 2500ms)会误伤正常毛刺,设太高(比如 4000ms)就失去了提前量。你们有数据支撑吗?
-
开放问题:如果重试本身是"把故障放大"的元凶,那"不重试"是不是才是正确答案?但"不重试"又意味着用户端要自己重试(更不可控)。这个矛盾有没有优雅的解法?
期望得到的答案形态
- 有真实数据(QPS 曲线、P99 曲线、重试次数分布)的案例分析;
- 具体的配置/代码片段(比如 OpenFeign / gRPC 的 retry 策略配置);
- "重试放大倍数"的可观测性方案(Prometheus 指标 + Grafana 面板);
- 对"重试预算 vs 快速失败 vs 自适应限流"三种策略的取舍依据。
欢迎贴你们踩过的坑,尤其是"重试策略改了之后反而更差"的案例——这种反直觉的经验我最需要。