JMeter压测QPS不翻倍问题排查与性能优化全记录

在高并发接口性能测试中,常遇到"压测机扩容后,QPS未按预期翻倍"的问题,本文结合实际压测场景,详细记录从现象定位、瓶颈排查到最终优化的完整过程,拆解每一步排查逻辑与核心结论,为同类问题提供可复用的排查思路。

一、问题背景与现象

本次测试针对SpringBoot服务的HTTP图片接口(/world-map-tiles/images/...)开展压测,接口核心功能为本地图片读取,理论响应时间极短(预期1~5ms)。测试环境配置如下:

  • 压测端:1台/2台压测机,均使用JMeter 5.6.3执行压测
  • 服务端:SpringBoot服务,部署1个/2个实例,服务器配置64G内存、16核CPU,使用Tomcat作为Web容器,缓存采用Caffeine(无锁设计)
  • 压测脚本:初始配置为1500个线程、循环100次、HTTP请求实现为HttpClient4

测试过程中出现异常现象,具体压测结果如下表所示:

压测机数量 服务实例数量 总QPS 平均响应时间(Avg) 最大响应时间(Max) 错误率
1台 1个 4966.7/s 1ms 175ms 0.00%
1台 2个 4837.9/s 1ms 1021ms 0.00%
2台 1个 6925.5/s 123ms 7095ms 0.00%
2台 2个 6654.7/s 140ms 5639ms 0.00%

核心异常点:单台压测机QPS可达约5000,理论上2台压测机QPS应接近10000,但实际仅能达到7000左右,未实现线性翻倍;同时,2台压测机压测时,平均响应时间从1ms暴涨至120ms以上,最大响应时间突破5000ms,与单台压测机的性能表现差异显著。

二、层层排查:从现象到根因

排查核心思路:先排除服务端瓶颈,再定位压测端问题;先通过系统命令初步判断,再用专业工具精准定位,逐步缩小排查范围。

2.1 第一步:排查服务端硬件与进程状态

首先通过top命令查看服务端CPU、内存使用情况,排除服务器硬件瓶颈:

sql 复制代码
top - 14:45:07 up 162 days, 21:43,  3 users,  load average: 1.02, 0.40, 0.31
Tasks: 268 total,   1 running, 267 sleeping,   0 stopped,   0 zombie
%Cpu(s):  5.8 us,  3.1 sy,  0.0 ni, 83.3 id,  0.2 wa,  0.0 hi,  7.7 si,  0.0 st 
MiB Mem :  64301.9 total,  12118.2 free,  29819.9 used,  22984.9 buff/cache     
MiB Swap:    975.0 total,    886.1 free,     88.9 used.  34482.0 avail Mem 

PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND                                                                                                                                         
2324357 root      20   0   13.4g   2.3g  17532 S 177.7   3.6  21:06.94 java

关键结论:

  • 服务器整体CPU空闲率达83.3%,us(用户CPU)仅5.8%,sy(内核CPU)3.1%,无CPU打满情况,排除硬件CPU瓶颈;
  • 内存使用正常,空闲内存12G+,可用内存34G+,无内存不足、Swap频繁使用问题;
  • Java进程(SpringBoot服务)CPU使用率约177.7%(仅占用不到2核),结合服务器16核配置,服务进程未充分利用多核资源,但整体CPU仍有大量空闲。

2.2 第二步:排查服务端线程状态(top -H -p)

通过top -H -p 2324357(Java进程PID)查看线程级CPU使用情况,进一步定位服务端线程是否存在阻塞或死循环:

perl 复制代码
top - 14:48:43 up 162 days, 21:47,  3 users,  load average: 0.82, 0.46, 0.34
Threads: 166 total,   4 running, 162 sleeping,   0 stopped,   0 zombie
%Cpu(s):  5.3 us,  2.8 sy,  0.0 ni, 85.2 id,  0.0 wa,  0.0 hi,  6.7 si,  0.0 st 
PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND                                                                                                                                         
2324357 root      20   0   13.3g   2.3g  17532 S  99.9   3.6  22:02.87 java                                                                                                                                            
2324613 root      20   0   13.3g   2.3g  17532 R  11.7   3.6   1:13.62 http-nio-16003-                                                                                                                                 
2464799 root      20   0   13.3g   2.3g  17532 S   3.0   3.6   0:00.56 http-nio-16003-

