4.5 性能压测与容量评估方法论 「调优之后到底快了多少」「这套硬件最多能扛多少并发」「再翻一倍流量要不要扩容」——这些都是生产决策里每天会被问的问题,而回答它们的唯一依据是规范的压测。但压测是个被严重低估的手艺活:我见过太多团队压测跑一通,拿个「吞吐 X tokens/s」的数字就交差,结果上线后表现完全对不上;也见过有人用错误的工具压错误的接口,得到的「优化效果」全是噪声。 这一节我把压测从「跑个脚本拿数字」提升到「可决策的容量评估方法论」。覆盖五个层面:压测工具怎么选、数据怎么造、指标怎么读、拐点怎么定、结果怎么转化为容量决策。每个环节我都会强调常见陷阱——这些陷阱让 90% 的压测结果失真,而踩过的人不多。
「调优之后到底快了多少」「这套硬件最多能扛多少并发」「再翻一倍流量要不要扩容」——这些都是生产决策里每天会被问的问题,而回答它们的唯一依据是规范的压测。但压测是个被严重低估的手艺活:我见过太多团队压测跑一通,拿个「吞吐 X tokens/s」的数字就交差,结果上线后表现完全对不上;也见过有人用错误的工具压错误的接口,得到的「优化效果」全是噪声。
这一节我把压测从「跑个脚本拿数字」提升到「可决策的容量评估方法论」。覆盖五个层面:压测工具怎么选、数据怎么造、指标怎么读、拐点怎么定、结果怎么转化为容量决策。每个环节我都会强调常见陷阱——这些陷阱让 90% 的压测结果失真,而踩过的人不多。
很多人把压测理解为「测速度」,于是盯着吞吐数字看。但生产决策真正需要回答的是「在满足体验约束的前提下,系统能承载多少负载」。这两个问题完全不同:
压测的核心产物应该是后者——一条「负载-体验」曲线和它的拐点,而不是一个孤立的吞吐数字。下面所有方法论都围绕这个核心展开。
压测工具的选型直接决定结果是否可信。我用过的主要工具:
benchmark_throughput.py:测离线吞吐(一批请求一起算),适合调参对比,但不反映在线服务场景。坑:很多人用这个数字对外承诺线上吞吐,严重失真。benchmark_serving.py:测在线服务(HTTP 请求并发),输出 TTFT、TPOT、吞吐,最接近线上场景。这是我最常用的工具。我的选型建议:调参对比用 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" 这种短 prompt 压出来的吞吐,和线上 5k prompt 的吞吐可能差 5 倍。
数据集三个维度必须贴近线上:
我的做法:从生产日志采样脱敏后的真实 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 秒。生产体验由尾延迟决定,平均数会掩盖尾部的灾难。
坑:聚合陷阱。平均吞吐也有坑——如果压测过程中并发从 1 慢慢加到 100,把全程平均吞吐算出来,会被低并发阶段拖低,看不到真实峰值。正确做法是分档压测(固定并发压一段时间),每档单独算指标。
这是这一节的真正核心。拐点(knee point)是「负载-体验」曲线上体验开始急剧恶化的点,定位拐点就是定位「可服务并发上限」。
操作流程:
分档:把并发从低到高分档(如 1 → 8 → 16 → 32 → 64 → 128 → 256),每档单独压测,持续足够长时间(至少 5 分钟,让系统进入稳态)。
记录每档指标:吞吐、TTFT P50/P90/P99、TPOT、错误率。
画图找拐点:
取小:实际可服务并发 = min(吞吐拐点, 延迟拐点, 错误拐点)。
打安全系数:生产容量 = 可服务并发 × 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),说明延迟比吞吐先扛不住,要按更严格的延迟拐点定容量。
陷阱 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 而非平均)。
压测的最终目的是支撑决策。一份合格的压测报告应该能回答:
一个常被忽略的决策:压测时如果发现吞吐拐点远早于延迟拐点(如吞吐在 64 并发到顶,但延迟到 128 才恶化),说明系统是「吞吐瓶颈」——加并发没用,要换更强硬件或更优并行策略。反之延迟先恶化,是「延迟瓶颈」,要优化 prefill/调度(开 chunked prefill、调 max-num-seqs)。两种瓶颈的处置方向完全不同,压测要能区分。
一次性压测只解决「上线前」的问题,但生产是动态的——业务流量涨了、模型升级了、硬件老化了,都需要重新压测。我团队里把压测做成常态化能力:
核心心态:压测不是上线前的一次性动作,而是持续运营的基础能力。一份好的压测体系,能让你的容量决策从「拍脑袋」变成「看数据」,从「事后救火」变成「事前规划」。
把这一节压缩成一份可照做的 SOP:
benchmark_serving.py 或自写流式客户端,确认支持 SSE。这套 SOP 走下来,你给出的容量结论是「可决策的」(有拐点、有安全系数、有瓶颈诊断),而不是「我跑了一下感觉还行」。压测的价值就在于此——把「感觉」变成「数据」,把「拍脑袋」变成「方法论」。