欢迎光临
我们一直在努力

检测链路的容量规划——用 Little‘s Law 算清 QPS、并发与积压

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */
#content_views .toc,
/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */
#content_views.markdown_views > p:empty:has(+ .toc),
#content_views.markdown_views > .toc + p:empty,
/* 富文本旧版目录标记 */
#content_views.htmledit_views #main-toc,
#content_views.htmledit_views #hr-toc,
#content_views.htmledit_views p[id*=\”-toc\”] {
display: none !important;
}
/* 目录去掉后,紧跟的首个标题不再多出一块上边距 */
#content_views.markdown_views > .toc + h1,
#content_views.markdown_views > .toc + h2,
#content_views.markdown_views > .toc + h3,
#content_views.markdown_views > .toc + h4,
#content_views.markdown_views > .toc + p:empty + h1,
#content_views.markdown_views > .toc + p:empty + h2,
#content_views.markdown_views > .toc + p:empty + h3,
#content_views.markdown_views > .toc + p:empty + h4 {
margin-top: 0 !important;
}

「我们每天要检测 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)。具体数值以你的账号档位为准,公式不变。

    赞(0)
    未经允许不得转载:171主机测评 » 检测链路的容量规划——用 Little‘s Law 算清 QPS、并发与积压
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址