1. 从下载到跑通第一个测试计划:JMeter 5.6 的环境底座
如果你刚接触性能测试,第一次打开 JMeter 5.6 大概率会有两个困惑:一是这玩意儿解压完一堆目录,哪个该点;二是界面点来点去,怎么才能让一个请求真正发出去。我最早做接口测试时也纠结过,后来发现只要把"环境底座"这三件事搞明白,后面所有复杂的脚本都是在上面搭积木。这篇文章不是官方文档的翻译,而是我这些年用 JMeter 做接口测试和压力测试踩出来的经验整理,从安装配置讲到参数化、断言、证书、常见报错排查,最后落到真实的高并发压测场景。不管你之前有没有接触过 JMeter,看完至少能独立搭出一套可复用的测试工程。
JMeter 是 Apache 基金会下的开源测试工具,最初为 Web 应用压力测试而生,现在已经能覆盖 HTTP、数据库、消息队列、FTP 等多类协议的测试需求。5.6 这个版本是很多人从旧版本迁移过来的节点,它的 JDK 要求和组件行为跟 3.x、4.x 有明显差异,所以环境这块必须先对齐。
1.1 JDK 版本与安装包的取舍
JMeter 5.6 是基于 Java 开发的,所以运行前提是本机装了 JDK。这里我强烈建议直接上 JDK 8 或 JDK 11 的 LTS 版本,JDK 17 也能跑,但部分老插件在模块化之后会有反射报错,反而给自己找麻烦。判断标准很简单:JMeter 官方发布包对应的 JDK 兼容范围,你按最低门槛往上取一档就是最稳的。
安装包本身分两种,一种是apache-jmeter-5.6.zip这样的压缩包,另一种是带_bin后缀的二进制包。两者区别在于是否包含源码,做测试用前者就够了,解压即用,不需要执行安装程序。下载来源认准 Apache 官网的下载页,别从第三方站点拿,避免捆绑或者版本被改动。下载下来解压到一个没有中文路径、没有空格的目录,比如D:\tools\apache-jmeter-5.6,这个细节看着小,但很多人脚本跑不起来就是路径里带了中文或者空格导致的。
解压后的bin目录里有几个关键文件你需要认识:
| 文件 | 作用 |
|---|---|
jmeter.bat/jmeter.sh | 图形界面启动脚本,Windows 用 bat |
jmeter-server.bat | 分布式压测时的从机启动脚本 |
jmeter.properties | 核心配置文件,改语言、日志、结果保存等都在这 |
user.properties | 用户级覆盖配置,升级版本时不用动主配置 |
启动前建议先改一件事:在jmeter.properties里把language=zh_CN打开,界面就能切到中文。另外如果你发现 JMeter 界面字体发虚或者中文显示成方块,那是 JVM 字体渲染问题,可以通过调整jmeter.bat里的 JVM 参数或者换 JDK 版本解决,这个后面讲报错时会再提到。
1.2 图形界面与命令行模式的分工
很多人有个误区,以为压测就是打开图形界面点开始。实际上 JMeter 有个非常重要的原则:图形界面只用来编写和调试脚本,真正的压测必须用命令行模式跑。
原因很直接,图形界面本身要消耗大量内存和 CPU 来渲染监听器、画图表,这会污染压测结果。你开着界面跑 500 并发,测出来的响应时间里可能有一大半是界面渲染拖累的。正确做法是脚本调通后保存为.jmx文件,然后用命令:
jmeter -n -t test_plan.jmx -l result.jtl -e -o report_output这里-n表示非 GUI 模式,-t指定脚本,-l指定结果文件,-e -o表示测试结束后直接生成 HTML 报告到指定目录。生成的报告目录必须为空,否则会报目录已存在的错,这也是一类高频报错。
命令行模式下的参数调优同样重要。默认 JVM 堆内存可能只有 1G,并发量上去后容易 OOM,可以在jmeter启动脚本里把HEAP调整到-Xms2g -Xmx2g -XX:MaxMetaspaceSize=512m这样。具体调多大取决于你的并发规模和服务端返回数据量,我一般按每 100 并发预留 1G 堆内存来估,再留点余量。
提示:命令行跑压测时,监听器里不要挂"查看结果树"这类内存杀手,改在调试阶段用,正式压测时用聚合报告或者直接靠生成的 HTML 报告分析。
2. 测试计划里那堆元件到底谁管谁:线程组、控制器与取样器
打开 JMeter 左侧那棵树,新手最容易懵的就是元件太多,不知道该拖哪个。其实整棵树的逻辑非常清晰,从上到下就是"用户怎么来(线程组)→ 来了做什么(控制器+取样器)→ 做的过程中怎么处理数据(配置元件、前置后置处理器)→ 做完怎么看结果(监听器)"。把这四层搞清楚,脚本组织就不会乱。
2.1 线程组的并发模型与三种调度方式
线程组是整个测试计划的入口,它决定了"有多少虚拟用户、怎么发起、发多久"。这里有几个参数必须吃透:
- 线程数(Number of Threads):虚拟用户数,一个线程模拟一个用户。
- Ramp-Up 时间:所有线程在多长时间内启动完。比如 100 线程配 10 秒,就是每秒启动 10 个。
- 循环次数:每个线程执行多少次。
很多人直接忽略 Ramp-Up,填个 100 线程 1 秒启动,结果瞬间把被测服务打挂,测出来的数据全是超时。实际上 Ramp-Up 是模拟真实用户逐步进入的过程,除非你要做瞬时尖峰测试,否则合理设置能让结果更接近真实业务。
线程组还有三种模式要区分。普通线程组是用户真正发起请求的;setUp线程组会在普通线程组之前执行,适合做登录获取 token、准备测试数据;tearDown线程组在所有线程组之后执行,适合清理数据。我做过一个压测项目,前 20 个请求要先拿到一个全局唯一编号作为后续接口参数,就是靠setUp线程组里调一次接口写进属性,然后其他线程组读取,避免了每个线程都去重复调用。
调度器模式也值得说。勾选"调度器"后你可以指定持续时间和启动延迟,做"持续压 30 分钟"这类稳定性测试时很方便。不过要注意循环次数和调度器同时存在时的优先级问题,容易配出"跑满时间但没跑够次数"或者反过来,我的习惯是做稳定性测试时循环次数设为"永远",靠调度器控制结束时间。
2.2 逻辑控制器解决的真实问题
逻辑控制器本身不发送请求,它管的是"取样器的执行顺序和条件"。用得最多的几个:
- 事务控制器:把多个请求合并成一个事务,统计总耗时。比如一个下单动作包含"加购物车→选地址→提交订单"三个接口,你关心的是整体耗时,就用它包起来。
- 循环控制器:控制子元件循环次数,跟线程组循环是叠加关系,容易配出天文数字的请求量,要小心。
- 仅一次控制器:子元件在每个线程里只执行一次,典型场景是登录。一个用户登录一次后,后面所有请求复用这个会话,就靠它。
- If 控制器:根据条件判断是否执行,配合函数和变量做分支逻辑。
2.3 元件执行顺序:一个被严重低估的知识点
这是我最想强调的一点。JMeter 里元件的执行顺序不是你在树里看到的排列顺序,而是有固定规则:
- 配置元件
- 前置处理器
- 定时器
- 取样器
- 后置处理器
- 断言
- 监听器
这个顺序决定了"你在哪里定义变量,后面才读得到"。比如你想在 HTTP 请求里用一个从数据库查出来的值,就得把 JDBC 后置处理器放在前置位置,让它在取样器之前把值取好写进变量。我见过有人把断言写在前置处理器里想提前拦截,结果完全没用,就是因为断言阶段晚于取样器。
理解了这个顺序,"为什么变量取不到""为什么断言没生效"这类问题基本能自己定位。同一层级内的多个同类元件,则按它们在树里的先后顺序执行。这个规则在我们后面讲参数化、Cookie 时会反复用到。
3. HTTPS 脚本录制与证书那些绕不开的事
手写脚本对简单接口没问题,但面对一个几十个接口的复杂系统,一个个手敲请求路径和参数太费劲。这时候录制脚本就派上用场了。JMeter 提供了一个测试脚本录制器,本质是一个本地的代理服务器,浏览器把流量发到它这里,它翻译成 JMeter 的取样器。
3.1 录制器的工作链路
录制涉及三个角色:浏览器、JMeter 录制器、目标服务器。链路是浏览器设置代理指向录制器端口,录制器再转发给真实服务器。JMeter 里叫"HTTP(S) Test Script Recorder",在测试计划下添加它,默认监听 8888 端口。
配置时几个关键点:
- Target Controller:指定录制下来的取样器放到哪个线程组下,一般单独建一个"录制线程组"。
- 分组方式:建议选"Put each group in a new transaction controller",这样同类请求会被归并,脚本更整洁。
- URL 过滤:把图片、css、js 这类静态资源过滤掉,只录接口请求,否则脚本里全是噪声。
浏览器侧把 HTTP 和 HTTPS 代理都指向127.0.0.1:8888。注意 https 的流量不能直接录,因为浏览器不信任录制器的证书,会直接拒绝连接,这就是证书问题的根源。
3.2 证书导入与浏览器信任
JMeter 在bin目录下自带了一个ApacheJMeterTemporaryRootCA.crt证书文件。第一次点击开始录制时,它会在 bin 目录下自动生成这个根证书。你需要把它导入到浏览器或者操作系统受信任的根证书存储里。
Windows 下的导入路径:双击证书文件 → 安装证书 → 选择"本地计算机" → 受信任的根证书颁发机构。导入完重启浏览器,再录制 https 就不会报证书错误了。
这里有个坑很多人踩:证书是每次生成的话如果不固定,换台机器或者重装 JMeter 就得重新导。而且证书默认有效期有限,过期后录制就会失败,报证书不受信任。解决办法是在jmeter.properties里把proxy.cert.validity调大,或者干脆维护一份固定的证书文件复用。
3.3 录制之后的参数关联
录下来的脚本几乎不能直接用,因为里面全是硬编码的、跟这次会话绑定的值:会话 id、时间戳、token、随机串。这些值在下次请求时已经失效,脚本一回放就失败。这就是"参数关联"要解决的问题。
核心做法是:把动态值从上一个响应里提取出来,存成变量,下一个请求用变量引用。提取用后置处理器,最常用的是正则表达式提取器和JSON 提取器(需要插件)。比如响应里返回{"token":"abc123"},用 JSON 提取器写$.token就能把abc123提到变量token里,下个请求填${token}。
关联的本质是识别"哪些值是服务端动态下发的",这需要你看响应内容判断。我一般先回放一次,看哪个请求失败了,再回头看它依赖什么值,逐个关联。这活儿没有捷径,靠的是对业务链路的理解。
4. 参数化:把 JDBC 查询结果喂给下一个接口
参数化是让脚本"活起来"的关键。同一个接口,不同用户要传不同账号、不同编号,才能模拟真实场景。JMeter 提供了多种参数化手段,选哪种取决于数据从哪来。
4.1 CSV 与 JDBC 两种数据源的差异
最常见的是 CSV 数据文件配置,把测试数据写成一行行的 csv,每个线程读一行。配置项里Recycle on EOF控制数据读完后是否循环,Sharing mode决定数据在线程间怎么分配。做多用户并发时,一般选"所有线程共享",让每个线程拿到不同的行。
但有些数据不在文件里,而在数据库里。比如压测下单接口,需要一批真实存在且状态正常的订单编号,这些编号存在业务库里。这时候就要用JDBC Request直接从数据库查。
JDBC 参数化的完整链路是这样的:
- 配置JDBC Connection Configuration,填数据库地址、驱动类、账号密码、连接池参数。
- 添加JDBC Request,写 SQL 查询,指定结果变量名,比如
orderIds。 - 用后置处理器或者直接通过变量引用,把查询结果传给 HTTP 请求。
查出来的结果,JMeter 会按${orderIds_1}、${orderIds_2}这样的格式存成一组变量,orderIds_#表示行数。你可以在 HTTP 请求里用${__V(orderIds_${__Random(1,${orderIds_#})})}这种写法随机取一行。这个嵌套函数看着绕,但它是把"随机行号"和"结果变量"拼起来的标准套路,写熟了很好用。
4.2 从查询结果取值传给下游接口
举个我实际做过的场景:压测一个对账接口,需要从数据库捞出 500 个待对账的流水号,每个线程随机取一个作为请求参数,并且要求不能重复。做法是:
- JDBC 查询出所有流水号到变量
flowNos。 - 用 CSV 或者计数器给每个线程分配一个唯一索引。
- HTTP 请求参数填
${__V(flowNos_${index})}。
这里有个坑:JDBC 连接池如果配得太小,高并发时线程抢连接会阻塞,导致请求超时。连接池的Max Number of Connections一般要跟并发线程数匹配,或者略大。另外数据库本身的连接上限也要考虑,别把生产库的连接占满影响业务。
注意:对生产库做查询参数化时,SQL 一定要走索引,别一个
select * from把库拖垮。压测工具把数据库搞挂的事故,比压测把服务搞挂的还常见。
4.3 RESTful 接口带路径参数怎么写
RESTful 风格的接口,参数不在查询串里,而是嵌在路径上,比如/api/order/{orderId}/detail。在 JMeter 里写法很直接:把路径里的占位符换成变量引用即可,/api/order/${orderId}/detail。如果参数需要 URL 编码,JMeter 的 HTTP 请求默认勾选编码就行。
真正麻烦的是 Path 参数里含特殊字符时的编码问题,以及 PUT、DELETE 这类请求的方法选择。HTTP 请求元件里的 Method 下拉支持 GET、POST、PUT、DELETE、PATCH 等,选对方法才能匹配到正确的后端路由。很多人测 RESTful 接口失败的根源就是方法选错了,或者 Content-Type 头没设对,服务器解析不了 body。
5. 断言体系:从响应断言到 Beanshell 断言
请求发出去不代表测对了。返回 200 也可能业务是失败的,所以必须加断言来判断结果是否符合预期。这是接口测试区别于"发请求"的核心。
5.1 基础断言的适用场景
JMeter 自带的响应断言(Response Assertion)能覆盖大部分场景。它支持匹配响应文本、响应代码、响应头、URL 等,模式可以选包含、匹配、相等、子串。判断返回码是不是 200,或者响应体里是不是包含"success":true,用它就够了。
大小断言(Size Assertion)判断响应字节数,持续时间断言判断响应时间是否超阈值。这俩在做性能测试时有用,能快速标记出慢请求和异常大小的响应。
基础断言的局限在于"只能做字符串级判断"。如果你想判断"返回的订单列表数量大于 0"或者"某个字段的数值在合理范围内",它就不够用了,得上脚本断言。
5.2 Beanshell 断言的写法与注意点
Beanshell 断言让你写一小段 Java 代码来判断,灵活性拉满。基本结构是:
String response = prev.getResponseDataAsString(); if (response.contains("\"code\":0")) { AssertionResult.setFailure(false); } else { AssertionResult.setFailure(true); AssertionResult.setFailureMessage("业务返回异常: " + response); }手上有个真实案例:某接口正常返回是 JSON,但偶尔会返回一段 HTML 错误页(网关超时),状态码还是 200。这时候基础断言判断 code=0 会失败但不知道为啥,Beanshell 里加一段判断响应是不是{开头,能快速定位是网关层出的问题。
写 Beanshell 断言要注意性能。它每请求都执行一次脚本,代码里别做复杂操作、别查数据库、别发网络请求,否则会成为压测瓶颈。另外 Beanshell 访问变量用vars.get()、vars.put(),跟前置后置处理器里的用法一致。JMeter 5.6 里还有 JSR223 断言,支持 Groovy 等语言,性能比 Beanshell 好不少,新项目我一般直接上 Groovy。
5.3 断言粒度怎么把握
断言不是越多越好。一个大压测脚本里塞满复杂断言,本身就是性能负担,而且断言失败时你还得逐个排查是哪条挂了。我的经验是分层断言:
- 接口级:只断言核心业务字段(如 code、关键 id 存在)。
- 事务级:用事务控制器包住一组请求,在事务上做整体时长断言。
- 全局级:用聚合报告或者后端监听器看整体成功率,异常时告警。
压测关注的是成功率和耗时,接口测试关注的是字段值对不对,两者断言策略应该不同,别用接口测试的严格断言标准去压测,会把大量本该成功的请求判成失败。
6. 那些让人抓狂的报错:从 error writing to server 到文件已存在
用 JMeter 最耗时间的不是写脚本,是排错。下面这几个报错我在实际项目里反复遇到,把排查思路整理出来,遇到时能少走弯路。
6.1 java.io.IOException: error writing to server 的排查链路
这个报错的意思是 JMeter 把请求发给服务端时连接断了。可能原因从上到下排:
- 服务端主动断开:并发太高,服务端的连接队列满了,直接拒绝。检查服务端的最大连接数、线程池、accept 队列。
- 请求体太大:上传大文件时,服务端或中间的负载均衡器有 body 大小限制。
- 连接被中间设备重置:网络层有设备限制了长连接或者包大小。
- JMeter 侧的问题:用了 keep-alive 但服务端不支持,或者 HTTP 实现选的 Java 默认实现在某些场景有兼容问题。
排查方法:先单线程跑一次,确认脚本本身没问题;再逐步加并发,找到开始报错的临界点;同时看服务端的日志和监控,确认是服务端拒绝还是网络中断。我遇到过一次,报错原因是压测机本地端口被耗尽(TIME_WAIT 状态太多),调大系统端口范围和缩短 TIME_WAIT 时间后解决,这属于压测机自身瓶颈,很容易被忽略。
6.2 文件已经存在与防伪标记缺失
"文件已经存在"这个报错,多半出现在生成测试报告时目标目录非空。JMeter 生成 HTML 报告要求输出目录是空的,我习惯每次压测前用脚本清空报告目录,或者干脆用带时间戳的目录名。
__RequestVerificationToken 未提供必要的防伪标记这个报错来自 ASP.NET MVC 的防伪机制。它要求发 POST 请求时带上一个防伪 token,这个 token 藏在页面里或者响应里。解决办法是用正则提取器从页面提取 token,然后在后续请求里带上。本质还是参数关联问题,只是这个值比较隐蔽,要看清服务端是从 cookie 校验还是从表单字段校验。
6.3 Cookie 管理器的默认行为
很多人不知道,JMeter 每个线程有自己的 cookie 存储,且默认就是开启的(HTTP Cookie Manager 会自动处理)。如果你在一个线程里登录后拿到了会话 cookie,后续请求会自动带上。这本来是好事,但也会造成问题:
- 你做多用户并发时,以为每个线程是独立用户,实际上如果 cookie 管理器配置了共享,就是共用一个会话。
- 有些接口要求清空 cookie 重新登录,结果旧 cookie 一直在干扰。
所以要么明确在 HTTP Cookie Manager 里勾选"每次迭代清除 Cookie",要么在需要独立会话的场景加多个 cookie 管理器。Cookie 处理不好,表现就是"单跑没问题,并发就失败",这类问题排查起来特别隐蔽。
7. 高并发压测落地:从单机到分布式
脚本调通、断言写好,最后要落到真实压测。前面提到过一个典型场景:把一套微服务环境整体迁移到云上,迁移完要用 JMeter 脚本做高并发测试,验证云环境的承载能力。这种场景对压测方案的要求比较高,我结合这类项目经验讲几个要点。
7.1 压测模型怎么设计才可靠
压测模型的核心是"模拟真实的用户行为"。别上来就 1000 并发打某个接口,那不叫压测,叫攻击。合理的做法是:
- 梳理业务模型:各个接口的调用比例是多少。真实用户不会只调一个接口,下单和查询的比例可能差异很大,按比例分配并发数。
- 确定目标指标:TPS 要达到多少、平均响应时间控制在多少、错误率不超过多少。有了指标才能判断压测是否达标。
- 梯度加压:从低并发开始,逐级加压,观察 TPS 和响应时间曲线。找到性能拐点,也就是 TPS 不再随并发上升、响应时间开始飙升的那个点,那才是系统的真实容量。
- 稳定性测试:在容量的 70%~80% 上持续压 1 到几小时,看有没有内存泄漏、连接泄漏。
这里有个容易被忽略的点:迁移到云上后,压测机和被测服务很可能在不同的网络区域。压测机到服务的网络延迟会算进响应时间,导致数据失真。要么把压测机部署到同一区域,要么单独测量网络延迟再扣除。
7.2 插件扩展解决特殊协议:MQTT、上传文件
JMeter 标准包只覆盖 HTTP、JDBC、FTP 这些,遇到 MQTT 这类物联网协议就得装插件。插件安装推荐用Plugins Manager,装好后在界面里直接搜索 MQTT 插件安装,比手动往 lib 目录拷 jar 包靠谱得多,也不会因为版本不匹配报类冲突。
MQTT 插件装好后,添加 MQTT 连接配置、MQTT 发布和订阅取样器,能模拟大量设备上报和接收消息,这是物联网压测的常见需求。装插件后如果启动报ClassNotFoundException,八成是 jar 包版本不匹配或者忘了重启 JMeter。
文件上传测试要注意几点:HTTP 请求里勾选"对 POST 使用 multipart/form-data",文件路径用参数化方式传入(支持随机选不同大小的文件),另外Files Upload区域填的 MIME 类型要对,不然服务端可能拒收。上传大文件时记得调大 JMeter 堆内存,否则文件本身就把内存吃光了。
7.3 分布式压测与结果解读
单机压测机再强也有上限,端口数、CPU、内存都会成为瓶颈。当需要几千并发时,得上分布式:一台主控机(master)+ 多台执行机(slave)。主控机通过 RMI 把脚本分发下去,各执行机跑完把结果回传汇总。
分布式压测的坑主要集中在网络和配置上:
- 各机器的 JMeter 版本必须一致,否则协议不兼容。
jmeter-server配置文件里要指定主控机 IP 和监听端口。- 防火墙要放行 RMI 相关端口。
- 各执行机的时间要同步,否则结果时间戳对不上。
- 脚本里用到的数据文件要分发到每台执行机上,路径保持一致。
结果解读上,重点关注几个指标:TPS、平均响应时间、90%/95% 响应时间、错误率。平均值容易被少数极慢的请求拉偏,所以看分位数比看平均值更靠谱。聚合报告里那列 95% Line 表示 95% 的请求都在这个时间以下完成,比平均时间更能反映大多数用户的体验。
还有一点经验:压测结果异常时,先分清是"被测服务的问题"还是"压测工具自身的问题"。压测机的 CPU、内存、网络带宽、端口占用情况都要监控,很多时候测出来 TPS 上不去,是压测机先扛不住了。这一步不做,很容易把压测机的瓶颈误判成服务的性能上限,得出错误结论。
最后分享一个我自己踩过的小经验。做环境迁移后的承载力验证时,别只压迁移后的新环境,最好把旧环境的同等压测数据拿出来做对比。因为很多性能问题不是迁移引入的,而是旧环境本来就存在,没有基线数据你根本判断不了迁移是成功还是失败。我就是因为提前在旧环境跑了一遍基准压测,迁移后才迅速定位到是新环境某个服务的连接池默认配置偏小,改完配置性能直接回到正常水位。