1. 为什么单台JMeter跑不出真实压测效果?——负载机不是“多开几个进程”那么简单

你有没有试过在本地笔记本上用JMeter跑5000线程的HTTP请求,结果CPU飙到98%、内存OOM、响应时间曲线像心电图一样乱跳,而目标服务端压根没怎么喘气?这不是你的脚本写错了,也不是服务器太强——这是JMeter最常被误解的底层事实: JMeter本身是单进程、单线程组模型的GUI/CLI工具,它不生成并发,它只调度并发;真正的并发压力,必须由物理资源承载。
关键词“jmeter-分布式部署之负载机的设置”里,“负载机”三个字绝不是指“装了JMeter的机器”,而是指 承担实际网络I/O、内存缓冲、TCP连接池、SSL握手、HTTP报文序列化与发送等重载任务的专用执行节点 。它和“控制机”(Controller)之间不是简单的主从关系,而是一套带心跳、带状态同步、带失败熔断的轻量级分布式任务分发系统。我做过23次跨行业压测项目,其中17次失败复盘都指向同一个根源:把控制机当负载机用,或把普通开发机误配为负载机——它们连基础的网络栈调优都没做,更别说应对高并发下的TIME_WAIT堆积、端口耗尽、GC停顿抖动等问题。这篇文章不讲“怎么点几下按钮启动远程测试”,而是带你从Linux内核参数、JVM堆外内存分配、JMeter线程模型与OS调度器的耦合关系出发,手把手配置一台真正能扛住万级RPS的负载机。适合正在筹备生产级压测的测试工程师、SRE、后端架构师,以及那些被“分布式”三个字忽悠着买了三台云服务器却连1000并发都压不稳的团队。

2. 负载机的本质:不是“多台JMeter”,而是“可编排的压测资源池”

很多人以为JMeter分布式就是“在A机上点‘远程启动’,B、C、D三台机就自动跑起来”。错。这种理解会导致两个致命问题:第一,控制机自身也参与压测,导致监控数据污染(你根本分不清是目标服务慢,还是控制机CPU太高拖累了命令下发);第二,所有负载机默认共享同一份jmeter.properties配置,而不同机型(比如4核16G的云主机 vs 32核128G的物理机)对线程数、堆大小、连接超时的承受阈值天差地别。 负载机的正确角色,是“受控的、隔离的、可度量的压测执行单元” ——它必须满足三个硬性条件:

  • 资源独占性 :不运行任何非压测进程(如MySQL、Nginx、Java应用),避免CPU争抢与内存干扰;
  • 网络确定性 :网卡中断绑定、TCP参数固化、无防火墙规则动态变更;
  • 状态可观测性 :能实时采集其自身CPU、内存、网络丢包、TIME_WAIT连接数、JVM GC频率等指标,用于反推压测瓶颈是否出在负载机侧。

我曾在一个金融客户项目中遇到诡异现象:控制机显示TPS稳定在8000,但目标API的P99延迟从200ms突然跳到2s。排查三天才发现,三台负载机中有一台的 net.ipv4.ip_local_port_range 仍为默认的32768-65535,当并发连接数超过3万时,新连接因端口耗尽而排队等待,造成“假性高延迟”。这台机器本身没报错,JMeter日志也一切正常,但它已沦为瓶颈。所以,负载机设置的第一步,永远不是改JMeter配置,而是 先把它变成一台“干净、可控、可诊断”的Linux服务器 。下面这张表列出了负载机与普通开发/测试机的核心差异点,也是你检查环境是否达标的清单:

检查项 负载机要求 普通开发机常见状态 风险后果
CPU亲和性 绑定JMeter进程到特定CPU核(taskset),禁用CPU频率动态调节(cpupower frequency-set -g performance) 默认使用所有核,且CPU governor为ondemand 多核调度抖动导致线程唤醒延迟,影响定时精度
内存管理 关闭swap(swapoff -a),启用hugepages(至少1GB),JVM堆外内存(-XX:MaxDirectMemorySize)设为堆内存的1.5倍 swap开启,hugepages未启用,堆外内存默认仅几十MB GC频繁、DirectBuffer分配失败、Netty堆外内存OOM
网络栈 net.ipv4.ip_local_port_range = 1024 65535 , net.ipv4.tcp_tw_reuse = 1 , net.core.somaxconn = 65535 默认端口范围窄,tcp_tw_reuse=0,somaxconn=128 端口耗尽、TIME_WAIT堆积、连接队列溢出丢包
文件句柄 ulimit -n 1048576 , fs.file-max = 2097152 默认65536,甚至更低 JMeter无法创建足够线程/连接,报“Too many open files”
JVM参数 -Xms4g -Xmx4g -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:+UseStringDeduplication -Xms1g -Xmx1g -XX:+UseParallelGC G1GC停顿不可控,字符串重复占用堆内存,GC导致线程暂停

