Java

一个接口的容量预算,QPS、超时、重试、线程池该怎么一起算?

接口容量预算最怕把 QPS、timeout、retry 和线程池拆开各算一遍。只有把入口流量、线程持有时间、重试放大量和下游退化场景放进同一张账里,预算结果才真的能指导限流、线程池 sizing 和高峰保护。

  • 容量预算
  • QPS
  • 超时
  • 重试
  • 线程池
18 分钟阅读

接口容量预算常见的失败,不是因为没人算,而是每个人只算了自己手里的那一段。流量同学盯 QPS,应用同学盯 timeout,框架同学盯线程池,结果每个数字单看都说得过去,拼到一次高峰或一次下游抖动里,系统还是会排队、超时、重试连着来。

真正决定接口能不能扛住高峰的,通常是几笔账同时叠在一起:

  • 入口流量到底有多大
  • 请求在系统里会停留多久
  • timeout 后会不会再来一轮 retry
  • 线程、连接和下游额度会被占多久
  • 下游一慢时,真实调用量会被放大到什么程度

所以容量预算真正该回答的,不是“平时能跑多少 QPS”,而是另外两个更接近事故现场的问题:

  • 高峰时这些请求会在系统里堆多久
  • 某一层一慢,原始流量会被放大成多大的真实压力

本文就围着这两件事展开,把 QPS、timeout、retry、线程池重新放回同一套预算口径里。

你手上的问题是不是容量预算

如果你现在要做预算,第一轮别只抓一个参数,而是把原始入口 QPS、线程持有时间、timeout 预算、retry 放大量和下游退化时的真实并发占用摆到同一张纸上。这样你才知道自己算的是“平时数字”,还是“高峰下真的会出事的那部分”。

你手上的问题先看哪里为什么
想给一个接口做容量预算,把 QPS、timeout、retry、线程池放进同一套口径看完本文再细化这里先把最容易被拆散的几笔账重新并到一套模型里
你正在设计 timeout、retry、熔断参数的协同关系一个接口的超时、重试、熔断参数,应该怎样整体设计?那篇更适合单独梳理保护参数怎么互相影响
你要把线程池和数据库连接池一起做联合预算线程池和数据库连接池的容量,为什么要一起做预算?那篇专门补资源池互相卡住时该怎么算
现场已经在慢 / timeout,想知道先查哪层如何建立接口慢 / 超时专题的值班检查表?先收敛故障现场,再回头补预算更靠谱

这篇主要算清哪几笔账

如果你现在是在高峰前做容量核算,或者刚经历过一次 timeout / retry 放大的故障,本文优先回答两个问题:

  • 高峰时请求会在系统里停多久
  • 某一层一慢,原始流量会被放大成多大的真实压力

如果你的现场已经在慢、在 timeout,本文不能替代第一轮排障;如果你主要在调 timeout / retry / 熔断参数,也该先去看参数设计那篇。这里的重点只有一个:把容量预算本身算清,再把结果落回线程池、连接池和保护阈值。

一、为什么只看 QPS,容量预算很容易失真

因为 QPS 只回答了:

  • 单位时间来了多少请求

但没回答:

  • 每个请求会占资源多久
  • 超时之后会不会衍生出更多请求
  • 线程是不是会被慢下游一起拖住

举个很典型的例子。

两个接口可能都是 1000 QPS:

  • A 接口单次 50ms,基本不重试,线程持有时间短
  • B 接口单次 300ms,偶发 timeout,retry 1 次,线程还会等数据库连接

表面上 QPS 一样,真实容量压力却完全不在一个量级。

所以更接近真实工程问题的容量预算,应该至少同时看:

  • 流量规模
  • 请求停留时间
  • 失败后的放大量

二、为什么 QPS、timeout、retry、线程池必须一起看

因为它们在一条调用链里本来就是互相作用的。

1. QPS 决定有多少请求在进来

这是入口规模。

2. timeout 决定你允许一个请求最多占资源多久

timeout 越长,请求可能占线程、连接和下游资源的时间就越久。

3. retry 决定一次失败会不会放大成多次尝试

retry 一旦存在,真实调用量就不再等于原始流量。

4. 线程池决定系统能同时吞下多少个“正在占线程的请求”

