压测报告写着 QPS 八千,结论是"容量充足"。上线后两千就挂,且不是因为 CPU 满——内存把容器推到了 limit,连接数撞到了下游上限。报告没错,被读错了:它测的是吞吐,被当成了容量。
吞吐是单位时间能处理多少,容量是同时能容纳多少。两者的瓶颈资源常常不是同一个,所以一个达标的数字不能保证另一个。这篇把两个词分开,给出压测数字生效的三种前提与容量表该填什么。
1. 两个词,两套瓶颈
| 吞吐(Throughput) | 容量(Capacity) | |
|---|---|---|
| 定义 | 单位时间处理完成的请求量 | 同时能容纳的在途量 |
| 典型瓶颈 | CPU、序列化、加解密、下游 RT | 内存、连接数、文件句柄、队列长度 |
| 时间尺度 | 短时可达的峰值 | 长时间运行后的稳定上限 |
| 失效表现 | 响应变慢、排队变长 | OOM、连接拒绝、句柄耗尽 |
| 扩容能否解决 | 通常能线性提升 | 只在无状态部分能 |
关键差别在最后一行:扩容能提升吞吐,但不一定提升容量。有状态的部分(会话、本地缓存、队列、未完成任务)不随实例线性扩展,实例数翻倍时这部分的总容量可能一点没变,只是被切成了更多的碎片。
用小定律能把两者连起来,这也是它们被混淆的根源:
Little's Law(适用于稳定状态):
未完成请求数 L = 到达率 λ × 平均耗时 W
─── 吞吐 8000 QPS、平均 RT 200ms → 同时在途 L = 8000 × 0.2 = 1600 个请求
推论一:吞吐上去了、RT 也上去了 → 在途量按乘积增长,容量需求被双重放大
推论二:容量(在途上限)不动而吞吐想提高 → 只能压 RT,压不动就撞墙
─── 但须先确认系统处于稳定状态;排队严重时这条式子会低估内存占用
推论一是最容易被忽略的地方:压测时把 QPS 打上去,同时平均 RT 从 50ms 涨到 200ms,在途量实际涨了 16 倍。容器内存炸掉的时刻,往往不是 QPS 最高的时刻,而是 QPS 中等偏上、RT 已经劣化的那一段。
这个式子在容量评审里有一个非常实际的用法:它把"QPS 目标"翻译成"内存预算"。如果服务每个在途请求占用若干 KB 的上下文,用目标 λ 与可接受 W 算出 L,就能得出必须预留多少内存,而不是等 OOM 之后再加 limit:
// 容量反推的写法,不是精确预测,而是给内存预算一个可解释的来源
目标 λ = 3000 QPS
可接受 W = 0.150 s(P95 而非均值,均值会低估长尾的在途量)
→ L ≈ 450 个在途请求
每个在途请求的上下文约 R KB(含缓冲、序列化中间态)
→ 内存预算 ≈ 450 × R,再乘 1.5 留余量
─── R 必须实测(单请求内存增量),不要用经验常数
2. 压测数字生效的三个前提
任何"QPS 八千"都必须附带三个前提,缺一个就无法移植到生产:
| 前提 | 具体指什么 | 缺了会怎样 |
|---|---|---|
| 数据量 | 表行数、索引形态、缓存命中率 | 在空库或小表上测出的数字,生产上指数级失真 |
| 请求分布 | 读写比、参数分布、冷热比例 | 全读、全命中、单 key 的测试最容易出漂亮数字 |
| 持续时间 | 跑了多久 | 短测掩盖增长类问题,见下一节 |
第三条要用不同的时长测同一个配置,结果会不一样:
// 同一个并发,三档时长,看指标是否收敛
wrk -t8 -c200 -d60s --latency http://[TARGET]/api/orders // 60 秒:只看吞吐上界
wrk -t8 -c200 -d600s --latency http://[TARGET]/api/orders // 10 分钟:看稳态是否漂移
wrk -t8 -c200 -d3600s --latency http://[TARGET]/api/orders // 1 小时:看内存与连接是否单调上升
─── 判据:60 秒的数字用于定吞吐上界,1 小时的数字才能进容量表
─── 若三档 P99 差异超过一个量级,说明 60 秒的结论不可用
短时压测会掩盖三件事,它们都不在 CPU 曲线上:
1) 内存增长:堆外的、缓存的、连接池的,只在持续运行后显现
2) 连接泄漏:每请求建连未归还,短测时不明显,长测时连接数单调上升到拒绝
3) 缓存被填满后的表现:命中率从 95% 掉到 70% 之后,下游 RT 与错误率同时变化
─── 这三项都表现为"短测都没问题,上线几小时后开始变慢"
3. 峰值与持续、并发与吞吐:填容量表该用哪个数
| 指标 | 用途 | 不能用于 |
|---|---|---|
| 峰值 QPS | 定突发能力、定限流阈值 | 填容量表(短暂可达,不代表可持续) |
| 持续 QPS | 定实例数、定数据库规格 | 定限流阈值(会误伤正常突发) |
| 拐点 QPS | 定安全水位 | 定预算(它随数据量与分布变化) |
并发数与吞吐不是线性关系。它在低并发区近似线性,过拐点后吞吐下降——因为排队、上下文切换、GC 压力开始互相放大:
并发扫描的读法(典型形状,具体数值必须实测):
并发 50 → QPS 3000,P99 40ms
并发 200 → QPS 7000,P99 90ms // 拐点在此附近,吞吐最高
并发 400 → QPS 6800,P99 300ms // 吞吐不再涨,延迟继续劣化
并发 800 → QPS 5200,P99 1200ms // 吞吐回落,开始出现超时类错误
─── 安全容量取拐点 QPS 的 60–70%,并留出下游 RT 劣化的余量
峰值与持续这两个数的正确用法是分开的:限流阈值参考峰值,扩容与规格参考持续。用峰值填容量表,结果是为一年里两次的活动付全年的钱;用持续定限流,结果是大促当天所有请求都被拦在门口。
为什么一过拐点延迟会突然变陡,而不是缓慢变差?近似排队的公式给了答案:
W ≈ W0 / (1 - ρ) // ρ = 资源利用率,W0 = 无竞争时的服务时间
ρ = 0.5 → W ≈ 2.0 × W0
ρ = 0.8 → W ≈ 5.0 × W0
ρ = 0.9 → W ≈ 10 × W0
─── 利用率没有"线性"区间;瓶颈资源的 80% 与 90% 差一倍,且尾部更长
这条式子的实践后果是:安全水位不该按吞吐定,该按瓶颈资源的利用率定。CPU 长期 85% 的系统,即使吞吐数字漂亮、没有报错,也已经在延迟的陡坡上;此时增加的流量带来的不是排队,是超时。
容量表还要有状态量一列,它决定长时间运行是否稳定:
| 状态量 | 上限由什么决定 | 满溢后的表现 |
|---|---|---|
| 会话/登录态 | 内存或集中存储的条目数 | 淘汰导致用户被踢,或内存 OOM |
| 连接(含下游) | 本端上限、下游 max_connections | 获取连接超时,表现为全线变慢 |
| 队列长度 | 配置上限、内存 | 拒绝入队或内存持续增长 |
| 未完成任务 | 线程池 + 队列 | 堆积到超时,任务被丢弃或重复执行 |
4. 扩容的前提、度量的读法与排错
"我们扩容了,所以能扛"这句话省略了三个前提:共享资源不随实例扩容、有状态部分不随实例扩容、每实例的容量在扩容后仍然成立。数据库连接数是最常见的反例——实例从 4 个扩到 16 个,每个实例的连接池上限不变,数据库连接总数翻了 4 倍,直接撞上 max_connections。
// 扩容前的检查项,缺一项扩容就可能变成故障放大器
shared_resource:
- db_max_connections: 当前值 / 扩容后需要 // 必须先算乘数
- cache_qps: 当前值 / 扩容后需要
- bandwidth: 出向带宽是否成为新瓶颈
stateless_ratio: 该服务的无状态比例 // 会话在本地内存里,扩了也没用
per_instance_capacity: 单实例拐点 × 0.65 // 每实例都用同一个数,不按平均值
| 度量 | 怎么取 | 读法 |
|---|---|---|
| 拐点 QPS | 阶梯加压,记录吞吐开始回落的那一档 | 安全水位 = 拐点 × 0.6–0.7 |
| 长稳压测的资源增长斜率 | 1 小时压测中,内存与连接数按分钟采样 | 斜率不收敛到 0 → 有泄漏或缓存无上限 |
| 连接与队列水位 | 连接池活跃数、队列深度 | 长期贴着上限 → 容量不足而非吞吐不足 |
| 每实例承载会话数 | 会话存储条目数 / 实例数 | 会话集中在单实例 → 扩容无效 |
| 现象 | 常见原因 | 处理方向 |
|---|---|---|
| 压测八千、上线两千就挂 | 缺少数据量或分布前提 | 用生产规模数据 + 生产读写比重测 |
| CPU 不高但请求超时 | 容量类瓶颈:连接、句柄、队列 | 看连接水位与队列深度,不看 CPU |
| 运行几小时后开始变慢 | 内存线性增长或连接泄漏 | 做 1 小时长稳,看斜率 |
| 扩容后总吞吐未提升 | 共享资源已饱和,或有状态部分在本地 | 先算共享资源乘数,再谈加实例 |
| 长稳压测通过、生产仍抖动 | 压测环境网络拓扑与下游限流不同 | 在准生产拓扑下复测,含下游限流 |
压测环境与生产有三处必然差异,必须在结论里显式标注:数据量(通常是生产的一个零头)、网络拓扑(少一跳或少一个代理)、下游限流(压测时常常被关掉)。在关掉下游限流的压测里得到的吞吐,是"没有下游"时的吞吐。
5. 边界:什么时候问题退化成成本
| 情况 | 结论 | 理由 |
|---|---|---|
| 完全无状态、请求极轻 | 容量问题变成本问题 | 加实例即可,只需算钱 |
| 可无限横向扩展且有弹性伸缩 | 关注成本与冷启动 | 容量由调度器兜住 |
| 离线批处理 | 换成"多久跑完"的问题 | 没有在途量概念,只有时长 |
| 有严格长连接(WebSocket、推送) | 容量问题集中在连接数与内存 | 扩容时要考虑连接迁移的代价 |
| 单实例、低流量内部系统 | 只需要一条资源告警 | 谈不上容量规划 |
反过来最需要这套区分的是有共享下游 + 会话有状态 + 有峰值的系统:三个特征同时存在时,吞吐数字与容量结论几乎必然分叉。
6. 自测
- 吞吐与容量的瓶颈资源通常分别是什么?
- 为什么说"扩容能提升吞吐,但不一定提升容量"?
- 用小定律推:QPS 上升且 RT 劣化时,在途量会怎么变?
- 压测数字要生效,必须附带哪三个前提?
- 限流阈值与容量表分别该参考峰值还是持续 QPS?
- 压测环境与生产的三处必然差异是什么?