4.5 性能压测与容量评估方法论


文档摘要

4.5 性能压测与容量评估方法论 「调优之后到底快了多少」「这套硬件最多能扛多少并发」「再翻一倍流量要不要扩容」——这些都是生产决策里每天会被问的问题,而回答它们的唯一依据是规范的压测。但压测是个被严重低估的手艺活:我见过太多团队压测跑一通,拿个「吞吐 X tokens/s」的数字就交差,结果上线后表现完全对不上;也见过有人用错误的工具压错误的接口,得到的「优化效果」全是噪声。 这一节我把压测从「跑个脚本拿数字」提升到「可决策的容量评估方法论」。覆盖五个层面:压测工具怎么选、数据怎么造、指标怎么读、拐点怎么定、结果怎么转化为容量决策。每个环节我都会强调常见陷阱——这些陷阱让 90% 的压测结果失真,而踩过的人不多。

4.5 性能压测与容量评估方法论

「调优之后到底快了多少」「这套硬件最多能扛多少并发」「再翻一倍流量要不要扩容」——这些都是生产决策里每天会被问的问题,而回答它们的唯一依据是规范的压测。但压测是个被严重低估的手艺活:我见过太多团队压测跑一通,拿个「吞吐 X tokens/s」的数字就交差,结果上线后表现完全对不上;也见过有人用错误的工具压错误的接口,得到的「优化效果」全是噪声。

这一节我把压测从「跑个脚本拿数字」提升到「可决策的容量评估方法论」。覆盖五个层面:压测工具怎么选、数据怎么造、指标怎么读、拐点怎么定、结果怎么转化为容量决策。每个环节我都会强调常见陷阱——这些陷阱让 90% 的压测结果失真,而踩过的人不多。

第一原则:压测回答的不是「多快」,是「能扛多少」

很多人把压测理解为「测速度」,于是盯着吞吐数字看。但生产决策真正需要回答的是「在满足体验约束的前提下,系统能承载多少负载」。这两个问题完全不同:

  • 「多快」是单点性能,回答的是「一个请求要多久」。
  • 「能扛多少」是容量边界,回答的是「多少请求同时来,系统还撑得住」。

压测的核心产物应该是后者——一条「负载-体验」曲线和它的拐点,而不是一个孤立的吞吐数字。下面所有方法论都围绕这个核心展开。

```mermaid flowchart LR A[压测目标] --> B[不是: 多快?] A --> C[是: 在体验约束下能扛多少?] C --> D[产物: 负载-体验曲线] D --> E[拐点 = 可服务并发上限] E --> F[× 安全系数 = 生产容量] ```

工具链:选对工具就成功一半

压测工具的选型直接决定结果是否可信。我用过的主要工具:

  • vLLM 自带 benchmark_throughput.py:测离线吞吐(一批请求一起算),适合调参对比,但不反映在线服务场景。:很多人用这个数字对外承诺线上吞吐,严重失真。
  • vLLM 自带 benchmark_serving.py:测在线服务(HTTP 请求并发),输出 TTFT、TPOT、吞吐,最接近线上场景。这是我最常用的工具。
  • Locust / wrk / k6:通用 HTTP 压测工具,灵活但要自己写流式处理。:通用工具默认不支持 SSE 流式响应,用它压 vLLM 的流式接口会等整个响应生成完才记录,TTFT 完全测不准。
  • 自定义脚本:业务侧真实调用模式的复刻,最贴近线上,但开发成本高。

我的选型建议:调参对比用 benchmark_serving.py,容量评估用业务真实流量回放(自己写脚本)。绝不要用通用 HTTP 工具压流式接口——这是最常见的压测错误。

关键检查:工具必须支持流式(SSE)。验证方法是看工具输出里有没有 TTFT 这个指标——没有就说明它没在读流,结果不可信。

# benchmark_serving.py 典型用法(流式压测) python benchmark_serving.py \ --backend vllm \ --base-url http://localhost:8000 \ --model <模型名> \ --dataset-name <真实数据集> \ --num-prompts 1000 \ --request-rate 10 # 每秒发 10 个请求

数据集:用真实分布,不用「hi」

压测结果失真的最大来源是数据集不真实。用 "hi" 这种短 prompt 压出来的吞吐,和线上 5k prompt 的吞吐可能差 5 倍。

数据集三个维度必须贴近线上

  1. prompt 长度分布:采真实流量的 P50/P90/P99,不要只看平均。一条 20k 的长 prompt 能拖垮整个 batch 的延迟。
  2. 生成长度分布:不同业务生成长度差异巨大(摘要 200 token、代码生成 2000 token)。压测时要按业务真实生成长度采样。
  3. 请求到达模式:是均匀到达(request-rate 固定)还是突发(burst)?线上往往是突发的,压测最好用真实流量的到达时间戳回放。

