Skip to content

压测与容量规划:上线前必须回答的三个问题

本页速览 上线前必须回答:能扛多少 QPS、P99 多少、要不要扩容。本文给出压测方法论:工具选型(wrk/locust/vegeta/ghz/k6)、场景设计、指标解读与容量计算公式。

压测(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 界面
ghzgRPC 压测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 概念

② 场景设计:四种压法覆盖真实形态

场景做法回答的问题
固定 QPSvegeta/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
    │        ┌── 拐点:吞吐封顶,再压只涨延迟
    │       /
    │      /
    │     /
    │────/───────────────────── 延迟开始恶化区
    │   /
    │  /
    │ /
    └──────────────────────────▶ 并发

四个关键信号:

  1. 吞吐拐点:QPS 不再随并发增长。拐点之后压测就是「用延迟换 QPS」,要停。
  2. P99 随并发漂移:低并发时 P99 稳定,高并发时开始线性恶化——线上容量要选在 P99 恶化点之前
  3. 错误率抬头:错误率从 0 到 0.1% 到 1% 的跳变,通常是超时、连接池打满、OOM 的前兆。
  4. 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 并发后快速恶化,建议先调批处理再压一轮

五、常见坑:压测数字为什么不可信

  1. 压测机成为瓶颈:8 线程 wrk 打不穿 4 worker 的服务,压测机先 CPU 打满。解法:top 看压测机负载,压不动就加线程/换分布式(locust 主从)。
  2. 预热不足:服务刚启动、JIT/缓存未热,前几十秒数字虚低或虚高。解法:正式记录前预热 30-60 秒。
  3. 缓存导致虚高:压测请求全命中缓存,测出 5000 QPS,真实流量零缓存命中直接崩。解法:压测用唯一参数或关闭缓存,单独测「真实路径」。
  4. 没有基线:没压旧版本直接压新版,数字出来不知道是快是慢。解法:优化前后各压一次,同一脚本同一环境(见 模型优化实战 的基线表)。
  5. 只测 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 未成为瓶颈(记录压测机负载)。

延伸阅读

参考资料