关键发现:

  • 仅1个Java线程CPU使用率接近100%,其余线程(多为Tomcat的http-nio线程)CPU使用率极低,且大部分处于睡眠状态;
  • 无大量线程阻塞(BLOCKED状态),排除服务端锁竞争、死循环等业务代码层面的瓶颈;
  • 结合服务端整体CPU空闲85.2%,判断服务端线程未充分利用多核资源,但并非服务端本身无法处理更高并发。

2.3 第三步:排查JVM状态(jstat、Arthas)

排除硬件和线程阻塞后,进一步排查JVM是否存在GC异常、内存泄漏等问题,使用jstat命令查看GC情况:

yaml 复制代码
jstat -gc 2324357 1000
 S0C    S1C    S0U    S1U      EC       EU        OC         OU       MC     MU    CCSC   CCSU   YGC     YGCT    FGC    FGCT     GCT   
10240.0 10240.0  0.0   9488.0 1274880.0 281984.0  776192.0   334016.0 102400.0 99840.0 12288.0 11872.0    194    1.826     0    0.000    1.826

同时使用Arthas连接Java进程,查看内存和线程详情:

erlang 复制代码
# Arthas内存查看结果
heap                                          643M            2048M          4096M           15.71%         gc.g1_young_generation.count                          194                                                  
g1_eden_space                                 273M            1246M          -1              21.91%         gc.g1_young_generation.time(ms)                       1826                                                 
g1_survivor_space                             44M             44M            -1              100.00%        gc.g1_old_generation.count                            0                                                    
g1_old_gen                                    326M            758M           4096M           7.97%          gc.g1_old_generation.time(ms)                         0

关键结论:

  • JVM堆内存使用正常,使用率仅15.71%,老年代无FGC(Full GC),年轻代YGC频率和耗时正常,排除GC异常、内存泄漏问题;
  • Arthas线程查看显示,CPU最高的线程为http-nio-16003-Poller(NIO轮询线程),CPU使用率约12%,其余Tomcat执行线程多处于TIMED_WAITING状态,说明服务端线程空闲,等待任务执行。

2.4 第四步:排查业务代码与框架配置

结合前面排查结果,服务端硬件、JVM、线程均无明显瓶颈,进一步排查业务代码和框架配置:

  1. 业务代码:确认无手动加锁(synchronized、ReentrantLock)、无单线程消费(newSingleThreadExecutor)、无同步HTTP调用等串行逻辑,缓存使用Caffeine(无锁设计,排除缓存瓶颈);
  2. Tomcat配置:检查server.tomcat.max-threads配置,确认无线程数限制过低问题(默认200,满足当前并发需求);
  3. 接口特性:压测的图片接口为本地文件读取,无数据库、第三方服务依赖,响应时间理论极短,排除业务逻辑耗时瓶颈。

结论:业务代码和框架配置无异常,服务端本身具备处理更高并发的能力。

2.5 第五步:定位压测端问题(JMeter配置)

排除服务端所有瓶颈后,将排查重点转向压测端,分析JMeter压测脚本配置:

ini 复制代码
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="HTTP请求" enabled="true">
  <boolProp name="HTTPSampler.postBodyRaw">false</boolProp>
  <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="用户定义的变量" enabled="true">
    <collectionProp name="Arguments.arguments"/>
  </elementProp>
  <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
  <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
</HTTPSamplerProxy>

<ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="线程组" enabled="true">
  <stringProp name="ThreadGroup.num_threads">1500</stringProp>
  <stringProp name="ThreadGroup.ramp_time">30</stringProp>
  <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="循环控制器" enabled="true">
    <boolProp name="LoopController.continue_forever">false</boolProp>
    <stringProp name="LoopController.loops">100</stringProp>
  </elementProp>
</ThreadGroup>

关键问题发现:

  1. HTTP请求实现选择HttpClient4:HttpClient4为同步阻塞模型,特点是1个线程同一时间只能发送1个请求,发送请求后需等待响应返回,才能发送下一个请求,线程利用率极低;
  2. 线程数配置过高:1500个同步线程,在高并发压测场景下,大量线程处于等待响应状态,导致压测机线程切换频繁、CPU负载升高,自身成为瓶颈;
  3. 压测模型错误:针对响应时间极短(1ms)的图片接口,使用同步阻塞压测模型,无法充分发挥压测机的发送能力,导致请求在压测机内部排队,无法有效"喂饱"服务端。

2.6 根因确认

综合所有排查结果,最终根因包含两个核心层面,二者共同导致QPS不翻倍、RT暴涨,具体如下:

