「我们每天要检测 5 万条,接口扛得住吗?」
这个问题没法直接回答,因为「每天 5 万条」不是负载,是总量。真正的负载由三个数决定:峰值到达率、单次处理时长、允许的在途任务数。三者之间有一个确定的数学关系,算清楚了,容量问题就变成了算术题。
这篇用一个具体场景把账算一遍。
一、先列出所有约束
假设用的是标准的异步检测接口,约束有四类:
| 约束 | 典型值 | 性质 |
|---|---|---|
| 提交速率 | 2 次/秒 | 硬限流,超了 429 |
| 查询速率 | 10 次/秒 | 硬限流 |
| 单账号日配额 | 1000 次/天 | 硬配额,超了 429 |
| 单任务耗时 | 1~3 秒(P95 约 3s) | 上游决定,不可控 |
这四条要分开算,因为它们限制的是不同的东西:提交速率限制「进得多快」,查询速率限制「看得多勤」,日配额限制「一天能用多少」,任务耗时决定「同时有多少在跑」。
最常见的错误是只看日配额:「1000 条/天,一天 86400 秒,平均 0.012 QPS,肯定没问题。」这句话在平均意义上是对的,但在峰值上可能错得很离谱。
二、Little's Law:把总量换成并发
排队论里最实用的一个公式:
L = λ × W
L:系统内的平均任务数(在途并发)λ:到达率(任务/秒)W:单个任务的平均停留时间(秒)
它不依赖任何分布假设,只要系统稳定就成立。
代入场景:峰值到达率 λ = 0.5 任务/秒(即 1800 条/小时),单任务耗时 W = 2 秒,那么在途任务数:
L = 0.5 × 2 = 1
平均只有 1 个任务在跑。这解释了为什么很多「日量很大」的系统实际压力很小——量被摊平了。
反过来,如果业务要求「1 小时内跑完 7200 条」:
λ = 7200 / 3600 = 2 任务/秒
L = 2 × 2 = 4
提交速率刚好顶到 2 QPS 的上限,一点余量都没有。这时候任何一次网络抖动都会让队列开始堆积。
三、轮询 QPS:真正容易被忽略的指标
查询速率 10 QPS 看起来比提交的 2 QPS 宽松,但它消耗得快得多,因为每个任务要被查多次。
轮询次数 ≈ 任务耗时 / 轮询间隔。耗时 2 秒、间隔 1.5 秒,平均查 2 次。但这不是关键,关键是瞬时并发:
查询 QPS = 在途任务数 / 轮询间隔
这个公式是容量规划里最容易翻车的地方。举两个例子:
| 在途任务数 | 轮询间隔 | 查询 QPS | 结果 |
|---|---|---|---|
| 4 | 1.5s | 2.7 | 安全 |
| 15 | 1.5s | 10.0 | 刚好打满 |
| 30 | 1.5s | 20.0 | 持续 429 |
也就是说,并发数才是查询 QPS 的驱动因素,不是日总量。日总量 10 万但并发只有 4,查询毫无压力;日总量 1 万但并发开到 30,查询立刻爆。
由此得到一个很实用的反推公式:
最大安全并发 = 查询 QPS 上限 × 轮询间隔
= 10 × 1.5 = 15
想要更高并发,只有两条路:拉长轮询间隔(5 秒 → 50 并发),或者申请更高的查询配额。没有第三条。
四、把四个约束画在一张图上
把上面的关系整理成一张表,业务方一看就懂:
| 目标 | 需要的提交 QPS | 需要的并发 | 需要的查询 QPS | 瓶颈 |
|---|---|---|---|---|
| 1000 条/天,8 小时跑完 | 0.035 | 0.07 | 0.05 | 无 |
| 1000 条/天,1 小时跑完 | 0.28 | 0.56 | 0.37 | 无 |
| 7200 条/天,1 小时跑完 | 2.0 | 4.0 | 2.7 | 提交速率 |
| 5 万条/天,1 小时跑完 | 13.9 | 27.8 | 18.5 | 提交 + 查询 |
| 5 万条/天,24 小时摊平 | 0.58 | 1.16 | 0.77 | 日配额 |
最后一行最值得注意:日配额 1000 的账号,根本跑不到 5 万条。总量目标在配额面前直接失效。所以容量规划的第一步不是算 QPS,是先确认配额够不够。
顺带算一下 2 QPS 跑满一天的理论上限:2 × 86400 = 172800 条。看起来很多,但受日配额和余额双重限制,实际能用到的远小于这个数。
五、积压从哪来:利用率先看 0.8
单任务耗时不是固定的,服务端也有处理能力上限。设服务端平均处理速率 μ(任务/秒),到达率 λ,则利用率:
ρ = λ / μ
ρ 超过 0.8 之后,等待时间开始非线性上升。用最简单的 M/M/1 估算平均等待:
Wq = ρ / (μ × (1 - ρ))
代入 μ = 0.5(每任务 2 秒)、λ = 0.4:
ρ = 0.8
Wq = 0.8 / (0.5 × 0.2) = 8 秒
再往上一点,λ = 0.45:
ρ = 0.9
Wq = 0.9 / (0.5 × 0.1) = 18 秒
到达率只涨了 12.5%,等待时间翻了一倍多。这就是为什么容量规划要留 20%~30% 余量——不是浪费,是买「不排队」。
六、背压:四层,从外到内
知道上限之后,要保证系统撞到上限时优雅退化,而不是雪崩。四层背压,缺一层都会在峰值时出问题。
第一层:入口拒绝。 队列长度超过阈值就直接返回 429 + Retry-After,不要收下再排队。收下的任务最终会超时,用户等了 5 分钟拿到一个失败,比立刻被拒更糟。
if queue.qsize() > MAX_QUEUE:
return jsonify({"code": "overloaded"}), 429, {"Retry-After": "30"}
第二层:有界并发。 用信号量限制同时在飞的请求数,而不是无限制地 asyncio.gather 或 go func。并发上限就是前面算出来的「最大安全并发」。
sem = asyncio.Semaphore(15) # 对应 10 QPS × 1.5s
async def detect(text):
async with sem:
return await submit_and_wait(text)
第三层:自适应限流(AIMD)。 固定阈值是静态的,实际可用容量会随对方负载波动。加一个简单的 AIMD:每 10 秒统计 429 比例,超过 5% 就把并发乘 0.7,低于 1% 就加 1。
class AIMD:
def __init__(self, init=4, lo=1, hi=15):
self.cur, self.lo, self.hi = init, lo, hi
def on_window(self, total: int, rejected: int):
rate = rejected / max(1, total)
if rate > 0.05:
self.cur = max(self.lo, int(self.cur * 0.7))
elif rate < 0.01:
self.cur = min(self.hi, self.cur + 1)
这比拍脑袋定一个并发数靠谱得多,尤其是对方也在扩缩容的时候。
第四层:时间预算。 每个任务从提交开始算总预算(比如 3 分钟),到点就放弃等待,把 task_id 落到「待续查」表。注意是放弃等待,不是取消任务——任务在服务端还在跑,取消只会让钱白花。
四层配合起来的效果:峰值时先拒绝一部分(第一层),在飞的保持稳定(第二层),对方变慢时自动降速(第三层),个别任务不会拖死整个批次(第四层)。
七、压测:找拐点,别找极限
压测的目的不是「看看最高能跑到多少」,而是找到开始出现 429 的那个点,然后把生产并发定在它的 70%。
阶梯加压,每档跑 5 分钟:
| 阶段 | 提交 QPS | 观察重点 |
|---|---|---|
| 1 | 0.5 | 基线延迟、错误率 |
| 2 | 1.0 | 延迟是否线性 |
| 3 | 2.0 | 是否出现 429 |
| 4 | 4.0 | 429 比例、P95 劣化速度 |
| 5 | 8.0 | 系统是否进入雪崩 |
三个必须记录的指标:
- 429 比例:超过 1% 就说明这一档已经过载;
- P95 端到端耗时:从提交到拿到结果,包含排队;
- 队列深度:如果队列在持续增长,说明到达率大于处理率,即使没有 429 也不可持续。
一个常见误区是只看平均延迟。平均值在过载时反而可能下降——因为大量请求被快速拒绝了。要看 P95 和错误率。
还有一点:压测要跑满一个完整周期。检测接口的延迟在白天和凌晨能差一倍,只在下午压测,得出的结论在凌晨不成立(反过来也一样)。
八、算钱:容量规划的另一半
容量不只是 QPS,还有预算。按 0.06 元/千字、平均每篇 1000 字算:
| 日检测量 | 日成本 | 月成本 |
|---|---|---|
| 1000 | 60 元 | 约 1800 元 |
| 5000 | 300 元 | 约 9000 元 |
| 50000 | 3000 元 | 约 9 万元 |
再叠加「重试即重复扣费」的系数。如果客户端重试率是 3%,实际成本就是表里的 1.03 倍——看起来不多,但 9 万的 3% 是 2700 元/月,足够买一台不错的机器。
所以压测时顺便统计一下重试率。重试率是成本指标,不只是稳定性指标。控制在 1% 以内是合理目标,超过 5% 说明客户端逻辑有问题(多半是超时设太短,或者没复用幂等键)。
九、小结
容量规划的全部内容,其实就是四个公式:
L = λ × W 在途并发 = 到达率 × 耗时
查询 QPS = L / 轮询间隔 并发决定查询压力
ρ = λ / μ 利用率超过 0.8 开始排队
最大安全并发 = 查询上限 × 间隔 反推并发上限
再配四层背压(入口拒绝、有界并发、AIMD、时间预算),和一次找拐点的阶梯压测,就够了。
最后提醒一句:先确认日配额和余额,再算 QPS。很多「接口扛不扛得住」的讨论,最后发现根本跑不到那个量——配额在 1000 条/天,算 5 万条的并发毫无意义。
示例基于文识云(sbnew.top)的检测接口约束(提交 2 QPS、查询 10 QPS、日配额 1000)。具体数值以你的账号档位为准,公式不变。