【JMeter 实战:大模型流式接口性能测试(含TTFT与Token统计)】

张开发
2026/4/11 16:51:27 15 分钟阅读

分享文章

【JMeter 实战:大模型流式接口性能测试(含TTFT与Token统计)】
JMeter 实战大模型流式接口性能测试含TTFT与Token统计一、背景与挑战随着大模型LLM的爆发式增长越来越多的业务系统采用流式SSE/WebSocket接口来提供对话生成能力。传统的HTTP请求测试工具如JMeter默认的HTTP Sampler无法有效处理Server-Sent EventsSSE的流式响应更无法统计首Token时间TTFT、生成Token数、每秒Token数等关键性能指标。本文将手把手教你如何使用JMeter的JSR223 Sampler Groovy脚本实现对LLM流式接口的完整压测并输出TTFT、Token统计等专业指标。二、基础功能实现流式接口请求与响应查看2.1 创建线程组与JSR223 Sampler在JMeter中新建线程组Thread Group。右键 → 添加 → Sampler →JSR223 Sampler。语言选择groovy。2.2 配置请求信息URL、Header、Body我们需要发送一个标准的POST请求携带JSON Body并设置SSE必需的HeaderAccept: text/event-streamContent-Type: application/json在JSR223 Sampler的脚本区域编写如下基础代码不含统计功能仅实现请求与显示importjava.net.HttpURLConnectionimportjava.net.URLimportjava.io.*// 从JMeter变量中获取参数可通过用户自定义变量或CSV配置String authorizationvars.get(Authorization)String cookievars.get(Cookie)String conversationIdvars.get(conversationId)String dialogueIdvars.get(dialogueId)// 请求体String requestBody { conversationId: ${conversationId}, dialogueId: ${dialogueId}, modelVersion: YAYI-V1-30B-8K, clientId: jmeter_${__UUID()} } // 目标URL可通过变量灵活配置String targetUrlhttp://your-server/lit/api/dialogue/stream_chat// 创建连接URL urlnewURL(targetUrl)HttpURLConnection conn(HttpURLConnection)url.openConnection()conn.setRequestMethod(POST)conn.setDoOutput(true)conn.setConnectTimeout(30000)conn.setReadTimeout(120000)conn.setRequestProperty(Content-Type,application/json)conn.setRequestProperty(Authorization,authorization)conn.setRequestProperty(Cookie,cookie)conn.setRequestProperty(Accept,text/event-stream)// 发送请求体try(OutputStream osconn.getOutputStream()){os.write(requestBody.getBytes(UTF-8))os.flush()}// 处理响应intresponseCodeconn.getResponseCode()SampleResult.setResponseCode(String.valueOf(responseCode))if(responseCode200){StringBuilder responseContentnewStringBuilder()try(BufferedReader readernewBufferedReader(newInputStreamReader(conn.getInputStream(),UTF-8))){String linewhile((linereader.readLine())!null){responseContent.append(line).append(\n)}}SampleResult.setResponseData(responseContent.toString(),UTF-8)SampleResult.setSuccessful(true)}else{SampleResult.setSuccessful(false)SampleResult.setResponseMessage(HTTP responseCode)}conn.disconnect()三、进阶用法统计TTFT、Token数、每秒Token数3.1 核心指标定义指标含义计算公式TTFT(Time To First Token)从请求发送完成到收到第一个有效内容块的时间首次有效token时间 − 请求发送完成时间输入Token数用户发送给模型的Prompt所消耗的Token数通常由服务端响应中的input_tokens字段提供输出Token数模型生成回复所使用的Token数服务端响应的output_tokens或累计content长度估算平均每秒Token数生成阶段平均每秒产出的Token数量输出Token数 / 生成耗时从首token到结束实际业务中大模型接口通常会在流结束时返回一个包含统计信息的最终data消息例如{agentStatus:stopped,usage:{input_tokens:20,output_tokens:150}}3.2 完整脚本带全指标统计以下脚本在基础功能之上增加了精确的TTFT计算基于请求发送完成时间从最终响应中提取input_tokens/output_tokens计算平均每秒生成Token数output_tokens / 生成耗时将结果存入JMeter变量用于聚合报告或后续断言importjava.net.HttpURLConnectionimportjava.net.URLimportgroovy.json.JsonSlurper// 配置 longTOTAL_TIMEOUT120000// 整个流式响应最大等待时间毫秒此处设为2分钟// 统计变量longttft0// 首Token时间mslongfirstTokenTime0// 首个有效token到达的绝对时间longrequestSentTime0// 请求发送完成时间longlastTokenTime0// 最后一个token的到达时间booleanfirstTokenReceivedfalse// 存储所有data:行内容不含前缀ListStringallDataLinesnewArrayList()StringBuilder rawResponsenewStringBuilder()// 最终指标intinputTokens0intoutputTokens0doubleavgTokensPerSec0.0// 获取JMeter变量 String authorizationvars.get(Authorization)String cookievars.get(Cookie)String conversationIdvars.get(conversationId)String dialogueIdvars.get(dialogueId)String targetUrlvars.get(stream_url)// 从用户变量中获取// 构造请求体String clientIdjmeter_System.currentTimeMillis()String requestBody { conversationId: ${conversationId}, dialogueId: ${dialogueId}, modelVersion: YAYI-V1-30B-8K, clientId: ${clientId} } SampleResult.setSamplerData(requestBody)HttpURLConnection connnulltry{// 1. 创建连接URL urlnewURL(targetUrl)conn(HttpURLConnection)url.openConnection()conn.setRequestMethod(POST)conn.setDoOutput(true)conn.setConnectTimeout(30000)conn.setReadTimeout(180000)conn.setRequestProperty(Content-Type,application/json)conn.setRequestProperty(Authorization,authorization)conn.setRequestProperty(Cookie,cookie)conn.setRequestProperty(Accept,text/event-stream)// 2. 发送请求记录发送完成时间try(OutputStream osconn.getOutputStream()){os.write(requestBody.getBytes(UTF-8))os.flush()}requestSentTimeSystem.currentTimeMillis()// 3. 处理响应码intresponseCodeconn.getResponseCode()SampleResult.setResponseCode(String.valueOf(responseCode))if(responseCode200){// 4. 逐行读取SSE流try(BufferedReader readernewBufferedReader(newInputStreamReader(conn.getInputStream(),UTF-8))){String linewhile((linereader.readLine())!null){rawResponse.append(line).append(\n)// 检测线程中断手动停止测试if(Thread.currentThread().isInterrupted()){SampleResult.setResponseMessage(用户主动停止)break}// 全局超时控制if(System.currentTimeMillis()-requestSentTimeTOTAL_TIMEOUT){SampleResult.setResponseMessage(全局超时${TOTAL_TIMEOUT}ms)SampleResult.setSuccessful(false)break}// 只处理 data: 开头的行if(line.startsWith(data:)){String dataJsonline.substring(5).trim()if(dataJson.isEmpty()||dataJson.equals(ping)||dataJson.equals(ping -)){continue// 跳过心跳包}allDataLines.add(dataJson)// 解析JSONdefjsonnewJsonSlurper().parseText(dataJson)// TTFT 计算 if(!firstTokenReceivedjson.content!nulljson.content.toString().trim().length()0){firstTokenTimeSystem.currentTimeMillis()ttftfirstTokenTime-requestSentTime firstTokenReceivedtruevars.put(TTFT,ttft.toString())}// 记录最后一个token的时间只要有content就更新时间if(json.content!nulljson.content.toString().trim().length()0){lastTokenTimeSystem.currentTimeMillis()}// 从最终消息中提取usage if(json.agentStatusstoppedjson.usage!null){inputTokensjson.usage.input_tokens?:0outputTokensjson.usage.output_tokens?:0// 计算生成耗时从首token到最后一个token的时间差if(firstTokenReceivedlastTokenTimefirstTokenTime){longgenerationDurationlastTokenTime-firstTokenTime// 毫秒if(generationDuration0){avgTokensPerSec(outputTokens*1000.0)/generationDuration}}}}}}// 5. 设置采样结果和断言SampleResult.setSuccessful(true)String resultMsgString.format(TTFT%dms | in_tokens%d | out_tokens%d | avg_tokens/sec%.2f,ttft,inputTokens,outputTokens,avgTokensPerSec)SampleResult.setResponseMessage(resultMsg)SampleResult.setResponseData(rawResponse.toString(),UTF-8)// 将指标存入JMeter变量供聚合报告使用vars.put(ttft_ms,String.valueOf(ttft))vars.put(input_tokens,String.valueOf(inputTokens))vars.put(output_tokens,String.valueOf(outputTokens))vars.put(avg_tokens_per_sec,String.format(%.2f,avgTokensPerSec))// 可选断言输出token数大于0if(outputTokens0){SampleResult.setSuccessful(false)SampleResult.setResponseMessage(resultMsg | 断言失败输出Token数为0)}}else{SampleResult.setSuccessful(false)SampleResult.setResponseMessage(HTTP错误: responseCode)// 读取错误流try(BufferedReader errReadernewBufferedReader(newInputStreamReader(conn.getErrorStream(),UTF-8))){String linewhile((lineerrReader.readLine())!null){rawResponse.append(line).append(\n)}}SampleResult.setResponseData(rawResponse.toString(),UTF-8)}}catch(Exception e){SampleResult.setSuccessful(false)SampleResult.setResponseMessage(异常: e.getMessage())SampleResult.setResponseData(e.toString(),UTF-8)log.error(JSR223执行错误,e)}finally{if(conn!null)conn.disconnect()SampleResult.setLatency(ttft)// 将TTFT作为延迟时间SampleResult.setDataType(org.apache.jmeter.samplers.SampleResult.TEXT)}3.3 关键代码解读代码片段作用requestSentTime System.currentTimeMillis()记录请求发送完成的精确时间作为TTFT计算的起点if (!firstTokenReceived json.content ! null ...)首次遇到非空content字段时计算TTFT并存入变量lastTokenTime System.currentTimeMillis()每次收到有内容的块就更新时间用于计算生成阶段总耗时if (json.agentStatus stopped json.usage ! null)提取最终统计信息输入/输出Token数avgTokensPerSec (outputTokens * 1000.0) / generationDuration计算平均每秒生成Token数3.4 在聚合报告中展示自定义指标为了在JMeter的聚合报告或汇总报告中看到TTFT、Token数等指标需要将它们作为采样器变量或响应消息的一部分然后通过后端监听器如InfluxDBGrafana或简单数据写入器保存。更简便的方法使用SampleResult.setResponseMessage()将关键指标以字符串形式保存然后在查看结果树中直接查看或者通过vars.put()存入变量再使用Debug Sampler检查。四、常见问题与避坑指南4.1 查看结果树不显示流式内容确保在JSR223 Sampler中调用了SampleResult.setResponseData(...)。如果响应过大JMeter默认会截断显示可以在jmeter.properties中调整view.results.tree.max_size。4.2 TTFT始终为0检查是否成功识别到了第一个有效content打印json.content的值确认不为空且不是纯空白字符。确认requestSentTime是否在读取响应之前正确赋值。4.3 无法提取Token统计不同大模型厂商的响应格式不同需要根据实际情况修改提取逻辑。例如某些接口会在最后一条data:中返回[DONE]而没有usage字段此时可能需要自己累计content长度来估算Token数简单方式中文字符数/2英文单词数/0.75。4.4 性能压测时内存溢出流式响应如果非常长几千个tokenallDataLines列表会占用大量内存。建议在压测时只保留必要的统计信息不存储完整响应数据。例如可以只记录首尾时间、累计token长度不存储每一行。五、总结通过JMeter的JSR223 Sampler Groovy脚本我们可以轻松实现对LLM流式接口的性能测试并获得✅ 完整的SSE数据流接收与展示✅TTFT精确到毫秒✅ 输入/输出Token数统计✅ 平均生成速度token/s这些指标能够帮助测试人员客观评估大模型接口的首字延迟和生成吞吐为性能优化和容量规划提供数据支撑。本文为原创转载需注明出处。

更多文章