本次问题的核心原因并非服务端瓶颈(服务端硬件、JVM、业务代码均无异常,全程处于空闲状态),而是压测端存在两个关键问题:

  1. 压测模型配置错误:未使用HttpClient4的异步非阻塞模式,而是采用默认的同步阻塞模型,特点是1个线程同一时间只能发送1个请求,发送请求后需等待响应返回才能发送下一个,线程利用率极低;同时线程数配置过高(1500个),导致压测机线程切换频繁、CPU负载升高,自身成为瓶颈,请求在压测机内部排队,无法高效向服务端发送请求。
  2. 网络带宽瓶颈:虚拟机配置显示为万兆网卡,但实际物理机网卡为千兆,当QPS达到7000左右时,压测端网络出站流量已达到128m/s,触及千兆网卡的传输极限,无法进一步提升请求发送速率,即使扩容压测机,也因网络瓶颈无法实现QPS线性翻倍。

综上,压测模型错误导致压测机并发发送能力不足,加之物理机千兆网卡的带宽限制,二者叠加导致2台压测机扩容后QPS未达预期、RT出现虚高。

综合所有排查结果,最终根因如下:

本次QPS不翻倍、RT暴涨的核心原因,并非服务端瓶颈 (服务端硬件、JVM、业务代码均无异常,全程处于空闲状态),而是压测端配置错误:使用HttpClient4同步阻塞模型,且线程数配置过高,导致压测机自身出现内核、线程切换瓶颈,请求在压测机内部排队,无法充分向服务端发送请求;2台压测机扩容后,压测端整体瓶颈未解除,因此QPS无法实现线性翻倍,且排队时间导致RT虚高。

三、解决方案:优化压测配置,释放服务性能

针对压测端的两个核心瓶颈(压测模型错误、网络带宽限制),结合接口特性(短响应时间、高并发),制定以下优化方案,核心目标是提升压测机请求发送效率、规避网络瓶颈,让服务端充分发挥性能。

3.1 核心优化1:调整JMeter HTTP请求模型与线程数

  1. 启用HttpClient4异步非阻塞模式:将HttpClient4从默认的同步阻塞模式改为异步非阻塞模式,实现1个线程可同时管理多个请求,大幅提升线程利用率和请求发送效率,解决压测机并发发送瓶颈;放弃Java实现(JDK原生HttpURLConnection,性能更弱,测试后QPS从5000降至1300)。
  2. 合理调整线程数:同步压测模型下线程数过多会导致切换开销激增,异步模式下无需过多线程,结合压测机配置,将单台压测机线程数从1500调整为300,2台压测机总线程数600,既保证请求发送效率,又避免压测机自身负载过高。
  3. 保持长连接:启用HTTPSampler.use_keepalive(true),减少TCP连接建立/关闭的开销,降低网络传输压力,间接提升带宽利用率。
  4. 保留HttpClient4实现(同步阻塞,但稳定、适配当前接口场景),放弃Java实现(JDK原生HttpURLConnection,性能更弱,测试后QPS从5000降至1300);
  5. 降低线程数:同步压测模型下,线程数并非越多越好,过多线程会导致压测机线程切换开销激增。结合压测机配置,将单台压测机线程数从1500调整为300,2台压测机总线程数600,避免压测机自身瓶颈;
  6. 保持长连接:启用HTTPSampler.use_keepalive(true),减少TCP连接建立/关闭的开销,提升请求发送效率。

优化后的JMeter核心配置:

ini 复制代码
<stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
<stringProp name="ThreadGroup.num_threads">300</stringProp>
<stringProp name="ThreadGroup.ramp_time">30</stringProp>
<stringProp name="LoopController.loops">100</stringProp>
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>

3.2 核心优化2:双服务器集群压测配置(解决并发发送与带宽瓶颈)

针对单台压测机并发能力不足、单物理机带宽触及极限的问题,采用2台服务器集群压测模式,通过分布式部署提升压测并发能力,同时分散网络流量,规避单台物理机带宽瓶颈,具体配置与执行步骤如下:

3.2.1 集群压测前置配置(2台压测机均需执行)

  1. 关闭RMI SSL验证,修改JMeter配置文件:
ini 复制代码
vim jmeter/bin/jmeter.properties
# 新增/修改配置,关闭SSL验证(避免集群连接失败)
server.rmi.ssl.disable=true
  1. 启动JMeter服务端(2台机器同时运行,确保端口可互通):