我的做法:从生产日志采样脱敏后的真实 prompt(500~2000 条),存成数据集,每次压测用同一份,保证可比性。:很多人每次压测随机生成不同 prompt,两次结果差 30% 都不知道是参数变化还是数据集波动。

指标:四个指标缺一不可,且必须看分位数

压测要同时看四个指标,少一个都会误判:

指标 含义 为什么重要
吞吐(tokens/s 或 requests/s) 单位时间产出 衡量系统「功率」,是容量上限的主要依据
TTFT(首 token 延迟) 请求到第一个 token 的时间 体验的「响应感」,决定用户感知的快慢
TPOT(每 token 延迟) 生成每个 token 的间隔 流式输出的流畅度
错误率 失败请求比例 健康 baseline,>0.1% 说明系统过载或异常

关键原则:必须看分位数(P50/P90/P99),不能只看平均。这是压测最常见的陷阱:平均延迟 500ms 看起来很健康,但 P99 可能是 5 秒——意味着每 100 个用户就有 1 个等 5 秒。生产体验由尾延迟决定,平均数会掩盖尾部的灾难。

```mermaid flowchart TD A[压测指标] --> B[吞吐: 容量上限依据] A --> C[TTFT: 响应感] A --> D[TPOT: 流畅度] A --> E[错误率: 健康 baseline] B --> F[必须看分位数 P50/P90/P99] C --> F D --> F E --> G[>0.1% 即过载] ```

坑:聚合陷阱。平均吞吐也有坑——如果压测过程中并发从 1 慢慢加到 100,把全程平均吞吐算出来,会被低并发阶段拖低,看不到真实峰值。正确做法是分档压测(固定并发压一段时间),每档单独算指标。

拐点定位法:压测的核心方法论

这是这一节的真正核心。拐点(knee point)是「负载-体验」曲线上体验开始急剧恶化的点,定位拐点就是定位「可服务并发上限」。

操作流程

  1. 分档:把并发从低到高分档(如 1 → 8 → 16 → 32 → 64 → 128 → 256),每档单独压测,持续足够长时间(至少 5 分钟,让系统进入稳态)。

  2. 记录每档指标:吞吐、TTFT P50/P90/P99、TPOT、错误率。

  3. 画图找拐点

    • 吞吐拐点:吞吐不再随并发线性上升、甚至开始下降的档位。这是系统吞吐上限。
    • 延迟拐点:TTFT P99 开始陡升(如从 1 秒跳到 5 秒)的档位。这是体验可承受的并发上限。
    • 错误拐点:错误率开始抬头的档位。这是硬上限。
  4. 取小:实际可服务并发 = min(吞吐拐点, 延迟拐点, 错误拐点)

  5. 打安全系数:生产容量 = 可服务并发 × 0.7~0.8,留余量给突发和长尾。

```mermaid flowchart TD A[分档压测 1→8→16→32→64→128] --> B[每档记录指标] B --> C[画 负载-体验 曲线] C --> D1[吞吐拐点: 吞吐不再上升] C --> D2[延迟拐点: P99 陡升] C --> D3[错误拐点: 错误率抬头] D1 --> E[取 min = 可服务并发] D2 --> E D3 --> E E --> F[× 0.7~0.8 = 生产容量] ```

真实案例:某次压测 DeepSeek V4 在 8 卡 H100 上,分档结果:

并发 吞吐(tokens/s) TTFT P99 错误率
8 1800 0.3s 0%
16 3200 0.5s 0%
32 5800 0.9s 0%
64 8500 1.8s 0%
128 9200 5.5s 0.3%
256 8800 18s 2.1%

分析:吞吐拐点在 128(之后下降),延迟拐点也在 128(P99 从 1.8s 跳到 5.5s),错误拐点也在 128。所以可服务并发 = 128,生产容量 = 128 × 0.7 ≈ 90结论:这套硬件标称「可服务并发 90」,超过 90 就要扩容。

启示:三个拐点重合不是巧合——系统过载时三个指标同时恶化。如果不重合(如吞吐拐点在 128 但延迟拐点在 64),说明延迟比吞吐先扛不住,要按更严格的延迟拐点定容量。

压测的常见陷阱(90% 的人踩过)

陷阱 1:压测不带流式。用同步 HTTP 工具压流式接口,TTFT 完全失真。对策:工具必须支持 SSE,验证方法看工具是否输出 TTFT。

