JMeter 完整实战——线程组、采样器、断言、监听器完整使用指南
JMeter 完整实战——线程组、采样器、断言、监听器完整使用指南
适读人群:后端开发工程师、测试工程师 | 阅读时长:约16分钟 | 核心价值:从零开始用JMeter做一场完整的压测,理解每个组件的作用和正确用法
第一次用JMeter就翻车的经历
2018年刚开始做性能测试,师傅让我用JMeter压一个用户登录接口。我下载了JMeter,拖了个Thread Group,加了个HTTP Request,填上URL和参数,点了运行按钮。
Aggregate Report显示TPS 12000,P99是2ms。
我兴奋地去跟师傅说:这接口性能超级好,完全没问题。
师傅盯着数据看了一会儿,问我:"你断言加了吗?"
我说:"加了什么断言?"
师傅让我看一眼Response:服务器返回的全是{"code":500,"msg":"系统繁忙"}——接口一直在报错,JMeter因为HTTP状态码是200所以没有计入错误,TPS当然高,P99当然低,因为返回500比正常返回快多了。
那天我明白了一件事:JMeter的组件不是摆设,每个都有存在的理由。
JMeter 核心架构
在深入每个组件之前,先建立一个整体认知。
JMeter的压测计划(Test Plan)是一棵树,父节点的配置对子节点生效,兄弟节点之间相互独立。基本结构是:
Test Plan
└── Thread Group(线程组,核心容器)
├── Config Element(配置元件,提供公共配置)
├── Sampler(采样器,发送实际请求)
├── Pre Processor(前置处理器,请求前执行)
├── Post Processor(后置处理器,从响应中提取数据)
├── Assertion(断言,验证响应正确性)
└── Listener(监听器,收集和展示结果)Thread Group(线程组)详解
线程组是一切的起点,控制虚拟用户的数量和行为。
核心参数
Number of Threads(线程数) 代表并发用户数。注意:线程数不等于TPS。TPS = 线程数 / 平均响应时间(秒)。200线程,平均响应200ms,理论TPS是1000。
Ramp-Up Period(启动时间) 所有线程在多少秒内全部启动完成。设为0表示瞬间启动所有线程,会对服务器造成瞬间冲击,适合测试系统承受突增流量的能力。设为60表示在60秒内均匀启动,适合模拟正常增长。
Loop Count(循环次数) 每个线程执行多少次循环。设为Infinite配合Duration使用,是持续压测的正确方式。
Duration(持续时间) 配合Infinite Loop使用,控制压测总时长。线上压测建议至少30分钟,稳定性测试4-8小时。
高级线程组
标准Thread Group功能有限,实际项目中推荐安装bzm - Concurrency Thread Group(阶梯线程组):
目标并发:200
启动时间:60秒(在60秒内逐步达到200)
保持时间:600秒(保持200并发10分钟)
结束时间:30秒(30秒内线程逐步退出)这样可以精确控制压测的各个阶段,压力曲线更平滑。
Sampler(采样器)
采样器是真正发送请求的组件。最常用的是HTTP Request Sampler。
HTTP Request 关键配置
协议:https
服务器名称:api.example.com
端口号:443
方法:POST
路径:/api/v1/order/create
Content-Type:application/json
Body Data:
{
"userId": "${userId}",
"productId": "${productId}",
"quantity": 1
}注意事项:
- 用变量(
${变量名})而不是硬编码,配合CSV Data Set Config参数化 - POST请求记得在HTTP Header Manager里设置
Content-Type: application/json - HTTPS接口需要在HTTP Request Defaults里勾选"Use KeepAlive"
HTTP Header Manager
独立组件,添加请求头:
Authorization: Bearer ${token}
Content-Type: application/json
Accept: application/json
X-Request-ID: ${__UUID()}${__UUID()}是JMeter内置函数,每次请求生成唯一ID,对需要幂等性验证的接口很重要。
Config Element(配置元件)
HTTP Request Defaults
统一配置所有HTTP请求的默认值,避免每个Sampler都重复填写:
协议:https
服务器名称:api.example.com
端口:443
编码:UTF-8放在Thread Group顶层,对所有子Sampler生效。
CSV Data Set Config(参数化核心)
这是最重要的配置元件,没有之一。
假设你准备了一个user_ids.csv文件:
user_id,password
10001,test123
10002,test456
10003,test789
...(10万行)CSV Data Set Config配置:
文件名:/data/user_ids.csv
文件编码:UTF-8
变量名称:userId,password
分隔符:,
是否允许引号数据:True
遇到文件结束时的处理:循环(Continue)
线程共享模式:All threads配置完成后,在HTTP Request里用${userId}和${password}引用即可。每个线程每次迭代会从CSV取不同的行,实现真正的参数化。
Assertion(断言)
断言是区分"有效压测"和"无效压测"的关键。 没有断言的压测数据毫无意义。
Response Assertion(响应断言)
最基础的断言,检查响应内容:
Apply to:Main sample and sub-samples
测试字段:Response Body
模式匹配规则:Contains
测试模式:"code":200也可以断言HTTP状态码:
测试字段:Response Code
模式匹配规则:Equals
测试模式:200JSON Assertion
当接口返回JSON时,用JSON Assertion更精准:
Assert JSON Path exists:$.data.orderId
Additionally assert value:True
Expected value:(?!null).* (正则,断言orderId不为null)Duration Assertion
断言响应时间不超过阈值,非常实用:
Duration in milliseconds:2000当响应时间超过2000ms时,该请求计为失败。结合这个断言,你可以直接从错误率看出有多少请求超过SLA要求的时间。
Post Processor(后置处理器)
用于从响应中提取数据,供后续请求使用。
JSON Extractor(JSON提取器)
登录接口返回token,后续接口需要带上token:
// 登录响应
{"code":200,"data":{"token":"eyJhbGci...","userId":10001}}JSON Extractor配置:
变量名称:authToken
JSON Path:$.data.token
默认值:MISSING_TOKEN提取后,其他HTTP Request在Header里用${authToken}引用。
Regular Expression Extractor(正则提取器)
当响应不是标准JSON时使用:
变量名称:sessionId
正则表达式:sessionId=([^;]+);
模板:$1$
匹配数字:1(取第1个匹配)Listener(监听器)
监听器收集压测数据,注意:大量监听器会消耗性能,压测时建议只保留必要的。
View Results Tree(查看结果树)
调试阶段必用,可以看到每个请求的完整请求/响应内容。正式压测时必须禁用,否则会占用大量内存。
Aggregate Report(聚合报告)
标准报告,展示:
Label #Samples Average Median 90%Line 95%Line 99%Line Min Max Error% Throughput
/api/order 50000 145 132 198 245 389 45 3421 0.12% 823.5/sec字段解读:
- 90%Line / 95%Line / 99%Line:即P90/P95/P99,重点关注
- Error%:错误率,正式压测要求 < 0.1%
- Throughput:TPS,每秒事务数
Backend Listener(后端监听器)
可以实时把压测数据推送到InfluxDB,配合Grafana做实时监控大盘:
classname:org.apache.jmeter.visualizers.backend.influxdb.InfluxdbBackendListenerClient
influxdbUrl:http://localhost:8086/write?db=jmeter
application:order-service
measurement:jmeter这样压测过程中就有实时曲线可以看,比压完了再看报告更及时。
一个完整的压测脚本示例
以下是一个完整的订单创建压测脚本结构,用JMeter的XML格式展示关键配置(可直接导入JMeter):
<?xml version="1.0" encoding="UTF-8"?>
<jmeterTestPlan version="1.2">
<hashTree>
<TestPlan testname="订单服务压测" enabled="true">
<boolProp name="TestPlan.functional_mode">false</boolProp>
<hashTree>
<!-- 线程组:200并发,运行10分钟 -->
<ThreadGroup testname="订单创建线程组" enabled="true">
<intProp name="ThreadGroup.num_threads">200</intProp>
<intProp name="ThreadGroup.ramp_time">60</intProp>
<boolProp name="ThreadGroup.scheduler">true</boolProp>
<intProp name="ThreadGroup.duration">600</intProp>
<intProp name="ThreadGroup.delay">0</intProp>
<hashTree>
<!-- CSV参数化 -->
<CSVDataSet testname="用户数据" enabled="true">
<stringProp name="filename">/data/users.csv</stringProp>
<stringProp name="variableNames">userId,token</stringProp>
<stringProp name="delimiter">,</stringProp>
<boolProp name="recycle">true</boolProp>
<boolProp name="stopThread">false</boolProp>
<stringProp name="shareMode">shareMode.all</stringProp>
</CSVDataSet>
<!-- HTTP Header Manager -->
<HeaderManager testname="公共请求头" enabled="true">
<collectionProp name="HeaderManager.headers">
<elementProp name="Authorization" elementType="Header">
<stringProp name="Header.name">Authorization</stringProp>
<stringProp name="Header.value">Bearer ${token}</stringProp>
</elementProp>
<elementProp name="Content-Type" elementType="Header">
<stringProp name="Header.name">Content-Type</stringProp>
<stringProp name="Header.value">application/json</stringProp>
</elementProp>
</collectionProp>
</HeaderManager>
<!-- HTTP Request:创建订单 -->
<HTTPSamplerProxy testname="POST 创建订单" enabled="true">
<stringProp name="HTTPSampler.domain">api.example.com</stringProp>
<stringProp name="HTTPSampler.port">443</stringProp>
<stringProp name="HTTPSampler.protocol">https</stringProp>
<stringProp name="HTTPSampler.path">/api/v1/order/create</stringProp>
<stringProp name="HTTPSampler.method">POST</stringProp>
<boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
<elementProp name="HTTPsampler.Arguments" elementType="Arguments">
<collectionProp name="Arguments.arguments">
<elementProp name="" elementType="HTTPArgument">
<stringProp name="Argument.value">{
"userId": "${userId}",
"productId": "${__Random(1001,9999,)}",
"quantity": "${__Random(1,5,)}"
}</stringProp>
</elementProp>
</collectionProp>
</elementProp>
</HTTPSamplerProxy>
<!-- JSON断言 -->
<JSONPathAssertion testname="断言下单成功" enabled="true">
<stringProp name="JSON_PATH">$.code</stringProp>
<stringProp name="EXPECTED_VALUE">200</stringProp>
<boolProp name="JSONVALIDATION">true</boolProp>
<boolProp name="EXPECT_NULL">false</boolProp>
<boolProp name="INVERT">false</boolProp>
<boolProp name="ISREGEX">false</boolProp>
</JSONPathAssertion>
<!-- 响应时间断言 -->
<DurationAssertion testname="响应时间<2000ms" enabled="true">
<intProp name="DurationAssertion.duration">2000</intProp>
</DurationAssertion>
<!-- 聚合报告 -->
<ResultCollector testname="Aggregate Report" enabled="true">
<objProp>
<name>saveConfig</name>
<value class="SampleSaveConfiguration">
<time>true</time>
<latency>true</latency>
<timestamp>true</timestamp>
<success>true</success>
<responseCode>true</responseCode>
<responseMessage>true</responseMessage>
<threadName>true</threadName>
<bytes>true</bytes>
<sentBytes>true</sentBytes>
<url>true</url>
<fileName>true</fileName>
<hostname>true</hostname>
<threadCounts>true</threadCounts>
<sampleCount>true</sampleCount>
<idleTime>true</idleTime>
<connectTime>true</connectTime>
<encoding>false</encoding>
<assertions>true</assertions>
<subresults>true</subresults>
<responseData>false</responseData>
<samplerData>false</samplerData>
<xml>false</xml>
<fieldNames>true</fieldNames>
<responseHeaders>false</responseHeaders>
<requestHeaders>false</requestHeaders>
<responseDataOnError>false</responseDataOnError>
<saveAssertionResultsFailureMessage>true</saveAssertionResultsFailureMessage>
</value>
</objProp>
<stringProp name="filename">/results/order_test_result.csv</stringProp>
</ResultCollector>
</hashTree>
</ThreadGroup>
</hashTree>
</TestPlan>
</hashTree>
</jmeterTestPlan>命令行运行 JMeter
GUI模式只用于调试,正式压测必须用命令行:
# 基本命令
jmeter -n -t /scripts/order_test.jmx -l /results/result.jtl -e -o /report/
# 参数说明:
# -n 非GUI模式
# -t 测试计划文件
# -l 结果文件(.jtl格式)
# -e 压测结束后生成HTML报告
# -o HTML报告输出目录
# 带参数覆盖(动态修改线程数和持续时间)
jmeter -n -t order_test.jmx \
-Jthreads=200 \
-Jduration=600 \
-l result_200.jtl \
-e -o report_200/在jmx里用${__P(threads,100)}引用参数,命令行用-Jthreads=200覆盖默认值,非常方便做不同并发档位的测试。
踩坑实录
坑1:GUI模式下压测数据严重失真
现象: GUI模式下压测,TPS只有500,但关闭所有监听器后TPS跳到1800。
原因: JMeter的Listener在GUI模式下需要实时渲染UI,消耗大量CPU。View Results Tree更是把所有响应数据存在内存里,非常耗资源。
解法: 调试完成后,压测必须用-n非GUI模式运行。所有Listener除Backend Listener(推数据到InfluxDB)外全部禁用,或只保留写结果文件的ResultCollector。
坑2:线程数设太高导致OOM
现象: 设置5000线程压测,JMeter进程自己先OOM了,服务器还没问题。
原因: JMeter默认JVM堆只有256M。5000线程 × 每线程约1MB内存 = 5GB,远超堆大小。
解法: 修改jmeter.sh(或jmeter.bat)里的JVM参数:
HEAP="-Xms2g -Xmx8g -XX:MaxMetaspaceSize=256m"或者用分布式压测,把线程分散到多台机器。
坑3:忘记关闭SSL证书验证导致HTTPS请求失败
现象: 所有HTTPS请求报错PKIX path building failed,错误率100%。
原因: 测试环境使用自签名证书,JMeter默认会验证证书链。
解法: 在jmeter.properties里设置:
https.use.cached.ssl.context=false或者在命令行加参数,或者在HTTP Request Sampler的高级选项里把SSL配置改为RELAXED。
总结
JMeter的六大核心组件:Thread Group控制并发,Sampler发请求,Config Element提供公共配置,Post Processor提取响应数据,Assertion验证结果正确性,Listener收集统计。
记住两个原则:断言不能少,GUI模式不压测。 这两条保证压测数据有意义、有参考价值。
下一篇进入JMeter进阶,CSV数据驱动、关联提取、分布式压测全讲清楚。