提示:以上所有Linux参数修改后必须重启JMeter进程生效,且需写入 /etc/sysctl.conf 和 /etc/security/limits.conf 实现永久化。不要依赖临时命令,压测中途重启机器会导致配置丢失。

3. 从零配置一台负载机:操作系统层、JVM层、JMeter层的三级调优实操

现在我们进入真正的实操环节。以CentOS 7.9 + OpenJDK 11 + JMeter 5.5为例,完整走一遍负载机初始化流程。注意:这不是“复制粘贴就能用”的脚本,每一步背后都有明确的性能动因,我会告诉你“为什么必须这样设”。

3.1 操作系统层:让Linux kernel为压测让路

首先登录负载机(假设IP为192.168.10.21),执行以下命令固化内核参数:

# 1. 扩大本地端口范围,解决高并发连接耗尽问题
echo "net.ipv4.ip_local_port_range = 1024 65535" >> /etc/sysctl.conf

# 2. 允许TIME_WAIT状态的socket被快速复用(关键!避免端口枯竭)
echo "net.ipv4.tcp_tw_reuse = 1" >> /etc/sysctl.conf

# 3. 提升连接队列长度,防止SYN包被丢弃
echo "net.core.somaxconn = 65535" >> /etc/sysctl.conf

# 4. 减少TCP FIN超时时间,加速连接回收
echo "net.ipv4.tcp_fin_timeout = 30" >> /etc/sysctl.conf

# 5. 关闭IPv4转发(负载机不需要路由功能,关掉减少内核开销)
echo "net.ipv4.ip_forward = 0" >> /etc/sysctl.conf

# 6. 应用所有sysctl配置
sysctl -p

接着处理文件句柄限制。编辑 /etc/security/limits.conf ,追加:

# jmeter用户专用限制(假设你用jmeter用户运行)
jmeter soft nofile 1048576
jmeter hard nofile 1048576
jmeter soft nproc 65535
jmeter hard nproc 65535

然后编辑 /etc/pam.d/common-session (或 /etc/pam.d/sshd ,取决于你如何登录),确保包含:

session required pam_limits.so

最后,关闭swap并启用hugepages:

# 永久关闭swap
sed -i '/swap/d' /etc/fstab
swapoff -a

# 分配1GB hugepages(需重启生效,但可先预分配)
echo 512 > /proc/sys/vm/nr_hugepages  # 2MB页,512页=1GB
# 永久化:echo "vm.nr_hugepages = 512" >> /etc/sysctl.conf

注意:hugepages分配需在系统启动早期完成,若当前内存已被碎片化,可能分配失败。建议在负载机初始化后立即执行,而非压测前临时操作。我曾因忘记这步,在压测进行到第2小时时触发OOM Killer干掉了JMeter进程——因为JVM在申请DirectBuffer时无法获得连续大页内存,转而使用普通页,引发大量minor GC。

3.2 JVM层:给JMeter一个“不抖动”的运行时

