1. 项目概述当大模型服务遇上真实流量最近在折腾一个基于 vLLM 部署的私有化大模型服务对外提供 OpenAI 兼容的 API。模型跑起来了接口也能调通一切看起来都很美好。但心里总有个疙瘩这服务到底能扛住多少并发延迟表现怎么样会不会在业务高峰期突然“摆烂”这种不确定性对于一个准备上线的服务来说是致命的。于是压力测试就成了上线前必须跨过去的一道坎。我们需要的不是“感觉还行”而是硬邦邦的数据。wrk这个老牌 HTTP 压测工具以其轻量、高效和可脚本化的特点成了我的首选。它不像 JMeter 那样“重”却能精准地模拟大量并发客户端持续对/v1/chat/completions这样的 POST 接口发起“冲锋”。这次测试的目标很明确摸清我们 vLLM 服务在特定硬件和配置下的性能天花板找出潜在的瓶颈并为容量规划提供数据支撑。无论你是刚部署好 vLLM 的开发者还是关心服务稳定性的架构师这套从压测执行到结果分析的实战流程都能给你带来直接的参考价值。2. 压力测试的核心思路与工具选型2.1 为什么是wrk而不是 JMeter 或 Locust在 HTTP 压测工具里JMeter 功能全面但资源消耗大图形界面在自动化流水线中并不友好Locust 基于 Python易于扩展但在极限压测场景下单机性能有时不如用 C 语言写的wrk。对于 vLLM API 这种典型的I/O 密集型、长连接、请求体较大携带对话消息的服务压测工具本身的开销必须尽可能小才能把压力真实地施加到服务端而不是消耗在压测客户端上。wrk采用多线程 事件驱动模型能够用极少的系统资源模拟出极高的并发连接。它的工作模式是启动 N 个线程每个线程维护一批事件循环每个循环处理一批 HTTP 连接。这种架构使得它在进行高并发、短连接的基准测试时数据非常准确。虽然它本身不支持复杂的逻辑如读取 CSV 参数化但通过 Lua 脚本可以轻松实现请求体的动态构建和返回结果的简单校验这对于我们测试固定的聊天补全接口已经足够。注意wrk的优势在于施压能力而非场景编排。如果你的测试需要非常复杂的业务流如先登录再对话可能需要结合其他工具或自行封装。但对于单纯的 API 端点性能摸底wrk是“手术刀”式的选择。2.2 vLLM OpenAI API 接口的压力测试特殊性分析vLLM 提供的/v1/chat/completions接口虽然协议与 OpenAI 官方一致但其后端实现和性能特征与直接调用云端 API 有本质不同计算密集型后端每个请求都会触发 GPU 上的自回归推理生成这是最耗时的部分。延迟主要来自于模型的前向计算而非网络传输。动态批处理PagedAttentionvLLM 的核心优势之一。它会将短时间内到达的多个请求动态批处理到一个 GPU 计算核中执行以此提高吞吐量。这意味着并发请求数Concurrency是一个关键指标。并发太低无法充分利用批处理优势并发太高则可能导致队列堆积和超时。输入输出长度敏感请求的max_tokens生成长度和messages的历史长度直接影响 GPU 的计算量和显存占用。压力测试的请求必须具有代表性最好接近业务真实场景。长尾延迟由于 GPU 计算和调度的不确定性即使平均延迟很低也可能出现个别请求的延迟P99 P999非常高。压力测试必须关注这些分位数延迟它们直接影响用户体验。因此我们的压测设计必须围绕这些特点展开不仅要测出极限 QPS每秒查询率更要分析在不同并发级别下的延迟分布、吞吐量变化以及观察 vLLM 的动态批处理效果。3. 测试环境搭建与wrk配置详解3.1 服务端vLLM部署与关键参数假设我们已经在服务器上部署了 vLLM。压测前服务端的启动参数至关重要它们决定了性能基线。# 示例使用 vLLM 启动一个 Qwen-7B-Chat 模型服务 python -m vllm.entrypoints.openai.api_server \ --model /path/to/qwen-7b-chat \ --served-model-name qwen-7b-chat \ --tensor-parallel-size 1 \ # 单卡推理 --gpu-memory-utilization 0.9 \ # GPU显存利用率目标 --max-num-batched-tokens 2048 \ # 批处理的最大token数影响吞吐 --max-model-len 4096 \ # 模型支持的最大上下文长度 --api-key “your-api-key-here” \ # 可选的API密钥 --port 8000关键参数解析--max-num-batched-tokens: 这是 vLLM 性能调优的“阀门”。它限制了单次批处理中所有请求的 token 总数上限。设置得太小无法充分发挥批处理效率设置得太大可能导致首个请求等待时间过长等待凑批或 OOM。需要根据模型大小、显存和业务场景反复测试找到甜点。--gpu-memory-utilization: 控制在 0.8-0.95 之间为系统和其他进程留出空间。--tensor-parallel-size: 如果是多卡大模型需要设置为 GPU 数量。在启动服务后务必先用一个简单请求验证接口是否正常curl http://localhost:8000/v1/chat/completions \ -H “Content-Type: application/json” \ -H “Authorization: Bearer your-api-key-here” \ -d ‘{ “model”: “qwen-7b-chat”, “messages”: [{“role”: “user”, “content”: “你好请介绍一下你自己。”}], “max_tokens”: 100, “temperature”: 0.7 }’3.2 客户端wrk安装与 Lua 脚本编写在另一台机器避免与服务端争抢资源上安装wrk# Ubuntu/Debian sudo apt-get install wrk -y # CentOS/RHEL sudo yum install wrk -y # 或者从源码编译 git clone https://github.com/wg/wrk.git cd wrk make sudo cp wrk /usr/local/bin/单纯的 GET 请求测试很简单但我们的目标是 POST 一个复杂的 JSON 请求体。这就需要编写 Lua 脚本。创建一个文件比如vllm_test.lua-- vllm_test.lua wrk.method “POST” wrk.headers[“Content-Type”] “application/json” wrk.headers[“Authorization”] “Bearer your-api-key-here” -- 如果服务端启用了认证 -- 初始化阶段可以读取外部文件或定义变量 function init(args) -- 可以定义多个不同的请求内容模拟多样化的用户输入 local messages1 {{role“user”, content“用Python写一个快速排序函数。”}} local messages2 {{role“user”, content“解释一下量子计算的基本原理。”}} local messages3 {{role“user”, content“今天的天气怎么样”}} -- 将请求体模板存入一个表 requests { string.format(‘{“model”: “qwen-7b-chat”, “messages”: %s, “max_tokens”: 128, “temperature”: 0.7}’, json.encode(messages1)), string.format(‘{“model”: “qwen-7b-chat”, “messages”: %s, “max_tokens”: 128, “temperature”: 0.7}’, json.encode(messages2)), string.format(‘{“model”: “qwen-7b-chat”, “messages”: %s, “max_tokens”: 128, “temperature”: 0.7}’, json.encode(messages3)), } counter 1 end -- 每个线程每次发起请求前调用用于生成请求体 function request() -- 循环使用不同的请求体避免所有请求完全一致导致缓存优化失真 local body requests[counter] counter counter 1 if counter #requests then counter 1 end return wrk.format(nil, nil, nil, body) end -- 可选的响应处理函数用于校验或记录 function response(status, headers, body) -- 可以在这里检查返回状态码是否为200或者body是否包含错误信息 -- if status ~ 200 then -- print(“Error: “ .. status .. “, body: “ .. body) -- end end这个脚本做了几件事设置了 POST 方法和请求头。在init函数中准备了三个不同的用户问题模拟真实场景中的请求多样性。request函数循环使用这些请求体确保压测流量不是完全单一的。response函数暂时只做简单注释你可以在这里添加逻辑来验证响应是否正确或者统计错误率。实操心得请求体的多样性很重要。如果所有请求一模一样vLLM 的 KV Cache 可能会带来过于乐观的测试结果这与生产环境不符。max_tokens也不要设得太小比如10这会让生成阶段太快结束无法充分考验持续生成的能力。128 或 256 是一个比较合理的起点。4. 执行压力测试与关键指标解读4.1 设计压测场景与执行命令压力测试不是一次性的而是一个阶梯式的探索过程。我们需要观察系统在不同压力下的表现。场景一低并发基线测试目的是检验服务在无压力下的单请求响应速度。wrk -t2 -c10 -d30s —scriptvllm_test.lua —latency http://your-server-ip:8000/v1/chat/completions-t2: 使用2个线程。-c10: 建立10个HTTP连接并发用户数。-d30s: 持续压测30秒。—latency: 输出详细的延迟分布统计。场景二逐步增加并发寻找吞吐量拐点逐步增加-c参数例如 20, 50, 100, 200。观察 QPS 和平均延迟的变化。当 QPS 不再显著增长而平均延迟开始飙升时说明已经达到或超过了当前配置下的最佳并发点。场景三极限压力与稳定性测试使用较高的并发数例如达到或略超过拐点进行较长时间如3-5分钟的压测观察服务是否稳定错误率是否上升以及 P99 延迟是否变得不可接受。wrk -t4 -c150 -d300s —scriptvllm_test.lua —latency http://your-server-ip:8000/v1/chat/completions4.2 理解wrk的输出报告执行完命令后wrk会输出一份详细的报告。以下面一份示例报告进行解读Running 30s test http://192.168.1.100:8000/v1/chat/completions 2 threads and 100 connections Thread Stats Avg Stdev Max /- Stdev Latency 1.23s 385.62ms 2.15s 85.12% Req/Sec 40.15 12.63 70.00 68.50% Latency Distribution 50% 1.19s 75% 1.45s 90% 1.78s 99% 2.02s 2397 requests in 30.09s, 45.12MB read Requests/sec: 79.66 Transfer/sec: 1.50MB逐项拆解Thread Stats (线程统计)Latency: 延迟。Avg (平均)1.23秒Stdev (标准差)385毫秒。标准差较大说明延迟波动不小这是大模型推理的典型特征。Max2.15秒是最慢的一个请求。Req/Sec: 每个线程每秒完成的请求数。平均 40.15波动Stdev12.63。这个值乘以线程数可以粗略估算总 QPS。Latency Distribution (延迟分布)这是黄金指标比平均延迟更重要。50%(中位数): 1.19秒。一半的请求比这快一半比这慢。90%: 1.78秒。90%的请求延迟在1.78秒以内。这是评估大多数用户体验的指标。99%: 2.02秒。99%的请求延迟在2.02秒以内。P99延迟是衡量服务稳定性的关键。如果 P99 比中位数高很多本例中 2.02s vs 1.19s说明存在“长尾请求”需要分析原因可能是GPU调度、队列堆积等。汇总信息2397 requests in 30.09s: 总请求数。Requests/sec: 79.66:这就是系统的吞吐量 QPS。在当前100并发下系统每秒能处理约80个请求。Transfer/sec: 每秒数据传输量对于API测试关注度不高。性能瓶颈初步分析在这个例子中平均延迟1.23秒QPS 80。对于7B模型生成128个token这个成绩取决于你的硬件比如是A10还是A100。如果QPS随着并发增加而线性增长说明远未达到瓶颈。如果并发从100增加到150时QPS停滞在80左右而平均延迟暴涨到2秒以上那么80 QPS/100并发可能就是当前配置下的一个性能拐点。5. 结合结果进行深度分析与优化方向5.1 从数据到洞察性能瓶颈定位拿到压测数据后我们需要像侦探一样分析线索GPU利用率在压测期间使用nvidia-smi命令观察 GPU-Util计算单元利用率和 Mem-Util显存利用率。如果 GPU-Util 持续在95%以上说明计算是瓶颈。如果 Mem-Util 接近100%则可能是--max-num-batched-tokens设置过高或并发请求太多导致显存不足。QPS与并发关系曲线绘制一张图X轴是并发数-cY轴是 QPS。理想情况下曲线先快速上升然后逐渐平缓最后可能下降。平缓的起点就是最佳并发区域。在这个区域内系统吞吐量高延迟可接受。延迟分布分析重点关注 P90 和 P99 延迟。如果它们随着并发增加而急剧恶化即使平均延迟尚可也意味着用户体验会变差。这可能是因为 vLLM 的调度队列在高压下堆积请求等待时间变长。错误类型在wrk的 Lua 脚本response函数中记录非200状态码。常见的错误有429 Too Many Requests: vLLM 有内置的限流机制被触发。503 Service Unavailable或超时服务端处理不过来请求队列满或单个请求处理超时。5.2 基于测试结果的 vLLM 服务调优实践根据瓶颈分析可以尝试以下调优手段1. 调整 vLLM 服务参数--max-num-batched-tokens: 这是最重要的吞吐量杠杆。适当调高如从2048到4096可以让单个批处理包含更多token提升GPU计算效率从而提高QPS。但副作用是可能增加首个请求的等待延迟等待凑批。需要权衡。--max-parallel-loading-workers: 如果模型加载慢或切换频繁可以增加这个值。--disable-log-stats: 在生产压测时可以禁用日志统计以减少开销。2. 优化请求模式控制生成长度 (max_tokens): 业务允许的情况下尽量限制不必要的长文本生成。使用流式输出 (streamTrue): 对于前端应用使用 Server-Sent Events (SSE) 流式输出可以显著改善用户感知的响应速度首字延迟降低但可能会轻微增加服务端总开销。预热模型: 在正式压测或上线前先发送一些低并发请求让模型加载完毕并建立初始的 KV Cache。3. 架构层面考虑水平扩展: 如果单实例性能已达瓶颈最简单的办法是部署多个 vLLM 实例并用 Nginx 等负载均衡器进行分流。GPU 升级: 更强大的 GPU如 A100 vs A10会带来质的飞跃。量化部署: 使用 GPTQ/AWQ 等量化技术将模型从 FP16 量化到 INT8/INT4可以大幅减少显存占用和提高推理速度从而提升 QPS。这是目前生产部署中非常普遍且有效的优化手段。5.3 常见问题与排查技巧实录在压测过程中你肯定会遇到各种问题。以下是一些典型场景和排查思路问题1压测刚开始 QPS 很低随后逐渐升高。原因vLLM 模型未完全预热。首次推理需要加载权重、构建计算图速度很慢。后续请求复用缓存速度加快。解决正式压测前先进行一轮“预热跑”发送几十个请求让服务进入稳定状态。在分析数据时可以忽略压测最初几秒的数据。问题2wrk报错 “socket: Too many open files”。原因系统打开文件描述符数量限制太低。wrk高并发时会建立大量连接。解决临时提高限制ulimit -n 65536。永久修改需要编辑/etc/security/limits.conf。问题3服务端返回大量 429 或 503 错误。原因触发了 vLLM 的限流或后端处理能力不足。排查检查 vLLM 日志看是否有明确的限流信息。降低压测客户端并发数 (-c)。检查服务端 GPU 和 CPU 使用率确认是否是资源耗尽。查看服务端操作系统dmesg日志排除 OOM内存溢出 killer 终止进程的可能。问题4平均延迟尚可但 P99 延迟极高比如是平均的5倍以上。原因典型的“长尾”问题。可能由于GPU 调度个别请求不幸与某些特别耗时的 CUDA 操作挤在一起。队列堆积瞬时流量过高请求在 vLLM 内部队列中等待时间过长。系统干扰主机上其他进程如日志轮转、监控采集突然占用资源。解决尝试调整--max-num-batched-tokens减少单个批次大小可能让调度更均匀。在客户端加入随机延迟平滑请求流量避免“毛刺”。为 vLLM 服务进程设置更高的 Linux 调度优先级 (nice值)。如果业务允许在负载均衡层设置更短的客户端超时时间并快速失败/重试但需谨慎使用。问题5wrk测试结果不稳定两次测试数据差异很大。原因测试环境存在干扰。解决确保压测客户端和服务端独占机器没有其他高负载任务。每次测试前重启 vLLM 服务确保状态一致。延长压测时间 (-d)例如从30秒增加到2分钟取更稳定的平均值。多次测试取中位数或去掉最高最低后的平均值。压力测试本身不是目的它只是一个诊断工具。真正的价值在于通过测试数据理解你部署的 vLLM 服务在特定硬件和配置下的真实行为边界从而做出合理的架构决策和参数调优。每一次压测都应该带着一个具体的问题去进行比如“把max_num_batched_tokens调到 4096 后吞吐量能提升多少”或者“我们的服务能否满足 50 QPS 且 P99 延迟低于 3 秒的 SLA”。有了明确的目标和严谨的方法数据才会告诉你清晰的答案。