线程池不是只和 QPS 有关,更直接和:

  • 请求停留时间
  • 慢下游等待
  • 重试后的额外尝试数

有关。

所以如果把这四项拆开看,最后很容易出现下面这种错位:

  • QPS 看起来不高
  • timeout 配得挺保守
  • retry 也不算太多
  • 线程池数字也不小

但拼在一起以后,系统依然会在高峰或依赖抖动时突然排队。

三、先建立一个最基本的容量认知:吞吐不只取决于流量,还取决于占用时间

这几乎是所有容量预算里最值得先记住的一句话。

如果单个请求在线程里停留的时间变长,那么即使入口 QPS 不变:

  • 同时占住线程的请求数也会变多
  • 线程池更容易打满
  • 后续请求会更早排队

所以真正决定线程池压力的,不只是 QPS,而更像:

  • 同时在场的请求数

而同时在场的请求数,又和:

  • QPS
  • 单次线程持有时间

直接相关。

这也是为什么 timeout、retry、慢下游和线程池一定要一起看。

四、容量预算时,最该拆清楚哪几个量

如果团队想认真做一个接口的容量预算,我更建议先把下面 5 个量拆开。

1. 原始入口 QPS

重点先放在:

  • 平均 QPS
  • 峰值 QPS
  • 突发窗口 QPS
  • 大促或活动窗口 QPS

注意不要只看日常峰值,还要看:

  • 是否有瞬时尖峰
  • 是否有批量重放或补偿流量

2. 单请求线程持有时间

这比单看接口 RT 更值钱。

因为真正吃掉线程池的,不是用户体感时间本身,而是:

  • 请求从拿到线程到释放线程,一共占了多久

这里尤其要看:

  • 平均值
  • P95 / P99
  • 高峰时是否明显拉长

3. timeout 预算

你要明确:

  • 客户端整体愿意等多久
  • 网关愿意等多久
  • 应用整体预算是多少
  • 关键下游单次 timeout 是多少

如果这层不清楚,后面 retry 和线程池都没法算准。

4. retry 放大量

重点不是“配了几次”,而是:

  • 哪些失败会触发 retry
  • timeout 时最坏会多出多少调用
  • 是否多层 retry 叠加
  • 是否有退避和抖动

很多团队容量预算完全没把 retry 放进去,结果纸面流量和真实流量根本不是一回事。

5. 下游退化场景下的线程持有时间

这一步最容易被忽略,却最重要。

你不能只看正常时:

  • 接口 80ms
  • 线程池没问题

更要看:

  • 如果下游从 80ms 抬到 400ms,会发生什么
  • 如果连接池等待起来了,会发生什么
  • 如果 retry 启动了,最坏同时在场请求数会涨到多少

没有退化场景,容量预算就只能说明“平时还行”。

五、容量预算别一上来就套线程池公式

我见过不少容量评估一上来就套线程池公式,但真到高峰翻车时,问题往往出在前面的账根本没对齐。更靠谱的做法,是先把总预算、放大量和并发占用接起来。

第 1 步:先定总预算

先回答:

  • 用户可以等多久
  • 业务允许失败快返回还是必须多等
  • 核心链路和非核心链路的时间预算分别是多少

这一步决定 timeout 的大框架。

第 2 步:再算放大预算

也就是:

  • retry 最多能把调用量放大到多少
  • timeout 和 retry 叠加后,单个用户请求最坏会变成几次内部调用
  • 网关、SDK、代码层是否存在多层重试

这一步决定真实流量不是原始 QPS,而是:

  • 原始 QPS x 放大系数

第 3 步:最后才算并发预算

也就是:

  • 在真实调用量和最坏持有时间下,同时在场的请求数会有多少
  • 线程池、连接池和关键下游是否一起扛得住

这样做的好处是:

  • 线程池数字不是拍出来的
  • 而是从流量、时间和放大量自然推出来的

六、一个典型例子:为什么 800 QPS 的接口也能把线程池打满

假设某个接口日常峰值只有 800 QPS,看起来不算夸张。

正常时:

  • 单请求线程持有 80ms
  • timeout 很少
  • retry 几乎不触发

这时线程池压力确实不大。

但某天活动时,下游库存服务抬到 500ms,且 timeout 设在 400ms,retry 1 次。

