ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

JMeter大模型服务QPS压测实战:从脚本搭建到结果解读

JMeter大模型服务QPS压测实战:从脚本搭建到结果解读 用JMeter给大模型服务做QPS压测听起来像是把普通HTTP接口压测套路直接搬过来就行但真正跑起来就会发现坑比想象中多。大模型接口单次请求往往要好几秒甚至几十秒才返回流式输出又会让JMeter的响应超时逻辑失真再加上GPU并发、KV Cache、Batch策略这些变量共同作用测出来的QPS数字很容易既不稳定、也不可信。这篇就把我从环境准备、脚本搭建、并发设计到结果解读的完整过程摊开讲重点解决“到底该开多少并发线程”“为什么并发上去了QPS反而掉”“流式接口怎么测”“Beanshell断言会不会拖垮压测”这类问题。适合刚接手大模型服务性能评估、正在做容量规划或者准备对比多个模型推理服务性能的读者参考。1. 先搞清楚大模型压测真正要压的是什么1.1 大模型接口和普通接口的本质差异很多新手接到任务后的第一反应就是把大模型服务当成一个普通的HTTP服务来压随便建一个线程组、填一个接口路径、扔一批并发请求然后看聚合报告里的“Throughput”。这么测出来的数字不是不能用而是你根本不知道它为什么是这个数服务端到底处于什么状态。普通Web接口一次请求的耗时主要取决于数据库查询、下游调用逻辑延迟相对稳定QPS基本代表服务端每秒能处理多少个请求。但大模型接口一个请求内部包含了两个截然不同的阶段预填充阶段需要把用户输入的全部Token做一次前向计算生成阶段则要逐个Token往外推每一步都依赖前面已经生成的Token。这两个阶段的算力消耗完全不一样所以模型的输入长度、输出长度、采样参数都会直接影响一次请求的耗时。同样的模型同样的并发线程数如果Prompt长度从50个Token变成500个Token或者max_tokens从64改成512QPS可能直接差出好几倍。再加上服务端还有Batch策略、KV Cache管理、并发队列这些机制大模型的QPS从来不是一个固定值而是在特定输入输出条件下才成立的相对值。1.2 别把QPS和Token吞吐量搞混我见过不少测试报告把“每秒处理请求数”和“每秒生成Token数”写在同一张图里看起来都在描述性能其实完全不是一回事。QPS衡量的是接口层每秒完成的请求数量也就是一个用户视角的指标它和并发数、响应时间满足类似Littles Law的关系而Token吞吐量衡量的是推理引擎每秒能生成多少个Token它更贴近模型本身的算力上限。举个例子某模型推理服务单请求平均响应时间是4秒并发线程数开到40那么端到端的QPS大约是10左右。但一个请求在4秒内可能生成了300个Token换算下来服务端每秒要产出750个Token才能支撑住这10个并发请求。如果这时候模型能力很强单请求生成速度快比如响应时间缩短到2秒那么同样40个并发就能换来20 QPS但Token吞吐量并不一定翻倍因为还要看Batch里同时跑多少请求。所以在压测前必须明确自己的目标。如果是给业务方回答“这个服务能扛多少并发用户”那测请求级QPS就够了如果是评估模型推理引擎本身能榨出多少算力那得同时记录Token吞吐量和GPU利用率光看QPS很容易低估或高估服务能力。1.3 测试前先定义清楚这四件事我每次开始压测之前都要先写一个简短的测试目标声明哪怕只是给自己看。至少包括四个方面。第一被测接口是同步还是流式。同步接口返回完整JSONJMeter好统计流式接口通过SSE逐段返回JMeter默认会把整个流读完才结束采样响应时间会被“完整生成时长”淹没这会影响你对延迟的判断。第二输入和输出规模定多少。大模型服务的QPS高度依赖Prompt长度和max_tokens。建议在这两个参数上固定档位比如Prompt平均200 Token、输出256 Token然后再去测系统的上限如果业务形态多样可以后再做不同长度组合的矩阵测试。第三QPS口径是什么。是只统计HTTP响应码为200的请求还是把超时、断言失败也算进去是统计所有已发出的请求还是只统计成功完成的请求。我建议最终对外汇报时既给总QPS也给成功QPS和错误率三者一起看否则很容易被一个畸高的裸QPS骗了。第四有没有预热期。大模型服务首次请求往往要加载权重、建立CUDA context延迟会明显偏高这部分数据应当从压测结果里剔除或单独标记。网络模型的推理框架还会有KV Cache逐步填充的过程直接上来就跑满并发会让服务端处于不稳定的冷启动状态。2. 压测前的环境准备与服务确认2.1 大模型服务部署形态与接口确认用JMeter压大模型本质上是在压一个HTTP服务所以第一步要确认这个服务长什么样。当前主流方案里vLLM、Ollama、TGI、llama.cpp这些框架大多提供了OpenAI兼容的接口路径常见为/v1/chat/completions或/v1/models。无论你用的是本地部署的开源模型还是通过内网网关接商业模型API都需要先确认四个信息协议、域名端口、路径、请求体格式。我习惯先用curl做一次冒烟确认服务真的能正常返回并且确认请求体字段和模型的model名称没有写错。以vLLM部署的Qwen2.5-7B为例启动后默认监听8000端口请求体大概长这样curl http://127.0.0.1:8000/v1/chat/completions \ -H Content-Type: application/json \ -d { model: qwen2.5-7b-instruct, messages: [ {role: user, content: 用一句话解释什么是大模型} ], stream: false, max_tokens: 128, temperature: 0.7 }如果curl能拿到JSON响应里面包含choices数组说明接口可用。这一步看起来简单但能省掉压测过程中大量“为什么全是红叉”的排查时间。我在实际项目里还见过因为模型服务前面加了网关网关默认对非流式接口有超时限制导致JMeter这边报错怀疑模型的问题最后发现是网关层把超过30秒的请求提前掐断了。2.2 JMeter安装、启动参数与压测模式选择JMeter本身是Java应用安装前先确认机器上有JDK版本建议8或11。到Apache JMeter官网下载二进制包解压后进入bin目录Windows下运行jmeter.batLinux/macOS下运行jmeter.sh打开GUI。不过做压测时绝对不要用GUI跑正式场景。GUI本身要绘制图表、刷新数据会消耗不少CPU和内存尤其当线程数开到几十上百后JMeter自己的界面都可能卡死测出来的数据根本不准。正确做法是先用GUI搭脚本、调参数、小并发试跑确认链路没问题然后关掉GUI用命令行非GUI模式执行压测。正式压测前建议把JMeter的堆内存调大避免压测机自己提前进入GC抖动。打开bin目录下的jmeter脚本找到JVM_ARGS或HEAP配置按机器情况调整比如export HEAP-Xms2g -Xmx4g注意堆内存不是越大越好压测机的内存资源要留给JMeter线程、网络连接和结果日志调成2到4G通常足够支撑模型压测的并发规模。如果发现JMeter自身CPU已经超过80%更可能是脚本里加了太重的高频断言或监听器而不是单纯堆内存不够。2.3 准备Prompt语料与CSV参数化大模型压测的请求体不能固定写死一个Prompt否则测出来的是模型对某一句话的响应速度缺乏代表性。更合理的做法是准备一批覆盖真实业务分布的Prompt保存成CSV文件通过JMeter的CSV Data Set Config在每次请求时动态读取。我建议CSV里至少包含两列一列是prompt内容一列是期望的max_tokens这样可以把不同输出长度分开测。举个例子prompt,max_tokens 请写一份关于分布式系统的技术分享大纲,256 用一句话解释反向传播算法,64 帮我给新产品想三个营销口号,128在JMeter里添加“CSV Data Set Config”配置如下文件名指向你的prompts.csv变量名称填prompt,max_tokens分隔符用逗号遇到文件结束符时的模式建议设成“继续循环”。这样做的好处是并发线程每次取到不同的Prompt既能模拟真实用户多样化的输入又能避免所有请求都打在同一段Cache上导致测量结果虚高。还需要注意Prompt不是越长越好。如果业务场景是短文本问答那就别造一堆长Prompt去压测出来的QPS对业务没有参考价值。如果拿不准业务分布可以统计历史日志里的Prompt长度分布按比例抽样生成CSV这样测出来的结果才更能代表真实流量。3. 手把手搭一套JMeter大模型压测脚本3.1 线程组与并发模型设计打开JMeter先在测试计划上右键添加一个线程组。这里面的参数直接决定了压测形态。线程数、Ramp-Up Period、循环次数、持续时间四件事要分开理解。第一次跑建议线程数不要直接拉满。先给10个线程Ramp-Up设成30秒让线程在30秒内逐步启动。这样做的原因是大模型服务在GPU上做推理时如果突然涌入大量请求可能导致显存分配失败或者服务端排队失控多个请求同时进入也会让首轮延迟异常夸张结果里全是尖峰。从低并发开始也能顺便确认服务端在低压力下的基线延迟。如果要做固定时长的稳定性压测可以把循环次数勾选为“永远”然后在“调度器配置”里勾选持续时间比如持续600秒。这样JMeter会均匀地跑满10分钟便于观察QPS波动和服务端的长期稳定性。注意Ramp-Up不宜过长否则前面的线程还在启动后面的线程都已经跑完了统计窗口会被拉扁。关于并发模型我见过两种路线一种是固定并发跑一段时间直接看结果另一种是阶梯加压比如先20并发跑2分钟再40并发跑2分钟再80并发跑2分钟。阶梯加压更适合找服务端性能拐点推荐在压测计划里直接使用“Ultimate Thread Group”插件把阶梯关系写成表格实施起来很清晰。3.2 HTTP请求配置与非流式/流式处理线程组下面添加“HTTP请求采样器”。如果是给所有请求都设置同样的协议、域名、端口建议先用一个“HTTP请求默认值”把这些公共参数配好之后每个采样器只管自己的路径和请求体避免重复修改。具体到ChatCompletion接口关键配置有这么几个。协议选http或https服务器名称填模型服务所在机器IP或域名端口号按实际填比如8000路径填/v1/chat/completions请求方式选POST。然后切到“Body Data”页签写入JSON模板。注意模板里的Prompt要从CSV变量中读取所以写成这样{ model: qwen2.5-7b-instruct, messages: [ {role: user, content: ${prompt}} ], stream: false, max_tokens: ${max_tokens}, temperature: 0.7 }同时要在“HTTP Header Manager”里添加Content-Type请求头值为application/json。如果服务端还要求Authorization就在Header Manager里加对应的鉴权头。超时参数非常关键。连接超时一般设1000到3000毫秒就够了大模型服务在线的话TCP握手很快但响应超时一定不能按普通接口的标准来设。我建议初始设成180秒等看了实际响应时间再收敛。有些人把响应超时设成10秒结果所有稍微慢点的请求全被JMeter判定失败测出来的错误率虚高完全不知道是服务超时还是压测设置太激进。流式接口的情况需要单说。如果请求体里stream为true服务端会用SSE分块返回数据JMeter的HTTP采样器会一直读取直到服务端关闭连接或到达响应超时。这时候测出来的“Sample Time”是整个流式生成过程持续的时间而不是首Token延迟。对于QPS统计来说这仍然可以表示“每秒完成的完整请求数”但响应时间的含义就变了写报告时得明确说明。我通常的做法是用非流式接口评估系统的容量上限用流式接口验证业务真实体验再通过服务端日志或独立脚本补充统计首Token耗时。3.3 断言与响应校验怎么加才不拖累压测很多人喜欢在压测脚本里加“Beanshell断言”我之前也这么干过后来发现它在大模型压测里是个双刃剑。Beanshell本身是一个解释执行的脚本引擎每处理一个请求都要额外执行脚本代码在高并发下JMeter进程的CPU会被脚本引擎吃掉不少压测机自己先成了瓶颈。如果只是想判断请求是否成功最简单的做法是使用JMeter自带的“响应断言”。添加断言后把“响应文本”选成要匹配的内容再填测试模式。对大模型接口来说可以匹配返回体中包含choices字段这样能确认服务端确实返回了完整结果而不是一个空壳或错误信息。如果你确实需要做更复杂的校验比如从返回体里提取内容长度或者把返回的JSON解析出来判断某个字段的取值范围那不要再用Beanshell改用“JSR223 断言”同时语言选择Groovy。JMeter从5.x开始对Groovy的支持已经非常成熟脚本引擎的缓存机制也更合理性能比Beanshell好一个数量级。下面是个最简单的Groovy断言示例def resp prev.getResponseDataAsString(); if (!resp.contains(\choices\)) { prev.setSuccessful(false); prev.setResponseMessage(response没有choices字段); }但即便用Groovy也要控制脚本复杂度。压测采样器里每写一行脚本都意味着每个请求要付出额外的解析开销。对大模型压测来说只要HTTP状态码正常、返回体包含choices、错误率在可接受范围内就已经能支撑QPS结论了。真正要追查返回内容是否正确应该单独用功能测试脚本去验证而不是在压测过程里扛着繁重的断言跑量产并发。4. 执行压测与结果解读4.1 用命令行方式跑压测并生成HTML报告脚本在小并发下验证通过后保存测试计划为jmx文件。正式压测时关闭GUI进入JMeter的bin目录执行命令jmeter -n -t chat_model_test.jmx -l result.jtl -e -o report这条命令会以非GUI模式加载测试计划把原始结果记录到result.jtl文件并生成一份HTML报告放到report目录。如果report目录已经存在且非空JMeter会报错记得先清空或换个目录。执行过程中JMeter会在控制台输出当前进度每隔一段时间打印一次摘要包含已完成的请求数、平均响应时间、错误率、吞吐量等。正式压测时不要盯着GUI看就让它跑但是可以在另一台机器上通过SSH连接模型服务端用nvidia-smi观察GPU利用率和显存变化这才是判断服务端是否打满的关键证据。压测结束后打开report目录里的index.html能看到很丰富的图表包括响应时间分布、吞吐量趋势、活跃线程数变化。线上发布报告的时候我会优先贴这些图因为它们比聚合报告里的单个数字更能说明问题尤其是能看出吞吐量是否在某个时间段突然塌陷。4.2 聚合报告里的关键指标怎么读压测结束后可以打开result.jtl或者在GUI里用“聚合报告”监听器看一眼汇总数据。聚合报告里有一堆字段真正需要关注的没几个。Sample表示总请求数Average是平均响应时间单位毫秒Error%是错误率Throughput就是每秒事务数在ChatCompletion接口场景下可以近似理解成QPS。Received KB/sec、Avg. Bytes这些字段主要用于判断响应体大小是否异常模型返回内容越长这个值越高不必过度解读。需要注意的是Throughput在JMeter里的计算方式是所有样本数除以从第一个请求开始到最后一个请求结束的总时间。如果测试过程中线程是逐步启动的或者中间有长时间Ramp-Up细分时段的吞吐量会比聚合值高不少。所以做结论时我更推荐看随时间变化的“吞吐量”趋势图而不是只看一个聚合数字。还要检查一个容易忽略的点线程数虽然设置成50但实际有多少线程真正发出了请求。如果线程还没全部启动压测就已经结束或者调度器配置错误导致线程根本没跑起来那Sample数会异常偏低。这种情况我见过不止一次排查时先看报告里的Active Threads趋势确认并发放到了目标值。4.3 用QPS计算公式反推并发线程数做容量规划时最常被问到的就是“要压到目标QPS到底得开多少并发线程”。理论上有一个很基础的公式QPS 并发数 / 平均响应时间。反过来就是并发数 目标QPS × 平均响应时间这里的响应时间单位是秒。举个例子假设目标QPS是20小并发下测出平均响应时间是5秒那就需要大约100个并发线程公式是20×5100。但这只是初值估算真实的大模型服务一定会在并发上升后出现响应时间恶化因为GPU算力有限请求排队的现象会越来越明显所以实际需要的线程数通常会比理论值更多。更稳妥的做法是阶梯加压找拐点。从20并发起步每次翻倍往上压记录每个并发档位下的QPS和平均响应时间直到QPS不再随并发增长甚至下降这个转折点附近就是服务端的容量上限。我把这种数据记成一张简单的小表比如并发20时QPS 3.5、并发40时QPS 6.2、并发80时QPS 9.8、并发160时QPS 11、并发320时QPS 9.6能看到从160到320并发之间QPS开始停滞或回落。这个“回落点”比任何公式都更能说明问题。如果遇到并发上涨但QPS不涨还要区分是不是压测机自己线程调度达到了极限。这时看一眼压测机CPU使用率如果JMeter进程已经吃满CPU那瓶颈在压测机不在模型服务端需要换更高级的分布式压测方案或者减少脚本里的无用监听器和断言。5. 我在实战中踩过的坑和排查思路5.1 现象一QPS上不去GPU利用率也不高这是最让人头疼的情况并发也加了请求也没报错但QPS就是卡在一个不高的水平一看GPU利用率只有40%显存也没占满。如果GPU和显存都没到瓶颈说明瓶颈不在推理算力而在别的地方。最常见的原因是请求并发根本没打到模型服务端卡在TCP连接或超时设置上。比如JMeter的响应超时设得太短大量请求被JMeter提前判定失败服务端其实已经生成了结果但来不及传回。排查看错误日志往往能看到大量SocketTimeout异常。另一个常见原因是模型服务端的并发队列限制很多推理服务为了保证稳定性允许同时在批的请求数有上限比如vLLM的max_num_seqs、Ollama的OLLAMA_NUM_PARALLEL一旦超出新请求要么排队要么被拒导致QPS被卡在配置值附近GPU永远打不满。这时调大服务端的最大并发批处理限制往往立竿见影。还有一种情况是Prompt太短导致预填充阶段变短而解码阶段每一批能处理的请求数受KV Cache限制GPU利用率自然上不去。这种情况想提升QPS要么增加输入长度要么调整Batch策略要么用更大的并发填满解码阶段。5.2 现象二压测机先倒下了模型服务还没出汗大模型单请求耗时长普通JMeter压测机的几百个线程通常就能压出足够压力但并不是说压测机永远不会成为瓶颈。有些人为了追求并发数把线程组设成500、甚至1000结果JMeter本机的CPU飙到90%以上聚合报告里的响应时间图出现大量毛刺这种毛刺反映的其实是JMeter线程调度延迟不是模型服务变慢。要避免压测机成为瓶颈第一原则是非GUI模式压测第二原则是去掉所有没必要保留的监听器。很多人习惯挂着“查看结果树”看实时数据这是压测大忌尤其在大并发下面查看结果树会缓存大量响应体直接把内存吃光。第三原则是合理设置日志和断言减少每个请求的附加开销。如果真的需要上千并发才能压满目标系统单机JMeter撑不住可以考虑用JMeter的分布式压测模式多台压测机同时打一个目标但要注意每台压测机的时间同步和结果合并复杂度会高一些。5.3 现象三流式接口在JMeter里统计出来的延迟异常偏高流式接口的响应时间问题前面提过不少这里是实测中遇到的具体表现。用stream为true压测时JMeter采样器会持续读取SSE流直到最后一条消息返回所以聚合报告里的Average响应时间会明显大于业务的“首Token时间”。如果业务面当时反馈“交互感觉挺快但压测报告说平均响应时间8秒”两边对不上问题就出在统计口径上。解决思路分两层。第一层是明确压测目的如果只关心系统的最大吞吐能力那流式和非流式都可以用只要口径一致如果想验证流式场景的首Token体验JMeter原生HTTP采样器不好满足建议写一个独立的小脚本用Python配合SSE解析逐条记录首Token到达时间JMeter则只统计整体完成请求数和错误率。第二层是不要把流式响应的响应超时设得太短因为模型生成长度变长时整个流式过程可能持续几十秒响应超时按180秒设置都不过分。5.4 现象四Beanshell断言让压测机CPU居高不下这个问题我特意放在最后说是因为它的隐蔽性很强。压测脚本表面看没什么问题线程数不高请求也正常但任务管理器里JMeter进程的CPU占用率莫名其妙飙高吞吐量数据波动极大。后来定位到是脚本里的Beanshell断言太频繁解释执行引擎每次都把整个返回体转成字符串再解析大模型响应体不仅大而且JSON解析逻辑复杂直接把压测机拖垮。我现在的习惯是能用响应断言解决的绝不用脚本能用Groovy解决的绝不用Beanshell能在脚本外解决的绝不在脚本里做。大模型压测的关键是压模型服务而不是验证模型回答质量所以保留最轻量级的断言确保请求有成功返回就可以了。真要验证返回内容的语义、格式那属于功能测试的范畴不该混进压测脚本里。5.5 常见问题速查表现象可能原因处理办法请求全部Connection refused模型服务没启动或端口错误先用curl冒烟确认接口可用错误率集中为Response timeout响应超时设置太短调大响应超时到120秒以上并发涨了但QPS不动服务端max_num_seqs或队列限制调整推理服务的并发批处理限制GPU利用率高但QPS低输出Token太长或请求体太重降低max_tokens或减少单请求输出长度JMeter自身CPU 90%Beanshell断言、结果树等开销大换成Groovy或响应断言关闭查看结果树聚合报告吞吐量波动剧烈压测机线程调度或Ramp-Up过快降低线程数拉长Ramp-Up分开多次迭代流式响应时间比业务感知高JMeter统计的是完整流式时长单独用脚本测首TokenJMeter记录总QPS最后说一点我自己的习惯。每轮压测开始前我都会把模型、Prompt长度、max_tokens、并发数、Ramp-Up、持续时间记成一行测试参数表报告里贴上这张表不然压测结束时光看QPS数字根本没法复盘。看结果时永远先看错误率再看吞吐量错误率超过0.5%的QPS我不会写进容量结论。测试完成后保留JTL文件后面用Python脚本自己算P95、P99分位点比GUI里的聚合报告灵活得多这也是我一直坚持用非GUI模式落盘数据的原因。希望这篇能帮你少踩几个坑。
返回列表