上一篇性能测试:指标、负载模型、结果解读与容量规划把「怎么想」讲透了——RT/QPS/并发怎么换算、开放 vs 闭合模型为什么决定成败、P99 与 Coordinated Omission 的陷阱、容量怎么规划。这一篇是它的动手篇:用最主流的开源压测工具 Apache JMeter,把那些原则真正落到一条能跑的压测计划上。
JMeter 是 Apache 基金会下的性能测试工具,Java 编写、图形化、开源免费,主要用于接口测试和性能测试。它上手快、协议全、插件生态成熟,是后端工程师做压测的「默认选项」。但它也有一堆一不小心就踩的坑——用 GUI 跑正式压测、监听器开着把内存吃爆、闭合线程组高估容量——这些恰恰是上一篇理论的照妖镜。本篇就带着理论来用工具。
一句话定位:JMeter = 测试计划树(组件分层作用)+ 线程组(控制并发与节奏)+ 非 GUI 命令行执行(出 HTML 报告)。GUI 只用来搭脚本和调试,正式压测一律
jmeter -n无界面运行;面向公网/竞价的高并发场景,还要用插件把默认的闭合线程组掰成开放模型。
TL;DR
- JMeter 的心智是一棵「测试计划树」:所有组件挂在树上、分层作用——配置元件/定时器/断言挂在哪一级,就对该级及其子节点的所有请求生效。挂错层级是新手第一大坑。
- 九大组件各司其职:线程组(并发与节奏)、取样器(发请求)、配置元件(辅助配置)、前/后置处理器(请求前后处理数据)、定时器(控制节奏)、断言(校验响应)、逻辑控制器(控制流程)、监听器(收集展示结果)。
- 线程组三旋钮:Number of Threads(并发)、Ramp-up Period(多久拉满并发)、Loop Count(每线程循环次数)。理想 QPS ≈ Threads × Loops ÷ Ramp-up——但真实吞吐受被测系统 RT 制约,公式只是上界。
- 最小可用回路:线程组 → HTTP 请求默认值 + 信息头管理器 → HTTP 请求 → 响应断言 → 监听器;数据驱动再加 CSV Data Set Config。
- 铁律:GUI 只调试,压测走 CLI。
jmeter -n -t plan.jmx -l result.jtl -e -o report/——-n非 GUI、-t脚本、-l结果、-e -o生成 HTML 报告;-g可从已有.jtl二次出报告。结果树只在调试期开,压测时开着几分钟就 OOM。 - CSV 数据驱动:用
CSV Data Set Config喂多样化参数,避免所有请求用同一个 ID 反复命中缓存/Buffer Pool,掩盖真实 I/O 瓶颈。 - 默认线程组是闭合模型(固定 N 并发、自带背压),会高估容量。面向真实到达流量要用
Concurrency Thread Group+Throughput Shaping Timer逼近开放模型。 - 单机施压有天花板:施压机自己的 CPU/网卡/端口会先打满,高 QPS 需分布式施压(Controller + 多台 jmeter-server worker)。
Table of contents
Open Table of contents
1. 下载安装与一条铁律
在 Apache JMeter 官网下载最新版本,解压后进入 bin/ 目录:图形界面用 jmeter(Windows 用 jmeter.bat),命令行压测用 jmeter -n ...。JMeter 依赖 Java,运行前确保装了 JDK(新版本需要 JDK 8+)。
先把最重要的一条纪律钉在最前面:
不要用 GUI 运行压力测试。 GUI 只用于压测计划的创建和调试;正式执行请一律用命令行:
jmeter -n -t <脚本.jmx> -l <结果文件.jtl> -e -o <HTML 报告目录>
原因很简单:GUI 要实时渲染界面、监听器要在内存里累积每一条请求/响应,这些开销会让施压机把资源花在画图上而不是发压,轻则测不准、重则施压机自己先崩。这条纪律贯穿全篇。
2. JMeter 组件模型:一棵分层作用的树
理解 JMeter 的关键,是把它想成一棵测试计划树:你在 GUI 里右键「添加」的每个组件都挂在树的某个节点下,而组件的作用域由它挂载的层级决定——挂在线程组下,就对该线程组里所有请求生效;挂在某个请求下,就只对那个请求生效。
JMeter 是一棵测试计划树:组件分层作用,挂在哪一级就对该级及其子节点的所有请求生效。
九大组件的职责:
| 组件 | 职责 | 压测中的角色 |
|---|---|---|
| 线程组(Thread Group) | 管理请求的运行时间和并发数量 | 性能测试必设,决定施压强度与节奏 |
| 取样器(Sampler) | 生成各协议请求(HTTP/JDBC/TCP…),编辑请求与传参 | 真正「发出去」的那一下 |
| 配置元件(Config Element) | 辅助请求:请求默认值、信息头、CSV 数据源 | 简化和统一请求配置 |
| 前置处理器(Pre-Processor) | 请求发出前处理数据(动态改参) | 构造动态请求 |
| 后置处理器(Post-Processor) | 响应后提取数据(JSON/正则提取器) | 把响应值传给下游请求(关联) |
| 定时器(Timer) | 控制请求节奏:思考时间、吞吐量整形 | 模拟真实用户停顿、控制到达率 |
| 断言(Assertion) | 校验响应是否符合预期(状态码/内容) | 判断请求「成功」的标准 |
| 逻辑控制器(Logic Controller) | 控制请求的执行顺序与分支循环 | 编排复杂业务流程 |
| 监听器(Listener) | 收集/展示结果(结果树、Summary、聚合报告) | 看结果,但压测时要克制使用 |
分层作用的直觉:把「HTTP 信息头管理器」挂在线程组下,线程组里每个 HTTP 请求都会带上这些头;把它挂在某个具体请求下,就只有那个请求带。断言、定时器同理。理解了这一点,你就能读懂任何一份
.jmx,也不会再纳闷「为什么我加的断言没生效」。
3. 线程组与负载:三旋钮和 QPS 换算
线程组是压测的引擎,核心是三个旋钮:
- Number of Threads (users):并发线程数,即「同时施压的虚拟用户数」。
- Ramp-up Period (seconds):用多长时间把所有线程逐步拉起——不是一上来就满并发,而是平滑爬坡(呼应上一篇讲的 Ramp-Up 预热,让 JVM/JIT 有时间热起来)。
- Loop Count:每个线程重复执行几轮。
线程组三旋钮与 Ramp-up 时间线:并发是「拉起来」的,理想 QPS = Threads × Loops ÷ Ramp-up。
理想 QPS 的换算:
- QPS ≈ Number of Threads × Loop Count ÷ Ramp-up Period(s)
例如 10 线程 × 100 循环 ÷ 5 秒 = 200 QPS。
关键提醒:这只是理想上界。真实吞吐会被被测系统的 RT 卡住——系统一慢,线程就卡在等响应上,实际发出的 QPS 达不到公式值。这正是上一篇
QPS = 并发 / RT的另一面:线程组给的是「并发」这个输入,QPS 是系统 RT 决定的输出。想精确控制到达率,得靠 §7 的吞吐量整形。
4. 从零搭一条 HTTP 压测计划
把组件拼起来,看一条最小可用的压测计划怎么搭(都在「测试计划」上右键逐级「添加」)。
4.1 创建线程组
在「测试计划」上右键 →【添加】→【Threads (Users)】→【线程组】。设置三旋钮:先用小值(如 5 线程、Ramp-up 1s、Loop 2)方便调试,脚本调通后再在命令行用属性覆盖成正式压测参数(见 §6)。
4.2 HTTP 请求默认值(配置元件)
在线程组上右键 →【添加】→【配置元件】→【HTTP 请求默认值】。把协议、服务器地址、端口填在这里,之后所有 HTTP 请求就不用各自重复填——改环境(测试→预发)只改这一处。
4.3 HTTP 信息头管理器(配置元件)
右键 →【添加】→【配置元件】→【HTTP 信息头管理器】。传 JSON 就加一行 Content-Type: application/json;需要鉴权就加 Authorization。挂在线程组下,对组内所有请求生效。
4.4 HTTP 请求(取样器)
右键 →【添加】→【取样器】→【HTTP 请求】。填路径、方法(GET/POST)和 Body。这就是真正发出去的请求。
4.5 响应断言(断言)
在 HTTP 请求上右键 →【添加】→【断言】→【响应断言】。根据响应判断请求是否正常——最常见是判断响应代码为 200,也可以断言响应体包含某个关键字。没有断言的压测是危险的:接口返回 500 但 HTTP 层「成功」时,报告里成功率依旧漂亮,你却在压一个一直报错的接口。
4.6 监听器:结果树与 Summary Report
- 【监听器】→【察看结果树(View Results Tree)】:能看每条请求的请求/响应详情,调试神器,但压测大忌——它把每条记录存进内存,高并发下几分钟就 OOM。调通后务必禁用或删除。
- 【监听器】→【Summary Report / 聚合报告】:汇总样本数、平均/最小/最大 RT、错误率、吞吐量。
调试阶段点 GUI 的「运行」看结果树,确认逻辑正确即可——但别在 GUI 里下压测结论,正式数据来自 §6 的命令行。
5. CSV 数据驱动:让参数「活」起来
如果所有虚拟用户都用同一个 ID、同一个关键词请求,数据库的 Buffer Pool 会把同一条记录反复预热、缓存命中率虚高,测出来的性能比真实好一大截(上一篇「数据多样性」误区讲的就是这个)。
解法是 CSV Data Set Config:右键 →【添加】→【配置元件】→【CSV Data Set Config】,指定一个 CSV 文件和变量名,JMeter 会为每次请求(或每个线程)依次读取一行,用 ${变量名} 引用到请求里。
# users.csv
userId,keyword
10001,shoes
10002,laptop
10003,phone
HTTP 请求里写 /search?uid=${userId}&q=${keyword},每次请求就吃不同的参数。GET 拉列表、POST 造不同 Body,都靠它喂多样化数据。这样压出来的缓存命中分布、慢查询模式才贴近生产。
6. 执行测试计划:非 GUI 命令行
脚本在 GUI 里调通、保存成 .jmx 后,正式压测一律走命令行。
GUI 只搭脚本和调试;正式压测 -n 无界面运行,-e -o 出 HTML 报告,-g 从已有结果二次生成。
6.1 一条命令跑完并出报告
jmeter -n -t testplan/RedisLock.jmx -l testplan/result/result.jtl -e -o testplan/webreport
参数说明:
-n:非 GUI(no GUI)模式运行——这是压测的正确姿势。-t testplan/RedisLock.jmx:测试计划脚本路径。-l testplan/result/result.jtl:原始结果文件路径(每条采样一行)。-e -o testplan/webreport:跑完后生成 HTML 报告到指定目录(-e生成、-o指定目录,目录必须不存在或为空)。
6.2 用属性把参数外置(便于 CI)
把并发/时长写死在 .jmx 里不利于复用。可以在线程组里用 ${__P(threads,10)} 这类属性占位,命令行用 -J 覆盖:
jmeter -n -t bidding_mix.jmx -l result.jtl -e -o ./report \
-Jthreads=500 -Jrampup=300 -Jduration=1800
这样同一份脚本在 CI 里就能参数化跑不同压力档位(可无缝接进 Jenkins 流水线做常态化性能回归)。
6.3 从已有结果二次生成报告(-g)
如果压测时只落了 .jtl、没直接出报告,或想对同一批数据重新生成报告:
jmeter -g testplan/result/result.jtl -o testplan/webreport
这在分布式压测各节点各自落盘、事后合并的场景尤其有用。
6.4 读 HTML 报告
生成的 HTML 报告里,最该盯的几项:
- Response Times Percentiles:P90/P95/P99 曲线——尾延迟才是真相,别只看平均值(详见上一篇 §8.2)。
- Throughput:实际吞吐(真·QPS),和你设的理想 QPS 对比,看系统有没有卡住。
- Errors:错误率与错误类型分布——配合 §4.5 的断言才准。
- Response Time Over Time / Active Threads Over Time:观察是否进入稳态、爬坡是否符合预期。
7. 逼近开放模型:Concurrency Thread Group + Throughput Shaping Timer
这是 JMeter 用户最容易忽略、却直接决定压测可信度的一点。上一篇讲过:默认线程组是闭合模型(固定 N 个虚拟用户循环「发→等→再发」),系统一慢,虚拟用户就「体贴地」少发请求——自带背压,会系统性高估容量,测不出真实拐点。
面向公网、尤其是 RTB 竞价这类开放到达的流量,应该逼近开放模型(恒定到达率)。JMeter 靠 JMeter Plugins 里的两个组件:
- Concurrency Thread Group:按目标并发平滑控制线程数,比默认线程组更适合阶梯加压。
- Throughput Shaping Timer:直接定义「每秒多少请求」的到达率曲线(如 5 分钟从 0 爬到 2000 RPS,再恒定 30 分钟),二者配合让 JMeter 尽量按恒定到达率注入。
# Throughput Shaping Timer 的一条曲线示例(起始RPS, 结束RPS, 持续秒数)
0 2000 300 # 5 分钟从 0 爬到 2000 RPS(预热)
2000 2000 1800 # 恒定 2000 RPS 压 30 分钟(稳态取数)
诚实的边界:即便配了整形器,JMeter 本质仍是线程驱动、阻塞采样,在极致尾延迟测量上不如原生开放模型工具(wrk2、Gatling、k6)——它们内建 Coordinated Omission 校正。所以选型上:JMeter 适合功能验证、协议丰富的中低压场景;要压 RTB bidder 的 P99.9、要 CO 校正,优先上 wrk2/k6。别用错了工具还怪数字不准。
8. 分布式压测:一台施压机不够就加机器
单台施压机有硬天花板:它自己的 CPU、网卡带宽、可用端口(ephemeral port)会先打满。这时你测出的「系统最大 QPS」其实是施压机的上限,不是被测系统的。
分布式压测:Controller 只分发脚本与聚合结果,多台 Worker(jmeter-server)才是真正的施压节点,一起压向被测系统。
用法概览:
- 每台施压机启动
jmeter-server(worker)。 - Controller(client)用
-R指定 worker 列表启动:
jmeter -n -t plan.jmx -R worker1:1099,worker2:1099,worker3:1099 \
-l result.jtl -e -o report/
- Controller 把脚本分发给各 worker,各 worker 独立发压,结果回传汇总。
注意:Controller 回传聚合本身也有开销,超大规模建议各 worker 各自落
.jtl、事后用-g合并出报告,避免回传成为瓶颈。判断「是不是施压机瓶颈」的简单方法:盯施压机的 CPU、网卡利用率和TIME_WAIT端口数——它们先饱和,就该加机器了(这也和连接与 I/O 模型里的端口/连接约束一脉相承)。
9. 小结
JMeter 的价值不在于点几下 GUI 出个数字,而在于用对方法把上一篇的理论落成可跑、可信、可复现的压测:
- 组件模型是地图:一棵分层作用的测试计划树,看懂层级就看懂任何
.jmx(§2)。 - 线程组是引擎:三旋钮控制并发与节奏,理想 QPS = Threads × Loops ÷ Ramp-up,但真实吞吐由 RT 决定(§3)。
- 搭建有套路:默认值 + 信息头 + 请求 + 断言 + 监听器是最小回路,CSV 让参数多样化(§4、§5)。
- 执行有铁律:GUI 只调试,压测走
jmeter -n,-e -o出报告、-g二次生成,结果树压测期必关(§1、§6)。 - 可信靠模型与规模:用 Concurrency Thread Group + Throughput Shaping Timer 逼近开放模型,单机不够就分布式施压(§7、§8)。
想把「为什么这么测」的原理补齐(负载模型、P99 与 Coordinated Omission、容量规划、wrk/Gatling/k6 脚本对比),回到理论篇:性能测试:指标、负载模型、结果解读与容量规划。理论管「怎么想」,JMeter 管「怎么做」——两篇配套,才是一条完整的性能验证闭环。
参考
- Apache JMeter. User’s Manual: Getting Started / Test Plan Elements:组件模型、线程组、取样器、监听器的一手权威说明。
- Apache JMeter. Non-GUI Mode & Generating Report Dashboard:
-n、-l、-e -o、-g与 HTML 报告的官方文档。 - Apache JMeter. Remote (Distributed) Testing:Controller + jmeter-server 分布式施压的配置与注意事项。
- JMeter Plugins. Concurrency Thread Group & Throughput Shaping Timer:逼近开放模型(恒定到达率)的插件文档。
- Gil Tene. How NOT to Measure Latency (Coordinated Omission):为什么闭合/阻塞采样会抹平尾延迟——理解 JMeter 尾延迟精度边界的必看。