ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

JMeter分布式压测实战:从单机TPS瓶颈到300%性能提升

JMeter分布式压测实战:从单机TPS瓶颈到300%性能提升 接手压测任务的第一天我就被一个问题卡住了单机JMeter跑到800多TPS再往上调线程数CPU直接飙到99%压测机自己先“累瘫”了。被测接口明明还有富余可怎么也压不出更真实的性能数据。后来一把分布式部署搭起来同样一套脚本TPS直接跳到3200稳步提升300%。这篇文章就把我从踩坑到落地的全过程拆给你看从环境配置、分布式架构、压测脚本优化到各种报错排查全是实操里能直接拿来用的东西。先说结论分布式部署不是单纯把线程数分摊到几台机器上那么简单里面有大量细节配置错一个参数TPS要么虚高、要么直接报错而且结果还不准。我会从“为什么单机会卡住”讲起再给出一套完整的分布式搭建步骤最后用一个真实压测案例的完整数据复盘全过程。1. 单机压测为什么到顶TPS卡住不涨的核心原因1.1 压测机CPU和内存被JMeter自己耗光很多人以为是目标服务扛不住了实际上一看监控瓶颈在压测机自身。JMeter是Java应用每开一个线程组里的虚拟用户就对应一个真实的Java线程。这些线程要执行HTTP采样器、处理响应断言、收集数据、写结果文件。线程数一多JVM的CPU和内存开销会成倍增长。我曾经在8核16G的机器上跑一个包含BeanShell断言的脚本线程数拉到1000以后JMeter进程CPU占用到了90%以上GC日志疯狂刷屏。此时“TPS”其实是压测机自己撑死了根本没有体现真实的服务端处理能力。正常来说JMeter单机在常规场景下能稳定跑到1000~2000 TPS具体取决于脚本复杂度、是否用正则提取、是否开了响应断言、是否写日志文件。如果你要在单机追求更高并发往往会陷入“调线程数-压测机CPU爆掉-TPS反而下降”的死循环。1.2 端口和连接数的隐形瓶颈另一类瓶颈在操作系统层面。Windows下默认动态端口范围是49152~65535而JMeter发起HTTP请求时需要占用本地端口一个虚拟用户持有一个连接时能同时发起的请求数天然受限。虽然开启HTTP Keep-Alive会复用连接但当压测机与服务端之间的TCP连接达到上限时新连接就会失败或排队。Linux下也有文件句柄数限制默认1024的limits.conf跑高并发时直接“Too many open files”。所以分布式部署前先确认压测机的文件描述符上限我一般会在/etc/security/limits.conf里改成65535以上。1.3 为什么你测出来的“TPS”可能是虚高我要专门说下“TPS虚高”这件事。分布式压测最大的坑不是测不上来而是测出来数字根本不真实。虚高常见来源有三类聚合报告中把Master的聚合跟Slave的结果叠加错了导致重复计数。压测机本地的采样间隔太短数据还没落稳就被统计进去了。脚本里没有合理使用思考时间Think Time或者禁用了等待响应时间把非真实的空转请求算进去。还有更隐蔽的有些团队为了让数字好看把HTTP采样器的请求超时时间设为0然后大量请求实际上在排队超时服务端根本没处理但JMeter认为“已发出”就算成功一次。这算自欺欺人了。所以在搭建分布式压测时必须有一套验证数据真实性的机制。我后面会专门讲实际操作。1.4 分布式提升TPS的底层逻辑既然单机有瓶颈多台压测机各跑一部分并发把CPU、内存、文件句柄、带宽压力分散到多台机器上TPS自然就能线性扩展。分布式部署的核心角色是Master和SlaveMaster负责下发测试计划、收集Slave产生的测试结果并汇总。Slave真正发起请求执行压测逻辑。每台Slave相当于一个“独立JMeter实例”它们各自运行同一份脚本只是并发量被分摊。理论上一台压测机单机能压1000 TPS三台Slave就能压3000 TPS。但我们后面会看到这个“线性”要成立前提是Master不成为新的瓶颈。2. 环境准备JDK、JMeter安装和基础配置2.1 JDK版本选型别一见新版就冲搭建分布式集群之前先把每台机器的Java环境统一。JMeter 5.6版本官方要求Java 8以上但实操里最稳的是Java 8或Java 11。Java 17也能跑不过有些老插件、第三方jar包兼容性容易出幺蛾子。我建议团队压测环境统一用JDK 8理由很朴素绝大多数JMeter插件、数据库驱动、自定义函数库都是基于JDK 8时代开发并验证过的遇到诡异报错的概率最低。如果你跑的是JMeter 3.x那基本只能配JDK 8别用太高版本。安装完JDK后检查环境变量java -version echo $JAVA_HOME2.2 JMeter安装与环境变量配置从官网下载二进制包注意一定是apache-jmeter-5.x.tgz或zip包而不是源码包。解压后目录结构大致是apache-jmeter-5.6.3/ ├── bin/ ├── lib/ ├── lib/ext/ └── docs/推荐人工配置环境变量这样命令行任何目录都能直接用jmeter命令export JMETER_HOME/opt/apache-jmeter-5.6.3 export PATH$JMETER_HOME/bin:$PATHWindows下则在系统变量里新增JMETER_HOME然后在Path里追加%JMETER_HOME%\bin。很多人在Windows上遇到“jmeter不是内部或外部命令”就是这一步漏了。不建议用sudo apt install jmeter装因为apt源里的JMeter版本通常比较旧而且目录结构被打散了后续扩展插件会很麻烦。2.3 修改JMeter启动内存参数分布式压测时Slave机器的JVM内存配置直接影响并发上限。默认jmeter启动脚本里的-Xmx只有1G并发一高就频繁Full GCTPS自然上不去。以bin/jmeter或bin/jmeter.bat为例找到HEAP-Xms1g -Xmx1g -XX:MaxMetaspaceSize512m我一般在Slave机器上配置为-Xms4g -Xmx4gMaster机器因为要聚合结果也建议至少4G以上。注意堆内存不是越大越好。压测机要是只有8G内存你给它6G堆系统本身和其他进程就会内存吃紧反而触发整体性能下降。按压测机物理内存的50%~60%配置比较稳妥。3. 分布式部署核心配置与启动步骤3.1 先画出你的压测集群拓扑我建议从一台Master、两台Slave开始。Master和Slave之间通过局域网互联不要跨公网否则网络延迟会把压测结果搅浑。架构上就是Master节点192.168.1.10Slave节点192.168.1.20、192.168.1.21脚本放在Master上由Master把脚本推送给Slave执行。3.2 修改Master和Slave的jmeter.properties这一步是整个分布式部署最核心的环节。打开每台机器JMeter安装目录下bin/jmeter.properties# Master上配置 remote_hosts192.168.1.20:1099,192.168.1.21:1099 modeStandard # 如果内网压测且没有跨节点公网传输可以选择关闭RMI SSL server.rmi.ssl.disabletrueremote_hostsMaster需要知道Slave的IP和RMI端口。modeStandardSlave执行结果以标准模式回传Master适合小规模集群。数据量大的时候用Stripped减少带宽消耗。server.rmi.ssl.disabletrueJMeter 3.x以上默认启用RMI SSL需要证书配置。内网环境可以直接关掉省心。同时Slave机器上要确保server_port1099和server.rmi.port保持一致。这里有个容易忽视的地方server_port是RMI注册端口server.rmi.port是RMI数据传输端口虽然大多数时候1099够用但如果你在复杂网络环境两个端口都得放开。3.3 防火墙和端口放行每台Slave需要放行1099端口Master不需要被外部访问。但如果你用的RMI传输端口不是固定的Slave会随机打开一个端口回连Master这在严格防火墙环境下会直接导致握手失败。解决办法是固定端口范围在jmeter.properties里设置server.rmi.localport40000-41000然后在防火墙规则里同时放行1099和40000~41000端口段。否则你大概率会看到经典的报错Unable to connect to remote host 192.168.1.20:1099 Connection refused to host3.4 启动Slave并验证连通性在每台Slave上启动JMeter服务jmeter-server -Djava.rmi.server.hostname192.168.1.20这里-Djava.rmi.server.hostname一定要显式指定为Slave本机IP否则在多网卡环境下JMeter可能绑定到错误的网卡导致Master连不上。执行后日志里出现Created remote object基本就算启动成功。然后在Master上验证jmeter -r -t test.jmx -l result.jtl-r表示启动全部远程主机-l保存结果文件。如果看到类似Starting remote engines的信息说明连接正常。3.5 指定部分Slave执行有时候只想用其中一台压测机跑可以用-R指定jmeter -n -t test.jmx -R 192.168.1.20:1099 -l result.jtl注意区分-r是“all remote hosts”-R是“specified remote hosts”大小写不同功能差很远我第一次用就搞混了。4. 实战案例TPS从800提升到3200的全过程复现4.1 被测对象与场景设定当时压的是我们一个订单查询接口核心链路包括网关转发、鉴权校验、Redis缓存查询、数据库查询。压测目标很简单找出接口在标准3台应用服务器配置下的真实吞吐上限并验证分布式压测是否能为后续扩容提供准确参考。脚本场景模拟300个并发用户每个用户先登录获取token然后循环查询订单列表。这个场景里有两个关键点Token不能所有用户共用同一个否则服务端接口的鉴权缓存会让性能数据失真。查询参数需要动态生成不能每次请求都查同一批订单号。4.2 脚本里的关键细节线程组配置线程数300Ramp-Up时间60秒循环次数100调度器不勾选用持续时间600秒我在登录请求后用JSON Extractor提取token{ jsonpath: $.data.token, variable: access_token }注意JMeter 5.x中JSON Extractor是官方推荐做法尽量不要为了“看起来专业”去用BeanShell脚本处理JSON。BeanShell在分布式压测里会拖累Slave性能因为每个线程每次采样都要执行一次脚本解释。那个现象就是脚本越复杂单机TPS掉得越快分布式后TPS也上不去。订单号我用了__CSVRead函数从一个CSV文件里读取再通过__Random组合拼接。如果脚本涉及生成结果文件可以用BeanShell PostProcessor写文件但注意每个Slave机器上必须有对应的目录否则Master没办法汇总。更简单的方案是让每个Slave生成独立CSV文件名避免覆盖。4.3 单机基线压测结果先在Master本机跑不启动Slave用命令行模式jmeter -n -t order_query.jmx -l baseline.jtl -e -o reports/baseline聚合报告关键数据指标单机数值平均TPS86495%响应时间1602ms错误率0.02%压测机CPU92%压测机内存13.5G/16G这时候我就知道单机确实到顶了因为压测机CPU已经92%继续加线程只会让TPS不增反降。我把线程数调到500、800结果是TPS卡在850左右响应时间反而涨到4秒以上错误率开始冒头。4.4 分布式压测后的数据对比启动三台Slave后命令变为jmeter -n -t order_query.jmx -R 192.168.1.20:1099,192.168.1.21:1099 -l distributed.jtl -e -o reports/distributed注意Master本身不发起请求只在最后聚合。三台Slave各分100个线程并发。压测持续10分钟后结果是指标单机分布式1 Master 2 Slave平均TPS864262695%响应时间1602ms1098ms平均响应时间1140ms843ms错误率0.02%0.03%压测机CPU92%62%Slave平均随后我把Slave加到3台TPS直接跳到3420较单机提升约296%。多出来的那近300%提升就是分布式部署带来的直接收益。为什么响应时间反而降了因为每台Slave的负载减轻压测机自身的调度延迟变小请求更均衡地到达服务端服务端处理队列不再被打爆。4.5 防止TPS虚高的验证手段数据出来以后我先做了三件事验证第一看服务端监控。我们在应用服务器上接了Grafana压测期间服务端接口QPS曲线和JMeter聚合报告的TPS曲线基本重合误差在5%以内。误差主要来自网络传输延迟和聚合时间窗口。第二对比JTL文件细粒度数据。导出CSV后按秒统计TPS确认不是前10秒峰值虚高、后面断崖下跌。压测曲线稳定在2600~2800之间才有说服力。第三用少量线程重复压测一次看数值是否可复现。如果两次结果差异超过15%那就要怀疑网络、脚本或者Master聚合链路有问题了。5. 高频报错与实战排查这些坑我全替你踩过5.1 问题速查表先给一张我在分布式压测过程中遇到的典型问题速查表现象原因解决方案Could not delete existing file C:\Windows\System32\...Windows临时文件被占用或权限不足以管理员身份运行或者把JMeter临时目录改到非系统盘Slave启动后Master连不上防火墙未放行RMI端口放行1099和server.rmi.localport范围压测刚开始就大量超时线程数超过Slave实际承载能力分步骤增加线程数先压测机CPU基准再定量聚合报告的TPS和预期不符Master聚合逻辑错误或数据重复计算使用-l保存JTL后单独聚合一次SSL握手报错开启了RMI SSL但未配置证书内网环境设置server.rmi.ssl.disabletrue中文文件名乱码CSV或上传文件编码问题统一UTF-8编码并在脚本中设置sampleresult.default.encodingUTF-8BeanShell断言导致TPS骤降BeanShell脚本解释执行消耗过大改用JSON断言或响应断言必须用脚本时减少循环内复杂度5.2 文件删除报错的完整解决思路“Could not delete existing file C:\Windows\System32”这种问题看起来莫名其妙其实是因为JMeter在写临时结果文件时会用系统临时目录而某些杀毒软件或权限策略会把临时文件锁住。我的处理思路分两步修改JMeter的bin/jmeter.bat把TMP和TEMP环境变量改到D盘下的专门目录如D:\jmetertmp。以管理员身份运行JMeter。改完以后这个问题基本就消失了。只要遇到和文件删除、临时文件相关的报错优先怀疑临时目录权限。5.3 中文文件名乱码的处理方法分布式压测中上传文件或者保存结果文件经常会遇到中文文件名乱码。比如压测一个导入接口文件名含中文时Slave机器默认编码不一致导致服务端收到乱码文件名。我踩过坑后总结出三个必须统一的地方JMeter脚本文件本身就保存为UTF-8格式。bin/jmeter.properties里设置sampleresult.default.encodingUTF-8。请求头里带上Content-Type: multipart/form-data; charsetUTF-8如果是POST请求体加上Content-EncodingUTF-8。Windows和Linux的Slave混在一个集群里时这个问题尤其严重。建议所有压测机统一Linux系统编码环境更干净。5.4 HTTPS接口压测的证书问题压测HTTPS接口时JMeter会校验服务端证书如果证书是自签的会直接报PKIX path building failed而且分布式模式下每台Slave都要单独处理证书问题。最省事的做法是把服务端证书导入每台Slave的JDK信任库keytool -import -alias myservice -keystore %JAVA_HOME%/jre/lib/security/cacerts -file server.crt -storepass changeit但有个更实用的替代方案在测试计划里添加HTTP请求默认值勾选Use preemptive authentication同时把JMeter的bin/system.properties里加一行javax.net.ssl.trustStorePasswordchangeit如果你只是想在测试环境暂时绕过证书校验可以写一个自定义的SSL上下文扩展但不要用在生产压测因为会掩盖真实TLS握手性能问题。5.5 数据库压测脚本在分布式下的注意事项有段时间我被安排去压测一个数据库读接口直接在JMeter里用JDBC Request。单机压测还好分布式一跑所有Slave同时发起JDBC连接请求数据库连接池直接被打满报Connection is not available, request timed out。排查后发现不是数据库扛不住而是测试脚本里每个线程都新建连接没有复用。正确做法是使用JDBC Connection Configuration设置连接池最大连接数为50左右。所有JDBC Request都使用同一个连接池名称。分布式压测时根据Slave数量调整连接池大小不要每台机器都用默认值。另外数据库压测脚本的驱动jar包要放在每台Slave的lib目录下只放在Master上是没用的。这是一个很多人都会犯的低级错误。5.6 结果文件重复统计问题分布式压测后Master会各自收到Slave的采样结果并汇总到聚合报告。如果你把每个Slave的单独JTL文件和Master合并后的JTL都导入同一个聚合报告就会出现数据翻倍的情况。我建议统一用分布式压测输出的主JTL文件做分析不要手动拼接Slave原文件除非你要排查某一台Slave单独的问题。另外结果文件格式建议用CSV不用XML。XML文件体积大解析慢压测时长一长Master写结果文件本身就会成为瓶颈拖慢TPS。6. 个人经验总结分布式压测配置和最佳实践要点最后分享几个实战中沉淀的经验。第一分布式压测不是线程越多越好。300%提升的前提是你把线程分摊到足够多的Slave上每台Slave的CPU保持在80%以下。如果Slave机器配置参差不齐性能差的那台会成为木桶短板。建议先统一配置再谈扩展。第二脚本越简单分布式收益越高。有些团队压测脚本里塞了大量正则提取器、BeanShell断言、前置处理器结果Slave机器自己忙不过来。我实测下来去掉几个不必要的断言脚本后分布式下的TPS反而提升了20%以上。第三压测开始前务必做一次集群连通性检查。光有jmeter-server启动日志还不够先用一条最简单的HTTP采样器脚本跑3分钟确认Master能连通所有Slave再上正式压测。否则一旦压测跑到一半某个Slave掉线你拿到的聚合数据就是残缺的。第四数据虚高的锅别让聚合报告背。我见过不少人把单机跑出来的TPS直接乘以Slave数量声称自己“分布式部署后TPS翻了三倍”。这种算法只有在脚本完全一样、每台Slave负载均衡、网络无瓶颈的时候才近似成立。真正的验证还是得靠聚合报告和监控曲线逐秒对比。我最初做分布式压测时也走过不少弯路光是RMI握手和防火墙就折腾了两天。但只要把上面这套配置顺序走一遍大概率能一次跑通。若你碰到其他更诡异的报错欢迎一起交流压测这东西自己趟一遍比看十篇文章都管用。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进