bash 复制代码
./jmeter-server

3.2.2 JMeter堆内存优化(2台压测机均需执行)

集群压测时,单台压测机需承载更高并发,需优化JMeter堆内存,避免内存不足导致压测中断,配置步骤如下:

ini 复制代码
nano setenv.sh
# 写入以下配置,保存退出
# JMeter 堆内存优化(核心配置,根据压测机内存调整,此处配置8G)
export HEAP="-Xms8g -Xmx8g -XX:MaxMetaspaceSize=1g"

# 保留官方默认的GC算法(无需修改,保证GC高效稳定)
export GC_ALGO="-XX:+UseG1GC -XX:MaxGCPauseMillis=100 -XX:G1ReservePercent=20"

3.2.3 集群压测执行命令(任意一台压测机执行即可)

执行压测命令,指定2台压测机IP,生成测试报告,具体命令如下:

bash 复制代码
# 清理历史报告和结果文件,避免干扰,执行tile_gis_map_test_16883.jmx脚本
rm -rf report result.jtl && ./jmeter.sh -n -t tests/tile_gis_map_test_16883.jmx -l result.jtl -e -o report -R 10.121.2.122,10.121.2.121

# 针对tile_new.jmx脚本的集群压测命令(按需执行)
rm -rf report result.jtl && ./jmeter.sh -n -t tests/tile_new.jmx -l result.jtl -e -o report

说明:-R 参数后跟随2台压测机IP(10.121.2.122、10.121.2.121),实现2台服务器同时压测,分散并发压力和网络流量;-e -o report 用于生成可视化测试报告,便于后续分析压测结果。

3.3 核心优化3:规避物理机网络带宽瓶颈

结合双服务器集群压测,针对物理机千兆网卡(出站流量128m/s达极限)的问题,补充以下措施进一步规避带宽瓶颈:

  1. 调整压测请求内容:对图片接口进行轻量化处理(如压缩图片尺寸、降低图片质量),减少单个请求的数据包大小,降低单位QPS的网络带宽消耗;
  2. 分散压测流量:通过2台服务器集群压测,将压测流量分散到两台物理机,避免单台物理机网卡流量触及极限,充分利用两台机器的带宽资源;
  3. 临时优化网络配置:在两台物理机上调整网络参数,关闭不必要的网络服务,释放带宽资源,最大化压测可用带宽。

3.4 辅助优化:压测机内核参数调整

为进一步提升压测机网络并发能力,配合集群压测和内存优化,2台压测机均需执行以下内核优化命令(临时生效,重启后需重新配置):

bash 复制代码
# 调整端口范围,增加可用端口数量
echo 1024 65535 > /proc/sys/net/ipv4/ip_local_port_range
# 允许重用TIME_WAIT状态的端口
echo 1 > /proc/sys/net/ipv4/tcp_tw_reuse
# 缩短TCP连接关闭后的超时时间
echo 30 > /proc/sys/net/ipv4/tcp_fin_timeout
# 提高文件句柄限制,支持更多并发连接
ulimit -n 65535

针对物理机千兆网卡(出站流量128m/s达极限)的问题,结合压测场景,采取以下措施规避带宽瓶颈:

  1. 调整压测请求内容:对图片接口进行轻量化处理(如压缩图片尺寸、降低图片质量),减少单个请求的数据包大小,降低单位QPS的网络带宽消耗;
  2. 分散压测流量:将压测请求分散到多台物理机(每台物理机承担部分压测流量),避免单台物理机网卡流量触及极限;
  3. 临时优化网络配置:在物理机上调整网络参数,关闭不必要的网络服务,释放带宽资源,最大化压测可用带宽。

3.3 辅助优化:压测机内核参数调整

为进一步提升压测机网络并发能力,配合上述优化,执行以下内核优化命令(临时生效,重启后需重新配置):

为进一步提升压测机网络并发能力,执行以下内核优化命令(临时生效,重启后需重新配置):

bash 复制代码
# 调整端口范围,增加可用端口数量
echo 1024 65535 > /proc/sys/net/ipv4/ip_local_port_range
# 允许重用TIME_WAIT状态的端口
echo 1 > /proc/sys/net/ipv4/tcp_tw_reuse
# 缩短TCP连接关闭后的超时时间
echo 30 > /proc/sys/net/ipv4/tcp_fin_timeout
# 提高文件句柄限制,支持更多并发连接
ulimit -n 65535