现场会发生什么?

  • 第一轮请求会在 400ms 左右超时
  • retry 再打一轮
  • 线程持有时间从 80ms 接近变成数百毫秒,甚至更长
  • 真实内部调用量明显高于原始 800 QPS
  • 同时在场请求数暴涨

这时你会看到:

  • 原始 QPS 并不吓人
  • 线程池却已经明显排队

因为真正把线程池打满的,不是单一的 QPS,而是:

  • QPS
  • timeout
  • retry
  • 下游退化

一起作用后的结果。

七、线程池预算里最容易漏掉的 3 个问题

1. 只按正常 RT 算,不按退化 RT 算

这会让容量预算在真正出问题时几乎没有参考价值。

2. 忽略 retry 带来的额外线程占用

每次 retry 都是新的线程和依赖占用,不是“免费再试一次”。

3. 把线程池和依赖容量分开看

如果下游连接池、数据库连接池或外部接口承受不了,单独放大线程池只会更快把请求压到堵点上。

八、容量预算最后要落到哪些工程动作上

一套接口容量预算,如果最后只产出一个 PPT 数字盘子,价值是不够的。

更应该落到下面这些动作。

1. timeout 设计

  • 哪些链路 timeout 要更短
  • 哪些链路要快速失败
  • 哪些链路要给重试留预算

2. retry 设计

  • 哪些错误可重试
  • 最多重试几次
  • 是否必须加退避和抖动
  • 哪些写接口禁止默认 retry

3. 线程池与连接池 sizing

  • 核心链路单独配池
  • 慢任务和异步任务要隔离
  • 线程池和连接池一起做联合预算

4. 保护策略

  • 限流阈值设在哪
  • 熔断何时触发
  • 降级哪些非核心逻辑
  • 高峰时优先保护哪些入口

也就是说,容量预算最终应该服务接口保护,而不是只说明“理论上能跑多少”。

九、关键误判

误判 1:接口容量预算就是看峰值 QPS

QPS 只是入口规模,不代表真实资源占用压力。

误判 2:timeout 只影响失败体验,不影响容量

timeout 直接决定请求最多会占线程和依赖多久,本身就是容量变量。

误判 3:retry 只是兜底逻辑,不用进容量模型

一旦 retry 触发,真实调用量和线程占用都可能明显放大。

误判 4:线程池够大,接口容量就够了

如果下游、数据库连接池或回源链扛不住,大线程池只会更快把压力压到更深层。

十、FAQ:做接口预算时最容易卡住的几个问题

1. 容量预算到底先看 QPS 还是先看线程池?

都要看,但顺序上更建议先看流量和时间预算,再看线程池和依赖容量。

2. 为什么接口 RT 正常,容量预算也可能仍然不稳?

因为平时 RT 正常不代表高峰和退化场景下的线程持有时间、retry 放大量和依赖承压也稳。

3. retry 应该怎么进容量模型?

至少要把:

  • 触发条件
  • 最坏重试次数
  • 多层重试叠加
  • 最坏调用放大量

一起算进去。

4. 容量预算和限流、熔断是什么关系?

容量预算给出边界,限流和熔断负责在逼近边界时保护系统别继续放大。

十一、最后总结:容量预算不是报一个 QPS 数字,而是把最坏场景提前算出来

如果预算只停在 QPS,很容易过分乐观;如果只盯线程池,又会漏掉 timeout 和 retry 带来的真实放大。接口容量真正需要的,是把入口流量、时间预算、重试放大和退化场景一起算到同一张账里。

更实用的顺序仍然是:先定接口总时间预算,再算 retry 带来的最坏放大量,最后结合线程持有时间、线程池、连接池和下游退化场景去估并发占用。这样得出来的结果,才真的能拿去指导接口保护、线程池 sizing 和高峰期稳定性治理。

这篇归在“稳定性治理与值班体系”专题里。看完预算模型之后,后面通常会分成三类问题继续往下补。

先把接口保护参数定清

资源池已经开始一起吃紧

准备把预算结果沉到治理动作

如果你还停留在参数表和峰值 QPS,就先把 timeout、retry、熔断这层补完整;如果线上已经出现线程池、连接池和下游一起变差,就优先把资源池和放大量画到一张图里;如果已经准备推进值班治理,再把这套预算结果沉到专题治理和检查表里。