外观
压测(load testing)是用受控流量让服务暴露真实能力边界的过程;容量规划(capacity planning)则是用压测数据回答「要几台机器」的数学题。 两者合起来回答上线前的三个问题:能扛多少 QPS?P99 延迟多少?需要多少实例? 没有压测数据就上线,等于在用户流量里做无保护的实验——容量预估错 10 倍、P99 悄悄恶化、促销日直接被打挂,都是没压测的典型事故。这篇给出从目标设定、工具选型到容量公式的完整方法论,让你在「上线」按钮前拿到一张有证据的答卷。理论基础见 性能指标与调优 与 监控。
先记住三个数字纪律
压测的三个常识:测 P99 不测平均值(平均值会掩盖长尾);必须预热(热身不足的数据虚高);必须记录环境(不可复现的压测等于没测)。
一、压测目标:三个问题一次回答
一次像样的压测,产出三份结论:
| 问题 | 由什么回答 | 输出物 |
|---|---|---|
| 能扛多少 QPS? | 逐步加压找吞吐拐点 | 饱和 QPS、最大并发 |
| P99 多少? | 各并发档位的延迟分布 | 延迟-并发曲线 |
| 要不要扩容? | 容量公式代入峰值流量 | 实例数、卡数 |
三份结论缺一,压测报告就是半成品。「能扛多少 QPS」和「P99 多少」必须绑定回答——孤立的数字会骗人:服务可能在 500 QPS 时 P99 已到 500ms(延迟先恶化),也可能 2000 QPS 还稳稳的(容量还有富余)。这两个维度构成的曲线,才是容量决策的依据。
二、工具选型:五张牌怎么打
| 工具 | 定位 | 适用场景 | 学习曲线 | 关键特性 |
|---|---|---|---|---|
| wrk | 单机 HTTP 基准 | 快速看裸吞吐、压 /healthz | 低(命令行) | 多线程 + 复用连接,脚本能力弱 |
| vegeta | 单机 HTTP 目标 QPS | 固定 RPS 攻击、Targets 文件化 | 低 | 精确控制攻击速率,输出格式好 |
| locust | 分布式场景脚本 | 业务路径(登录→查询→下单)、带 think time | 中(Python) | 可编程、主从分布式、Web 界面 |
| ghz | gRPC 压测 | gRPC 服务 | 中 | 原生支持 proto、流式调用 |
| k6 | 云原生/CI 生态 | K8s 环境、流水线集成、SLO 断言 | 中(JS 脚本) | 内置断言与阈值、K8s Operator |
选型的一句话决策:只要快速摸吞吐用 wrk;需要模拟真实业务节奏用 locust;服务是 gRPC 用 ghz;要进 CI 断言 SLO 用 k6;需要精确打固定 QPS 用 vegeta。 更多工具见 工具与资源清单。
bash
# wrk 摸裸吞吐(4 线程、100 连接、10 秒)
wrk -t4 -c100 -d10s http://localhost:8000/healthz
# vegeta 精确打 200 QPS 持续 30 秒,输出直方图
echo "POST http://localhost:8000/predict" | vegeta attack -rate=200 -duration=30s \
-body=payload.json | vegeta report --type=hist[0,50ms,100ms,200ms,500ms]三、压测流程:五步走到容量结论
① 定义目标指标(先写 SLO 再开测)
压测前把「什么样算通过」写下来,否则测完无法下结论:
text
SLO 草案(示例):
- 吞吐:稳定支撑 200 QPS(当前预估线上峰值的 2 倍)
- 延迟:P99 < 100ms,P50 < 30ms
- 稳定性:错误率 < 0.1%,无 5xx 峰值
- 资源:单实例 CPU < 80%,GPU 利用率 < 85%(留 15% 余量)SLO 的完整设计(燃烧率、告警阈值)见 可观测性落地 与 SLO 概念。
② 场景设计:四种压法覆盖真实形态
| 场景 | 做法 | 回答的问题 |
|---|---|---|
| 固定 QPS | vegeta/k6 打固定速率 10 分钟 | 稳态能力:这个负载下稳定吗? |
| 逐步加压 | locust 从 10 并发逐步翻倍 | 吞吐拐点在哪?P99 从哪开始恶化? |
| 峰值突刺 | 突然从 100 打到 300 QPS 保持 60s | 扛得住毛刺吗?回弹快吗? |
| 混合场景 | 90% 正常流量 + 10% 慢请求/重模型 | 慢请求会不会拖垮队列(队头阻塞)? |
逐步加压是找拐点的关键:以 50 并发起步,每 60 秒翻倍(50→100→200→400),同步记录 QPS 与 P99。QPS 不再随并发增长的那个点,就是吞吐拐点(saturation point)。
python
# locustfile.py —— 逐步加压 + 混合场景骨架
from locust import HttpUser, task, between
import random
class MixedUser(HttpUser):
wait_time = between(0.05, 0.2) # 模拟真实请求间隔
@task(9)
def predict_small(self): # 90% 轻请求
self.client.post("/predict", files=self._img("small"))
@task(1)
def predict_large(self): # 10% 重请求
self.client.post("/predict", files=self._img("large"))③ 执行与监控:客户端和服务端同时看
压测执行时必须同时采集两端数据,只看客户端会误判瓶颈位置:
- 客户端(压测工具输出):QPS、延迟分布、错误率;
- 服务端(Prometheus):CPU、内存、GPU 利用率、排队数、GC/显存水位;
- 对照:客户端 QPS 上去但服务端 CPU 只有 30% → 瓶颈在网络/压测机,不是服务。
GPU 场景还要看两个数:GPU 利用率和显存占用(nvidia-smi 或 DCGM,采集方式见 可观测性落地)。GPU 打满而 CPU 空闲,说明批处理没吃满;GPU 空转而 CPU 打满,说明预处理/反序列化是瓶颈。
④ 指标解读:四个信号看明白
text
QPS
│ ┌── 拐点:吞吐封顶,再压只涨延迟
│ /
│ /
│ /
│────/───────────────────── 延迟开始恶化区
│ /
│ /
│ /
└──────────────────────────▶ 并发四个关键信号:
- 吞吐拐点:QPS 不再随并发增长。拐点之后压测就是「用延迟换 QPS」,要停。
- P99 随并发漂移:低并发时 P99 稳定,高并发时开始线性恶化——线上容量要选在 P99 恶化点之前。
- 错误率抬头:错误率从 0 到 0.1% 到 1% 的跳变,通常是超时、连接池打满、OOM 的前兆。
- GPU 打满 vs CPU 打满:GPU 打满 → 换引擎/量化/加大 batch;CPU 打满 → 优化预处理、加大 worker、上 GPU 或换机。
⑤ 容量计算:两条公式 + 一个系数
text
并发数 ≈ QPS × P99 延迟(秒) # Little's Law 的实用形式
峰值 QPS ≈ 日均 QPS × 峰值系数(通常 3-10 倍)
所需实例数 = 峰值 QPS ÷ 单实例安全 QPS(取拐点的 70-80%)算一个真实例子:
text
日均请求 2000 万 → 日均 QPS ≈ 231 → 峰值系数取 5 → 峰值 QPS ≈ 1155
压测出单实例拐点 400 QPS → 安全容量取 70% = 280 QPS
所需实例 = ceil(1155 / 280) = 5 台三个容易忘的修正:峰值系数要按业务实际取(电商大促 10 倍不止,SaaS 内部工具 2-3 倍);预留 20-30% 余量给流量预测误差;跨机房/容灾再 ×2。硬件差异直接影响容量,选型维度见 硬件。
并发数公式别用反了
常见错误是把「并发」当「QPS」。并发 = QPS × 延迟:延迟 100ms、目标 1000 QPS → 并发 ≈ 100 就够了;同一服务延迟涨到 500ms,同样 1000 QPS 需要 500 并发——延迟恶化会自动放大并发需求,这是服务在峰值时雪崩的机制之一。
四、压测报告模板
压测结果要能让别人(或两周后的你)复现和信任,模板如下:
markdown
# 压测报告:image-classifier v1
## 环境
- 压测机:8C16G 虚拟机,wrk 4.6 / locust 2.30
- 服务机:4C8G,Docker,4 worker,onnxruntime 1.19.2
- 模型:mobilenetv2 ONNX INT8,输入 (1,3,32,32)
## 目标 SLO
- 200 QPS 稳定、P99<100ms、错误率<0.1%
## 结果表
| 并发 | QPS | P50(ms) | P99(ms) | 错误率 | CPU% | 备注 |
| --- | --- | --- | --- | --- | --- | --- |
| 50 | 310 | 12 | 38 | 0% | 55% | 稳态 |
| 100 | 410 | 18 | 72 | 0% | 78% | 接近拐点 |
| 200 | 430 | 45 | 380 | 0.3% | 92% | 拐点后,P99 恶化 |
## 结论
- 单实例安全容量 ≈ 280 QPS(拐点 430 的 65%)
- 线上峰值预估 1155 QPS → 需要 5 台(含 1 台冗余)
- 风险项:P99 在 100 并发后快速恶化,建议先调批处理再压一轮五、常见坑:压测数字为什么不可信
- 压测机成为瓶颈:8 线程 wrk 打不穿 4 worker 的服务,压测机先 CPU 打满。解法:
top看压测机负载,压不动就加线程/换分布式(locust 主从)。 - 预热不足:服务刚启动、JIT/缓存未热,前几十秒数字虚低或虚高。解法:正式记录前预热 30-60 秒。
- 缓存导致虚高:压测请求全命中缓存,测出 5000 QPS,真实流量零缓存命中直接崩。解法:压测用唯一参数或关闭缓存,单独测「真实路径」。
- 没有基线:没压旧版本直接压新版,数字出来不知道是快是慢。解法:优化前后各压一次,同一脚本同一环境(见 模型优化实战 的基线表)。
- 只测 30 秒:内存泄漏、长连接泄漏要 10 分钟以上才现形。解法:稳态场景至少压 10 分钟,同时盯服务端内存曲线(显存泄漏与 OOM 坑④)。
六、压测后的动作:数据要变成决策
压测不是为了出报告,是为了三个决策:上线(SLO 达标且有余量)、扩容(不达标但可以加机器)、优化(不达标且加机器解决不了——比如延迟本身超标)。决策路径:
text
压测结果 ──▶ SLO 达标? ──yes──▶ 上线(留监控,见 /practice/observability-practice)
│
└──no──▶ 是容量问题还是延迟问题?
├─ 容量问题(QPS 不够)──▶ 扩容 / 加 worker / 量化
└─ 延迟问题(P99 超标)──▶ 优化预处理 / 换引擎 / 批处理容量调整后必须重新压测验证,公式只是估算,实测才是答案。
检查清单
- [ ] SLO 先写后测(QPS、P99、错误率三项齐全);
- [ ] 工具选型匹配协议与场景(HTTP/gRPC/分布式/CI);
- [ ] 预热 ≥ 30 秒,稳态场景 ≥ 10 分钟;
- [ ] 客户端与服务端指标同时采集(含 GPU 利用率);
- [ ] 找到吞吐拐点并记录拐点前后 P99;
- [ ] 用容量公式算出实例数并给出安全余量;
- [ ] 报告含环境、参数、结果表、结论四件套;
- [ ] 压测机 CPU 未成为瓶颈(记录压测机负载)。
延伸阅读
- 性能指标与调优 —— 延迟/吞吐指标的定义与调优顺序
- 监控 —— 压测时服务端指标怎么采、SLO 怎么定义
- 可观测性落地 —— 压测期间需要盯的黄金指标与告警
- 从零部署一个模型 —— 最小部署闭环里的压测环节
- 模型优化实战 —— 压测发现延迟不达标后的优化路线
- 框架与平台怎么选 —— 换引擎(Triton/vLLM)后的再压测
参考资料
- wrk(HTTP 基准工具):https://github.com/wg/wrk
- Locust 官方文档:https://docs.locust.io/
- vegeta(HTTP 负载测试):https://github.com/tsenart/vegeta
- ghz(gRPC 压测工具):https://ghz.sh/
- k6 官方文档:https://k6.io/docs/
- Google SRE 手册(SLO 与容量):https://sre.google/sre-book/service-level-objectives/