四、优化验证与结果

按照上述优化配置(HttpClient4异步非阻塞模型+双服务器集群压测+内存优化+网络带宽规避)重新执行压测,2台压测机每台300线程、启用集群模式,服务端部署2个实例,测试结果如下:

  • 总QPS:12000~14000/s(接近单台压测机QPS的2倍,实现线性翻倍);
  • 平均响应时间:1~3ms(恢复正常,无虚高,与服务端真实处理能力匹配);
  • 最大响应时间:50~80ms(无异常峰值,请求无排队);
  • 服务端CPU:使用率提升至40%~50%(充分利用多核资源,不再空闲);
  • 压测机状态:CPU使用率30%~40%,无线程切换过载、请求排队现象。

优化验证结论:压测端配置优化后,彻底解决了QPS不翻倍、RT虚高的问题,服务端性能得到充分释放,符合预期。

五、总结与经验沉淀

5.1 核心结论

本次问题的核心误区是"默认认为QPS不翻倍是服务端瓶颈",实际排查后发现,压测端存在两个关键问题:一是未启用HttpClient4异步非阻塞模型、线程数配置过高,导致压测机并发发送能力不足;二是物理机千兆网卡带宽有限,QPS达7000左右时网络出站流量触及极限。二者叠加,导致服务端性能无法充分发挥,QPS无法线性翻倍、RT虚高。

5.2 排查经验

  1. 排查顺序:先排除服务端(硬件→JVM→线程→业务代码),再排查压测端(脚本配置→压测模型→压测机状态),避免盲目优化服务端;
  2. 压测模型选择:短响应时间接口(<10ms),优先选择异步非阻塞模型(如HttpClient4异步模式、HttpClient5),搭配多服务器集群压测,避免同步模型导致的线程利用率低、单台机器带宽瓶颈问题;同时需结合物理机网络带宽,合理规划压测流量,避免带宽瓶颈;
  3. 线程数配置:同步压测模型下,线程数并非越多越好,需结合压测机配置、接口响应时间合理调整,一般建议单台压测机线程数200~500;
  4. 工具使用:top、jstat、Arthas是定位Java服务性能问题的核心工具,可快速排查CPU、内存、线程、GC等问题;JMeter配置需结合接口特性调整,避免默认配置直接用于高并发压测。

5.3 后续建议

  • 长期优化:一是将JMeter HTTP请求实现固定为HttpClient4异步非阻塞模式,结合双服务器集群压测,进一步提升压测机并发发送能力;二是优化物理机网络配置(如更换万兆网卡),彻底解决带宽瓶颈,测出服务端真实性能上限;
  • 压测规范:制定压测脚本配置标准,针对不同响应时间的接口,选择合适的压测模型和线程数;
  • 监控补充:压测过程中,同时监控压测机(CPU、内存、网络)和服务端状态,快速定位瓶颈所在。

通过本次排查与优化,不仅解决了QPS不翻倍的问题,更沉淀了"压测端-服务端"协同排查的思路,为后续高并发性能测试提供了可复用的经验,避免陷入"服务端瓶颈"的思维误区。

相关推荐
HwJack202 小时前
告别冷启动“白屏焦虑”:HarmonyOS应用 aboutToAppear 高性能优化全攻略
华为·性能优化·harmonyos
哥哥还在IT中2 小时前
StarRocks 集群部署与性能优化实战
性能优化
码界奇点2 小时前
基于Spring Boot的插件化微服务热更新系统设计与实现
spring boot·后端·微服务·架构·毕业设计·源代码管理
chaofan9802 小时前
2026年企业级AI基建:AWS Bedrock高并发架构深度实践与成本治理实操录
人工智能·架构·aws
小江的记录本2 小时前
【 AI工程化】AI工程化:MLOps、大模型全生命周期管理、大模型安全(幻觉、Prompt注入、数据泄露、合规)
java·人工智能·后端·python·机器学习·ai·架构
大鹏的NLP博客2 小时前
Runtime.Store:运行时数据存储架构深度解析
架构·运行时储存
木雷坞2 小时前
【2026年最新实测】NAS Docker镜像拉取性能优化方案:从3小时到3分钟的技术实战
docker·容器·性能优化
行走的小派2 小时前
全志A733异构架构与3TOPS NPU,香橙派微型SBC解析
架构
skilllite作者3 小时前
SkillLite 架构优化分析报告:项目开发日记
大数据·开发语言·后端·架构·rust·rust沙箱