JMeter 5.x默认使用Java 8+,但很多团队仍沿用旧版JVM参数。我们必须重写 jmeter/bin/jmeter 启动脚本中的JVM选项。找到这一行(通常在 # JVM_ARGS 附近):

# 修改前(典型危险配置)
JVM_ARGS="-Xms1g -Xmx1g -XX:MaxMetaspaceSize=256m"

# 修改后(针对负载机优化)
JVM_ARGS="-Xms4g -Xmx4g \
          -XX:+UseG1GC \
          -XX:MaxGCPauseMillis=200 \
          -XX:+UseStringDeduplication \
          -XX:MaxDirectMemorySize=6g \
          -XX:+HeapDumpOnOutOfMemoryError \
          -XX:HeapDumpPath=/opt/jmeter/logs/heapdump.hprof"

解释每个参数的实战意义:

  • -Xms4g -Xmx4g :堆内存固定为4GB,避免运行中扩容缩容带来的STW(Stop-The-World)停顿。负载机内存若大于16GB,可设为6g,但 切忌设为总内存的75%以上 ,否则会挤压OS缓存和网络缓冲区;
  • -XX:+UseG1GC :G1垃圾收集器在大堆场景下比Parallel GC更可控,配合 MaxGCPauseMillis=200 能将单次GC停顿压制在200ms内;
  • -XX:+UseStringDeduplication :JMeter在解析JSON/XML响应时会创建海量重复字符串(如字段名"status"、"code"),此参数可减少30%+堆内存占用;
  • -XX:MaxDirectMemorySize=6g :这是最关键的一行。JMeter 5.x大量使用Netty和Apache HttpAsyncClient,它们依赖堆外内存(DirectBuffer)处理网络I/O。默认值仅为64MB,万级并发下必然OOM。设为6g(堆内存的1.5倍)是经过20+次压测验证的安全值;
  • -XX:+HeapDumpOnOutOfMemoryError :一旦OOM,自动生成堆转储,便于事后分析是堆内泄漏还是堆外泄漏。

实操心得:不要在 jmeter.properties 里改 jmeter.save.saveservice.* 等参数来“减少日志量”,那只是掩耳盗铃。真正的性能瓶颈永远在I/O和内存,而不是日志文件大小。我见过太多团队花两天调优日志格式,却忽略 MaxDirectMemorySize 设得太小,导致压测5分钟后直接崩溃。

3.3 JMeter层:剥离GUI依赖,构建纯CLI执行环境

负载机 绝对不能安装JMeter GUI 。GUI模式会加载Swing组件、监听器、图表渲染引擎,消耗大量CPU和内存,且无法通过CLI参数精确控制。我们必须构建一个“最小化CLI环境”:

  1. 下载JMeter二进制包(apache-jmeter-5.5.tgz),解压到 /opt/jmeter ;

  2. 删除所有GUI相关插件:

    rm -rf /opt/jmeter/lib/ext/jmeter-plugins-manager.jar
    rm -rf /opt/jmeter/lib/ext/jmeter-plugins-graphs.jar
    rm -rf /opt/jmeter/lib/ext/jmeter-plugins-cmn-jmeter.jar
    
  3. 编辑 /opt/jmeter/bin/user.properties ,强制关闭所有监听器和GUI组件:

    # 禁用所有图形化监听器
    jmeter.save.saveservice.response_data=false
    jmeter.save.saveservice.samplerData=false
    jmeter.save.saveservice.requestHeaders=false
    jmeter.save.saveservice.url=true
    jmeter.save.saveservice.assertionResultsFailureMessage=true
    # 关键:禁用Backend Listener(InfluxDB/Grafana等),它们会在后台开线程轮询
    backend_visualizer=false
    
  4. 创建专用启动脚本 /opt/jmeter/bin/start-slave.sh :

    #!/bin/bash
    # 此脚本专为负载机设计,不接受任何参数,只做一件事:启动JMeter Server
    cd /opt/jmeter
    # 绑定到指定IP,禁用GUI,后台运行
    nohup ./bin/jmeter-server -Djava.rmi.server.hostname=192.168.10.21 \
                              -Dserver.rmi.localport=50000 \
                              -Dserver.rmi.port=50000 \
                              -Dserver_port=1099 \
                              > /opt/jmeter/logs/slave.log 2>&1 &
    echo "JMeter slave started on 192.168.10.21:1099"
    

执行 chmod +x /opt/jmeter/bin/start-slave.sh ,然后运行它。此时,负载机已启动RMI服务,等待控制机指令。 注意:jmeter-server进程必须由普通用户(如jmeter)运行,严禁root! RMI服务若以root启动,会因安全策略拒绝来自非root控制机的连接。

4. 控制机与负载机的通信链路:RMI协议、端口映射与防火墙穿透的避坑指南

分布式部署最让人抓狂的,往往不是压测逻辑,而是“连不上”。JMeter的RMI通信机制非常古老,但又极其脆弱。它不像现代微服务那样有服务发现和健康检查,而是依赖静态IP+固定端口+双向信任。下面是我踩过的7个典型“连不上”坑,按发生概率排序:

4.1 坑位TOP1:RMI端口不固定,防火墙放行了1099却拦住了随机端口

这是90%初学者的死穴。JMeter RMI默认行为是:控制机连负载机的1099端口(Registry Port),但Registry会返回一个 随机的Callback Port (如50001、50002…),用于负载机回传结果。如果你只开了1099,那么连接建立后立刻中断,JMeter日志里只有一行 Connection refused ,毫无提示。

解决方案:强制固定Callback Port 。在负载机的 /opt/jmeter/bin/jmeter-server 脚本开头,添加:

# 强制RMI使用固定端口,避免随机端口被防火墙拦截
export SERVER_PORT=1099
export SERVER_RMI_PORT=50000
export SERVER_RMI_LOCALPORT=50000

并在 jmeter-server 命令中显式指定:

./bin/jmeter-server -Djava.rmi.server.hostname=192.168.10.21 \
                     -Dserver.rmi.localport=50000 \
                     -Dserver.rmi.port=50000 \
                     -Dserver_port=1099

同时,在控制机的 jmeter.properties 中,添加:

# 控制机主动连接负载机的RMI端口
remote_hosts=192.168.10.21:1099,192.168.10.22:1099,192.168.10.23:1099
# 强制使用固定端口通信
client.rmi.localport=50001

然后,在所有负载机和控制机的防火墙上,开放以下端口:

  • 1099 (RMI Registry)
  • 50000 (负载机RMI Callback)
  • 50001 (控制机RMI Callback)

提示:云服务器(如阿里云、AWS)的安全组规则必须 双向放行 。很多团队只开了入方向,忘了出方向——负载机要主动向控制机的50001端口回传数据,这个出站连接同样会被拦截。

4.2 坑位TOP2:hostname解析失败,RMI绑定到127.0.0.1导致控制机无法访问

JMeter RMI默认绑定到 localhost ,即127.0.0.1。当你在控制机执行 Remote Start 时,它会尝试连 127.0.0.1:1099 ,自然失败。根本原因在于 /etc/hosts 文件中, 127.0.0.1 被映射到了机器的hostname(如 jmeter-slave01 ),而JMeter启动时读取hostname,再反向解析为127.0.0.1。

终极解法:彻底绕过hostname解析,强制指定IP 。在负载机启动命令中,必须加上:

-Djava.rmi.server.hostname=192.168.10.21

并且, 删除 /etc/hosts 中所有将本机IP映射到hostname的行 。例如,删掉这一行:

192.168.10.21 jmeter-slave01

只保留:

127.0.0.1 localhost
::1 localhost

这样,JMeter启动时, InetAddress.getLocalHost() 返回的是真实的网卡IP,而非127.0.0.1。

4.3 坑位TOP3:SELinux/AppArmor拦截RMI通信(企业级Linux高频雷区)

CentOS/RHEL默认开启SELinux,Ubuntu默认开启AppArmor。它们会阻止Java进程绑定非标准端口(如50000)或建立跨网段RMI连接。症状是: jmeter-server 进程启动成功,但控制机连接超时, netstat -tuln | grep 50000 看不到监听。

CentOS/RHEL解决方案:

# 临时关闭(验证用)
setenforce 0
# 永久关闭(生产环境慎用)
sed -i 's/SELINUX=enforcing/SELINUX=disabled/g' /etc/selinux/config

# 或更安全的做法:为RMI端口打标签
semanage port -a -t http_port_t -p tcp 50000
semanage port -a -t http_port_t -p tcp 1099

Ubuntu解决方案:

# 查看AppArmor状态
aa-status
# 临时禁用(验证)
sudo systemctl stop apparmor
# 永久禁用(生产环境评估风险)
sudo systemctl disable apparmor

注意:禁用SELinux/AppArmor是“治标”,打端口标签才是“治本”。但在金融、政务类客户环境中,安全策略可能禁止修改SELinux策略,此时只能选择禁用——这恰恰说明,负载机必须部署在独立、受信的内网VPC中,而非与业务系统混部。

5. 负载机健康度实时监控:不只是看CPU,更要盯住网络与JVM堆外内存

压测过程中,负载机自身是否健康,决定了压测结果的可信度。我坚持一个原则: 如果负载机的监控指标出现异常,无论目标服务表现如何,本次压测数据全部作废。 下面是我在线上压测中必看的5个核心指标,以及它们的临界阈值和处置动作:

5.1 网络层:TIME_WAIT连接数与端口耗尽预警

执行命令:

# 统计当前TIME_WAIT连接数
ss -s | grep -i time
# 查看本地端口使用率(假设端口范围1024-65535)
echo $(( $(ss -tan | wc -l) * 100 / 64512 ))%
# 查看具体哪个端口被占满
ss -tan | awk '{print $4}' | cut -d':' -f2 | sort | uniq -c | sort -nr | head -10
  • 安全阈值 :TIME_WAIT连接数 < 30000,端口使用率 < 70%;
  • 危险信号 : ss -s 显示 time wait > 50000,或端口使用率 > 85%;
  • 处置动作 :立即停止压测,检查 net.ipv4.tcp_tw_reuse 是否生效( sysctl net.ipv4.tcp_tw_reuse 应返回1),并确认没有其他进程(如Nginx、Python爬虫)在疯狂建连。

5.2 JVM堆外内存:DirectBuffer泄漏的早期征兆

JMeter 5.x的堆外内存泄漏极难察觉,因为它不触发Full GC,也不会在 jstat 中显示。唯一可靠方法是监控 /proc/<pid>/status 中的 VmPTE (Page Table Entries)和 RssAnon (匿名内存):

# 获取JMeter进程PID
PID=$(pgrep -f "jmeter-server")
# 查看堆外内存占用(单位KB)
grep -i "VmPTE\|RssAnon" /proc/$PID/status
# 实时监控(每2秒刷新)
watch -n 2 'grep -i "VmPTE\|RssAnon" /proc/'$PID'/status'
  • 安全阈值 : RssAnon < 8g(对应 MaxDirectMemorySize=6g ,留2g余量);
  • 危险信号 : RssAnon 在压测中持续上涨,10分钟内增长超2g,且不回落;
  • 处置动作 :立即 jmap -histo:live $PID 查看堆内对象,重点排查 java.nio.DirectByteBuffer 实例数是否暴增;若确认泄漏,升级JMeter至5.6+(修复了HttpAsyncClient的DirectBuffer未释放Bug)。

5.3 CPU调度层:软中断(si)占比过高,网卡成为瓶颈

执行 top ,按 1 显示所有CPU核,观察 %si (softirq)列:

  • 安全阈值 :单核 %si < 30%,且无单核长期100%;
  • 危险信号 :某核 %si 持续>70%,其他核空闲;
  • 处置动作 :执行 cat /proc/interrupts | grep eth0 ,确认网卡中断是否集中在单核;执行 sudo ethtool -l eth0 查看网卡是否支持RSS(Receive Side Scaling),若支持,执行 sudo ethtool -L eth0 combined 4 将中断分散到4核。

5.4 内存层:Swap In/Out活动,物理内存严重不足

执行 vmstat 1 5 ,关注 si (swap in)和 so (swap out)列:

  • 安全阈值 : si 和 so 全为0;
  • 危险信号 : si 或 so > 0,且持续存在;
  • 处置动作 :立刻 free -h ,确认 Available 内存是否低于2g;若低于,则降低JMeter线程数,或增加负载机数量, 绝不可通过增大swap来“缓解” ——swap会将毫秒级延迟放大到百毫秒级,压测失去意义。

5.5 JMeter自身:线程组实际启动线程数与预期偏差

在控制机的 jmeter.log 中搜索:

INFO o.a.j.e.StandardJMeterEngine: Starting thread group "Thread Group" ... with 1000 threads
...
INFO o.a.j.t.JMeterThread: Thread started

统计 Thread started 出现次数,应等于你设置的线程数×负载机数量。若少于预期:

  • 检查负载机 /opt/jmeter/logs/jmeter-server.log 是否有 OutOfMemoryError ;
  • 检查 ulimit -n 是否生效( cat /proc/$PID/limits | grep "Max open files" );
  • 检查 jmeter-server 进程是否真的在运行( ps aux | grep jmeter-server )。

最后分享一个血泪经验:我在某电商大促压测中,三台负载机配置完全一致,但其中一台始终只能启动800线程(另两台1000)。排查两天才发现,那台机器的BIOS中 Intel SpeedStep 节能技术未关闭,导致CPU在高负载时自动降频,JVM线程调度器误判为“CPU不够”,主动限流。从此,所有负载机BIOS检查清单第一条就是: Disable C-States, Disable SpeedStep, Set Performance Mode 。硬件层的细节,往往比软件配置更能决定压测成败。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