JMeter 分布式压测:配置、数据分片与真实故障排查¶
单机压 800 TPS 时,top 里 jmeter 占满 CPU,聚合报告的 TPS 不再上涨——这时瓶颈在压测机,不在服务端。分布式不是“多开几台”,RMI、数据、结果合并三处都可能让数据失真。
1. 先确认单机确实到瓶颈¶
不要凭感觉加机器。用三个信号判断:
# 压测机 CPU:us 高、且 JMeter 进程占主导
top -p $(pgrep -f ApacheJMeter.jar)
# 压测机网络出口是否打满(1Gbps ≈ 125MB/s)
sar -n DEV 1 5 | grep eth0
# 端口是否耗尽(大量 TIME_WAIT)
ss -s | grep TIME-WAIT
如果压测机 CPU < 60%、带宽 < 70%、没有端口耗尽,TPS 上不去多半是服务端或脚本问题,加机器没用。
2. 角色与端口¶
Controller(1 台,只调度,不施压)
├── Worker A 192.168.10.11
├── Worker B 192.168.10.12
└── Worker C 192.168.10.13
术语先澄清:JMeter 没有像 LoadRunner Controller 那样独立的"控制器"组件。这里的 Controller 是分布式测试里的角色名,实际就是启动
jmeter -n -t ...的那台 master(客户端)。JMeter 自身叫 "Controller" 的东西是逻辑控制器(Transaction Controller、Loop Controller 等),只控制采样器执行顺序,与场景调度无关。
- RMI 默认端口
1099(server_port) - Worker 本机 RMI 通信端口
server.rmi.localport(建议固定,别让系统随机分配) - Controller 需要能访问 Worker 的 1099 与 localport
- Worker 需要能访问被测服务和 Controller 的回连端口
3. Worker 配置¶
修改 $JMETER_HOME/bin/jmeter.properties:
# 关闭 RMI SSL(内网压测最常见的第一步)
server.rmi.ssl.disable=true
# 固定服务端口与本机通信端口
server_port=1099
server.rmi.localport=4000
# 堆内存按机器规格给,不要超过物理内存 70%
jmeter-server 启动脚本里加上 hostname,否则在多网卡/容器里 RMI 会把自己注册成 127.0.1.1 或内网不可达地址:
export JVM_ARGS="-Xms4g -Xmx4g -XX:+UseG1GC -Djava.rmi.server.hostname=192.168.10.11"
nohup ./jmeter-server > jmeter-server.log 2>&1 &
server_port已写在jmeter.properties,不需要在命令行再传-Dserver_port;jmeter-server的参数会作为 JMeter 程序参数解析,JVM 参数请统一放在JVM_ARGS里。
-Djava.rmi.server.hostname必须是 Controller 能路由到的 IP,不是0.0.0.0,也不是容器内网 IP。
4. Controller 配置与启动¶
remote_hosts=192.168.10.11:1099,192.168.10.12:1099,192.168.10.13:1099
server.rmi.ssl.disable=true
client.tries=2
client.retry_delay=5
# -r 使用 remote_hosts 全部 worker;-R 指定本次参与的 worker
jmeter -n -t order.jmx -r -l result.jtl -e -o report \
-Gthreads=900 -Gduration=1800
关闭 GUI 的监听器,结果只写 jtl,结束后再生成 HTML:
5. 数据必须分片,否则等于没压¶
这是分布式最容易被忽略的一点:CSV 文件不会自动分发,且每台 worker 若从第 1 行开始读,会造成大量重复账号、缓存命中虚高、唯一键冲突。
三种做法:
方案 A:按 worker 分文件(稳定)¶
脚本里用 __P(worker) 或机器名区分文件名。注意 -J 只在 Controller 本地生效,不会下发到 worker(JMeter 官方文档明确:-J/-q 定义的属性不发送到远程服务器,需要下发用 -G)。
# 每台 worker 的 $JMETER_HOME/bin/user.properties 里写自己的编号
# worker .11
worker=11
# worker .12
worker=12
Filename: data/users_${__P(worker,01)}.csv
Sharing mode: All threads
Recycle on EOF: false
Stop thread on EOF: true
不想逐台改配置时,用内置函数自动取机器名:
方案 B:CSV 用线程号偏移¶
offset 必须每台 worker 不同,而 -Goffset=0 会把同一个值发给所有 worker,做不到差异化。正确做法是在每台 worker 的 user.properties 里写:
方案 C:全局唯一 ID 现场生成¶
适合注册、下单这类需要全局唯一但不需要真实账号的场景。
6. 线程数怎么分¶
不要每台都配 900 线程。总并发按 worker 平均分,考虑机器规格差异:
JMeter 单机线程数经验上限:4C8G 约 300-500 线程(取决于断言与响应体大小),超过后线程切换开销会让 RT 虚高。
7. 结果文件:只在 Controller 侧,不需要合并¶
分布式模式下,worker 会把采样结果回传给 Controller,Controller 用 -l result.jtl 写出的文件已经包含所有 worker 的样本。worker 本地默认不生成 jtl(除非测试计划里显式放了写入本地文件的监听器)。
因此:
- 不要去找
result_11.jtl、result_12.jtl,默认不存在; - 不要手工合并多份 jtl,重复合并会让样本翻倍、TPS 虚高;
-e -o report直接基于 Controller 的result.jtl生成报告即可;- 测试结束时加
-X,让远程 worker 自动退出,避免下次启动报Engine is busy。
验证"所有 worker 都贡献了数据"用样本数对照,而不是找文件:
# check_jtl.py —— 核对样本数、错误构成与时间跨度
import csv
from collections import Counter
rows = list(csv.DictReader(open("result.jtl", encoding="utf-8")))
print("samples:", len(rows))
codes = Counter(r["responseCode"] for r in rows)
print("top codes:", codes.most_common(5))
print("success=false:", sum(1 for r in rows if r["success"] == "false"))
ts = sorted(int(r["timeStamp"]) for r in rows)
print("start(ms):", ts[0], "end(ms):", ts[-1],
"span(min):", round((ts[-1] - ts[0]) / 60000, 1))
# 同一次压测里,若时间跨度明显小于计划时长,说明某台 worker 提前停了
再对照每台 worker 的 jmeter-server.log 末尾的 summary = 行,三台相加应接近 Controller 的样本数。差异超过 1% 就要查:worker 是否掉线、是否因 Stop thread on EOF 提前结束、是否被 -X 之外的原因终止。
注意:各 worker 的时钟要同步(chrony/ntp),否则时间序列图会错位,P95 也会算错。
8. 真实报错对照表¶
| 报错 | 根因 | 解法 |
|---|---|---|
java.rmi.ConnectException: Connection refused to host: 127.0.1.1 |
worker 注册了回环地址 | 启动加 -Djava.rmi.server.hostname=<可达IP> |
Connection refused to host: 192.168.10.11; nested exception is java.net.ConnectException |
worker 未启动或端口未放行 | 检查进程、ss -lntp \| grep 1099、防火墙/安全组 |
The following remote engines could not be started: [192.168.10.12] |
部分 worker 失败 | 单独 jmeter -n -t x.jmx -R 192.168.10.12 定位 |
Engine is busy - please try later |
上一次任务未结束 | 等结束或重启 worker;Controller 加 client.tries |
Could not read chunk size / RMI 序列化异常 |
主从 JMeter 版本或插件不一致 | 统一版本、插件、JDK |
| 结果只有 1/3 的数据 | 某台 worker 掉线或 jtl 未回收 | 每台单独落 jtl,按文件数核对 |
| TPS 增加但 RT 暴涨 | 压测机带宽/端口瓶颈 | 看 sar -n DEV、ss -s;拆更多机器或换 10G 网络 |
| 所有 worker 报同一账号冲突 | CSV 未分片 | 用方案 A/B/C 做数据隔离 |
9. 网络与系统调优¶
# 临时端口范围(默认 32768-60999,短连接压测时可能耗尽)
sysctl -w net.ipv4.ip_local_port_range="10240 65000"
# TIME_WAIT 复用
sysctl -w net.ipv4.tcp_tw_reuse=1
# 文件句柄
ulimit -n 65535
# 连接队列
sysctl -w net.core.somaxconn=65535
这些是压测机侧的调优,不要和服务端调优混在一张表里,否则结论会误导容量评估。
10. 一次可复现的压测记录¶
报告里必须带这些,否则数据无法复现:
版本: order-service 2.3.1 / JMeter 5.6.3 / JDK 17.0.10
拓扑: 3 worker(8C16G ×2,4C8G ×1),Controller 4C8G
脚本: order.jmx,线程 900,Ramp-Up 120s,持续 30min
数据: users_11/12/13.csv 各 5000 行,Recycle=false
结果: TPS 2140,P95 312ms,P99 880ms,错误率 0.02%
资源: API CPU 68%,MySQL CPU 81%,Redis 连接 1800
11. 验证清单¶
- 每台 worker 的 jtl 样本数之和 ≈ 总请求数
- 抽样检查用户名/订单号无重复
- 三台机器的
date相差 < 1s - 关闭断言跑一次、开启断言跑一次,对比 TPS 差异(量化断言开销)
- 单机压测结果与分布式结果在同一 TPS 档位下 P95 接近