陷阱 2:冷启动污染。首次请求含 CUDA 编译、权重 warmup 等开销,前几轮数据要剔除。对策:先 warmup 一轮(如发 100 个请求),再开始正式压测。

陷阱 3:只压平均。只看平均延迟/吞吐,看不到尾延迟。对策:所有指标必须看分位数。

陷阱 4:压测时长不够。压 30 秒就停,系统还没进稳态,指标不稳定。对策:每档至少压 5 分钟,观察指标是否进入稳态(方差变小)。

陷阱 5:客户端瓶颈。压测工具的客户端机器性能不够、或客户端并发数设错,导致「测的是客户端不是服务端」。对策:压测时盯服务端 GPU 利用率——如果 GPU 没满载但吞吐上不去,多半是客户端瓶颈。

陷阱 6:背景流量干扰。在已有线上流量的实例上压测,结果被线上流量污染。对策:压测在隔离的实例上做,不要和生产流量混跑。

陷阱 7:忽略 GC/JIT 抖动。Python GC、CUDA Graph 重新录制会引发周期性延迟尖刺。对策:压测时长足够,让尖刺在统计上被捕捉到(看 P99 而非平均)。

从压测结果到容量决策

压测的最终目的是支撑决策。一份合格的压测报告应该能回答:

  1. 单实例容量:这套硬件 + 这个配置,可服务并发是多少?(拐点 × 安全系数)
  2. 集群容量:N 个实例的集群,可服务并发是 N × 单实例容量吗?(不是简单线性,多实例有负载均衡损耗、共享存储瓶颈,要实测)
  3. 扩容触发点:什么负载水位(如 70%)触发扩容?基于容量上限算。
  4. 降级触发点:什么负载水位(如 90%)开始限流?
  5. 成本-性能曲线:不同硬件/配置组合的「每 QPS 成本」是多少?用于选型决策。

一个常被忽略的决策:压测时如果发现吞吐拐点远早于延迟拐点(如吞吐在 64 并发到顶,但延迟到 128 才恶化),说明系统是「吞吐瓶颈」——加并发没用,要换更强硬件或更优并行策略。反之延迟先恶化,是「延迟瓶颈」,要优化 prefill/调度(开 chunked prefill、调 max-num-seqs)。两种瓶颈的处置方向完全不同,压测要能区分。

把压测做成常态化能力

一次性压测只解决「上线前」的问题,但生产是动态的——业务流量涨了、模型升级了、硬件老化了,都需要重新压测。我团队里把压测做成常态化能力:

  • 每次模型/配置变更都要回归压测:升级模型、改启动参数、换硬件,都要跑一遍标准压测脚本,对比容量变化。:很多人模型升级后只验证「能跑」,不验证「容量没退化」,结果上线才发现吞吐掉了一半。
  • 线上影子流量压测:把线上真实流量复制一份打到测试实例,持续压测,捕获生产流量模式下的真实表现。比离线压测更真实。
  • 容量水位监控:基于压测定的容量上限,监控生产负载/容量的比值,>70% 自动告警扩容。
  • 压测结果入库:每次压测的配置、结果、拐点存档,形成「配置-容量」对应表,未来选型直接查。

核心心态:压测不是上线前的一次性动作,而是持续运营的基础能力。一份好的压测体系,能让你的容量决策从「拍脑袋」变成「看数据」,从「事后救火」变成「事前规划」。

收尾:一个完整的压测 SOP

把这一节压缩成一份可照做的 SOP:

  1. 选工具:用 benchmark_serving.py 或自写流式客户端,确认支持 SSE。
  2. 造数据:从生产采样脱敏 500~2000 条 prompt,记录长度分布。
  3. 定指标:吞吐、TTFT P50/P90/P99、TPOT、错误率。
  4. 分档压测:并发从低到高分档,每档 ≥5 分钟,先 warmup。
  5. 画图找拐点:吞吐拐点、延迟拐点、错误拐点,取 min。
  6. 打安全系数:可服务并发 × 0.7 = 生产容量。
  7. 诊断瓶颈类型:吞吐瓶颈 vs 延迟瓶颈,定优化方向。
  8. 入库归档:配置 + 容量结果存档,供未来选型。

这套 SOP 走下来,你给出的容量结论是「可决策的」(有拐点、有安全系数、有瓶颈诊断),而不是「我跑了一下感觉还行」。压测的价值就在于此——把「感觉」变成「数据」,把「拍脑袋」变成「方法论」。


作者与出处
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 运行报错的菜鸟的小龙虾 转发
评论区 (0)
U