☰
SpringCloud微服务脚手架JMeter压测实战与调优全记录
2026/10/22 5:02:38 网站建设 项目流程

SpringCloud微服务开发脚手架跑了一次完整的JMeter压测,从脚本设计到结果分析踩了不少坑,这篇文章把整个流程、关键参数和调优思路都整理出来,给准备做微服务性能验证的同学一份能直接参考的实战记录。

先说背景。我手上的这个脚手架是某项目团队内部沉淀的一套SpringCloud基础框架,集成了网关、注册中心、配置中心、OpenFeign、Sentinel限流等常用组件,业务方新服务基本都是基于这套脚手架来开发。此前只验证过功能和联调,从没做过系统性的压力测试,大家对它的容量上限、性能瓶颈和配置是否合理其实心里没底。所以这次压测的目标很明确:验证脚手架在典型业务场景下的吞吐能力,定位潜在瓶颈,给出可落地的调优建议。

1. 测试目标与场景设计

1.1 先想清楚压什么,再动手压

很多人一上手就是建线程组、配并发数、跑聚合报告,但微服务的压测和单体应用有一个很大的区别:请求链路长、组件多,如果不先明确测试目标和业务场景,压出来的数据很难指导优化。

我这次把测试目标拆成了三个层级:

  • 功能验证级:确认脚手架各组件在压力下不报错、不超时,接口返回数据正确。
  • 容量评估级:找到系统在可接受的响应时间(比如RT p99 < 500ms)下的最大吞吐量(TPS)。
  • 瓶颈定位级:在压测过程中同步监控各节点的CPU、内存、GC、连接池等指标,找出最先到达瓶颈的资源或组件。

场景设计上,我选择了三条具有代表性的链路:

  1. 网关直通链路:客户端请求 → SpringCloud Gateway → 后端单服务,这测试的是网关本身的基础转发能力。
  2. 服务间调用链路:客户端请求 → Gateway → 服务A → OpenFeign → 服务B → 数据库,这是微服务最典型的调用场景,重点看服务间调用的开销和数据库连接池的容量。
  3. 限流降级链路:客户端请求 → Gateway → 服务 → Sentinel,验证限流规则在压力下是否能正常触发,以及降级兜底逻辑是否正确。

这三条链路分开压,不要混在一起,否则出问题了你很难定位是哪一段掉了链子。

1.2 压测环境准备

压测环境我建议单独部署,尽量不要复用开发或测试环境。原因很简单:开发环境有其他人在联调,数据随时在变,压测结果根本不可信。

这次用的是三台独立机器:

  • 压测机(JMeter):8核16G,跑JMeter主控和分布式压测节点。
  • 应用节点:8核16G,部署SpringCloud脚手架相关服务。
  • 数据库节点:4核8G,部署MySQL。

服务部署方式用的是Docker Compose,脚手架里本身带了编排文件,起服务很方便。数据库单独放在了物理机上,避免容器化带来的性能干扰。

注意:压测机和被测机的网络延迟要控制在1ms以内,最好在同一个内网网段。跨公网压测的结果几乎没有任何参考价值,因为网络延迟会完全淹没应用的性能差异。

2. JMeter脚本设计要点

2.1 线程组参数怎么定

线程组是压测的入口,参数设置直接决定了压测的形态。我这次用的是JMeter 5.x,几个关键参数分享下:

并发数(线程数):不要一上来就跑1000并发。我习惯用阶梯加压的方式,先50、再100、200、500,每个梯级跑5分钟,观察TPS和RT的变化趋势。这样能清楚看到系统在哪个并发点开始出现性能拐点。

Ramp-Up Period:意思是多长秒内把线程全部启动。比如100个线程,Ramp-Up设10秒,就是每秒启动10个线程。我建议Ramp-Up时间设置为并发数除以每秒期望启动的线程数,一般控制在5到15秒。

循环次数:勾选"永远",然后用Scheduler的持续时间来控制压测时长。这样比固定循环次数更精准,也好统计数据。单轮压测时长建议不少于10分钟,太短了数据不稳定,尤其是GC抖动和第二轮热点代码编译(JIT)的影响还没完全稳定。

思考时间(Think Time):压测要区分两种模式:并发能力测试和容量测试。这次的目的是验证系统容量,所以不加思考时间,让请求像洪水一样连续打过去,测的是系统的极限处理能力。

2.2 用梯度压测替代固定并发

我这次没有用一个固定并发跑到底,而是用了一个简单的阶梯递增脚本,模拟真实业务量慢慢涨上来的过程:

# 阶梯线程组配置示例(通过JMeter插件或用多个线程组串联) # Thread Group 1: 50并发,持续2分钟 # Thread Group 2: 100并发,持续2分钟 # Thread Group 3: 200并发,持续2分钟 # Thread Group 4: 400并发,持续2分钟 # Thread Group 5: 600并发,持续5分钟

这样的好处是,你能直接看到TPS和RT的曲线在哪个并发点开始陡变,而不是等到压测结束去聚合报告里看平均数。平均数会掩盖很多问题——比如前50并发表现很好、后500并发崩溃,平均一下看起来好像还能用,但实际系统已经不可用了。

提示:如果想用JMeter原生功能实现梯度压测,可以用多个线程组并列、设置不同的启动延迟来实现,效果跟插件差不多,还能避免额外插件的兼容问题。

2.3 断言、监听器和数据采样

HTTP请求里我加了两层断言:

  • 响应状态码断言:必须返回200。
  • 响应内容断言:检查响应体里是否有预期的业务标识字段。

这样能确认不只是网络通,业务逻辑也正常处理了。

监听器我用了三种配合:

  1. 聚合报告(Aggregate Report):看整体TPS、平均RT、错误率、p90/p99。
  2. 用简单数据写入器(Simple Data Writer):把每次请求的响应时间落盘,后面用脚本做分位数分析。
  3. 后端监听器(Backend Listener):配合InfluxDB+Grafana展示实时曲线。这个强烈推荐,压测过程中看着实时TPS和RT曲线变红变绿,比事后看报告直觉得多。

关于采样数据,JMeter默认在聚合报告里只统计样本总数、平均数和百分比,但压测的时候我更关心的是p99的走势。建议在简单数据写入器里把"Successes"和"Failures"分开保存,方便后续分析错误请求的分布。

3. 压测执行与监控体系搭建

3.1 压测前的预热和基线

正式压测前,我先跑了一轮小流量预热,大概100并发持续5分钟。目的是让JIT编译热点代码、让Spring容器和数据库连接池都处于正常工作状态。如果不预热,刚开始跑的前几百个请求里,很多类和方法还在解释执行,响应时间会偏大,数据会误导人。

预热完,我记录了一组基线数据,作为后续压测的参照。

基线数据包括:

  • 单次请求的平均RT(经过网关 → 服务A → 服务B → MySQL,全链路)
  • TPS基线
  • 数据库连接池当前活跃连接数
  • 各服务pod的CPU和内存占用

有了基线,后面跑任何一轮压测,都有对比的锚点。

3.2 压测过程中要盯哪些监控指标

压测不是启动JMeter就完事了,真正的技术活在于压测过程中能不能眼观六路,在系统还没完全崩溃前捕捉到瓶颈的信号。

我这次同时开了三块监控屏:

应用监控(通过SpringBoot Actuator暴露指标)

  • Heap内存使用量
  • GC频率和耗时(重点看Full GC,如果Full GC频率超过了每分钟一次,基本说明堆太小或内存泄漏)
  • 活跃线程数(Tomcat线程池/Netty事件线程)
  • 数据库连接池活跃连接数和等待获取连接的线程数

系统监控

  • CPU使用率(用户态和系统态分开看)
  • 磁盘IO(如果有大量日志写入,磁盘IO也可能变成瓶颈)
  • 网络带宽和TCP重传率

链路追踪

脚手架里带了Sleuth+Zipkin,压测的时候我采样了一部分请求,看各节点耗时分布。这个非常有用,能直接告诉你时间到底花在哪个环节——是网关转发耗时高,还是OpenFeign调用对端服务慢,还是SQL慢查询。

强烈建议:压测时把JVM的GC日志打开,并记录日志到文件。分析结果时,如果TPS波动很大,多半能在GC日志里找到重大停顿(Stop-The-World)的证据。

3.3 分布式压测的选择

单台JMeter压测机在压到较高并发(比如800以上)时,JMeter本身可能成为瓶颈,因为要维护成千上万个线程的状态和网络连接。

我这次的压测目标是600并发以内,单台压测机其实够用了,但我还是准备了JMeter分布式压测的预案,以防模拟量需要继续往上加。

分布式压测配置要点:

  • 一台主控机(Master),两台执行机(Worker/Slave)
  • 主控机只负责下发脚本和收集结果,不加压
  • 所有执行机的时间要同步,否则聚合报告的时间戳对不上
  • CSV数据文件如果用到的话,要分发给所有执行机,不能只在主控机上

配置方式:

# 在每台执行机上启动jmeter-server jmeter-server -Djava.rmi.server.hostname=<执行机IP> # 在主控机上指定远程主机列表 jmeter -n -t test_plan.jmx -R <执行机1IP>,<执行机2IP> -l result.jtl

注意:分布式压测的输出结果要统一汇总到主控机,聚合分析用汇总后的JTL文件。另外,用远程主机跑的时候,每台机器上的JMeter版本要完全一致,不然可能会有兼容性报错。

4. 压测结果分析与调优实录

4.1 第一轮压测:网关直通链路的体感

先跑网关直通链路,脚本很简单:一个HTTP请求打到网关,网关直接路由到后端服务的一个健康检查接口(这个接口查了一次缓存,返回很快)。

线程组设置:50、100、200、400、600五个梯级,每梯级持续3分钟。

我预期网关应该能扛住很高的并发,毕竟只是转发,不涉复杂业务逻辑。但实际数据让我有点意外:

并发数TPS平均RT(ms)p99(ms)错误率
504801042100%
1006201613050%
2007102825200.1%
4007805139860.8%
60076079016202.3%

注意几个信号:

第一,200并发往上,TPS增长非常缓慢,400并发到600并发TPS基本原地踏步,这是典型的瓶颈已现的表现。 第二,平均RT在400并发时已经超过500ms,对业务来说已经偏慢。 第三,错误率从400并发开始出现,600并发时明显升高,说明有连接超时或线程池拒绝的情况。

顺着这个信号,我看了网关所在Pod的监控。

发现:Netty事件循环线程的占用率接近100%,同时线程池的等待队列持续堆积。也就是说,网关本身成了第一个瓶颈,而不是后端服务。

进一步分析,SpringCloud Gateway默认基于Netty,事件循环线程数是CPU核数的两倍。服务器8核,事件线程16个,单链路压测时,所有请求都在走路由转发的逻辑,Netty处理能力被压到了极限。

另外一个发现是:路由断言的正则匹配也吃了不少CPU。脚手架里测试环境的网关配置了十几条路由,每条路由都有断言表达式,Gateway在转发前会逐条进行模式匹配,规则多了之后开销不可小觑。

4.2 网关调优:线程模型和路由配置

针对网关瓶颈,我做了几个调整:

调大Netty事件循环线程数。这个参数在application.yml里配置:

spring: reactor: netty: event-loop-count: 32

改成32后,重新压测,同等600并发下TPS从760提升到了920,RT也有下降。

但这里要提醒一下:事件循环线程数不是越大越好。每个事件循环线程绑一个Selector,线程之间会有上下文切换开销。一般建议是CPU核数的2到4倍,具体需要实测调整。我这边8核机器配32是偏激进的,实际生产环境建议从16开始试。

精简路由断言。把网关路由里低优先级的正则表达式改成精确匹配或者Path前缀匹配,减少匹配开销。比如:

# 原配置(正则匹配,开销较大) - id: xxx-service uri: lb://xxx-service predicates: - Path=/api/xxx/** # 调优后(PrefixPath + 精确匹配,减少正则计算) - id: xxx-service uri: lb://xxx-service predicates: - Method=GET,POST

不过说实话,路由匹配的优化对整体吞吐的提升幅度有限,主要收益还是在事件循环线程数的调整上。路由优化更大的价值在于降低单请求的CPU消耗,让同一台机器能支撑更多实例。

4.3 第二轮压测:服务间调用链路的数据库瓶颈

网关直通链路调完后,我开始压第二条链路:客户端 → Gateway → 服务A → 服务B → MySQL。

这条链路加了真实的业务逻辑:从MySQL查一批订单数据,做一些聚合计算后返回。查询走的是索引,单次SQL耗时大约2到5ms。

结果如下:

并发数TPS平均RT(ms)p99(ms)错误率
502202273900%
1003153175800%
20034058811050.3%
400310129026003.8%

这次的问题更明显:TPS在200并发后就上不去了,RT却还在涨,错误率也上来了。这是典型的某一下游组件容量满的表现。

按经验,先看数据库。

数据库侧监控显示:MySQL的CPU使用率接近100%,连接数持续飙高,大量线程处于"Waiting for table metadata lock"或"executing"状态。慢查询日志里出现了好几条之前没见过的慢SQL。

这里有个很常见的坑:脚手架里的数据库连接池默认配置是maximum-pool-size: 10,对单体应用可能够用,但微服务场景下,多个服务实例共享同一个数据库,每个实例都有自己的连接池,叠加起来很容易打爆数据库的连接上限。

我当时数了一下:服务A部署了3个实例,每个实例的HikariCP连接池最大10,服务B部署了3个实例,也是10,加上其他服务,一共几十个连接同时打到数据库。数据库默认的max_connections是151,很容易就被占满了。

优化方案有两步:

第一步,把服务A和服务B的maximum-pool-size从10调整到合适的值。怎么算?参考PGBouncer的公式:连接数 = ((核心数 * 2) + 有效磁盘数)。对普通SSD,一般给到每个实例5到8就够,我最后每个实例配了6。

spring: datasource: hikari: maximum-pool-size: 6 minimum-idle: 2 connection-timeout: 30000 idle-timeout: 600000

第二步,分析慢SQL。

慢查询里有几条很典型:一个ORDER BY create_time DESC LIMIT 10的查询,因为create_time没建索引,走了全表扫描。还有一个关联查询,join时驱动表的过滤字段没索引。

这两个在压测前其实就有,只是没人发现。压测的高并发让它们现了原形。

给create_time和关联字段加了索引后,这两条SQL的耗时从几百毫秒降到了几毫秒,数据库的CPU负载明显下降。

提示:压测前的数据准备也很关键。我这次测试库里的订单表只有2万多条数据,压测时加了索引,一切正常。但如果数据量到几百万条,索引策略和SQL行为可能完全不同。建议在准备环境时,把数据量往真实环境靠,不然容易误判SQL的真实性能。

4.4 JVM参数和线程池的调整记录

第三次优化,我把注意力转向了应用侧的JVM参数。

第一轮压测时,服务A的堆内存默认配置是-Xmx1g,压到400并发时,GC日志显示Full GC出现了好几次,每次停顿都在300ms以上,RT曲线有明显锯齿状波动。

分析下来,堆太小只是表象,更核心的问题是新老年代的分配比例不合理。服务A的逻辑是:请求进来 → 查库 → 组装JSON → 返回,大量短生命周期对象在Young区被创建和回收。默认的-XX:NewRatio=2表示老年代是年轻代的2倍,但在这个场景下年轻代偏小,对象过早晋升到老年代,导致老年代快速填满,频繁触发Full GC。

调整后的参数:

-Xms2g -Xmx2g -XX:NewRatio=1 -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=2 -XX:+UseG1GC -XX:MaxGCPauseMillis=200

核心思路:

  • 堆从1g加到2g,给老年代留更多空间。
  • NewRatio=1让年轻代和老年代大小相当,给短生命周期对象更大空间。
  • MaxTenuringThreshold=2让对象在年轻代经历两次GC后如果还活着就晋升,减少无意义的复制。
  • 用G1替代默认的ParallelGC,毕竟微服务实例一般不需要追求极端吞吐,更在意响应时间的稳定性。

调整后,同一并发下,Full GC频率从每分钟3次降到了基本不出现,RT曲线平稳了很多。

还有一个线程池的坑也值得一提。服务A里的一个线程池(用来异步处理某个上报任务)初始配置是:

ThreadPoolExecutor( 2, // corePoolSize 10, // maxPoolSize 60, // keepAliveSeconds new LinkedBlockingQueue<>(100) )

压测时发现,当并发请求打到300以上时,这个线程池的任务队列持续堆积,导致异步任务的完成时间从几百毫秒拖到几秒,反过来增加了请求的RT(因为主流程里有一步在等待异步任务的结果)。

调整思路:把corePoolSize设为8,队列容量降到20,最大线程数设为20,并用CallerRunsPolicy作为拒绝策略。这样在高并发下,任务不会无限堆积在队列里,而是让提交任务的线程自己执行,起到了天然背压的作用。

ThreadPoolExecutor( 8, // corePoolSize 20, // maxPoolSize 60, // keepAliveSeconds new ArrayBlockingQueue<>(20), // 队列改小 new ThreadPoolExecutor.CallerRunsPolicy() )

4.5 Sentinel限流和降级验证

最后验证的是限流降级链路。Sentinel在脚手架里已经集成了,我配置了一条针对服务A某个接口的限流规则:QPS超过300时,触发限流,返回一个降级提示。

压测时直接把并发拉高,看Sentinel的表现:

并发数通过QPS被限流QPS降级响应体
100980无
3002991无
500300200降级提示
800300500降级提示

Sentinel的限流表现很精准,超过阈值后,超出的请求全部走了降级逻辑,没有出现击穿的情况。这说明脚手架里Sentinel的基础配置是生效的。

但这里有个细节值得注意:Sentinel默认的规则是内存态,也就是服务重启后规则会丢失。如果真的要做生产环境的限流保护,最好把规则推到Nacos配置中心,用推模式下发,而不是直接在控制台上临时添加规则。这也是很多团队在用脚手架时容易忽略的点。

5. 常见问题与排查技巧实录

5.1 压测过程中遇到的典型问题

这次压测前后,踩了一些坑,整理出来给各位参考。

问题一:压测结果忽高忽低,TPS像过山车

排查过程:

  • 首先排除网络因素,内网ping延迟正常。
  • 想到可能是JIT编译波动,但预热后仍然不稳。
  • 看GC日志,发现Full GC频繁,平均每1到2分钟一次。
  • 用jmap -dump抓了堆快照,分析发现有个ConcurrentHashMap存了不该常驻的中间结果数据,导致老年代持续增长。

解决:修正了代码逻辑,把缓存对象改成合适的数据结构,并且给缓存设了过期时间。

这个问题的通用排查思路是:TPS波动大 → 先看GC → 再快照堆分析 → 定位是内存泄漏还是大对象。不要上来就调参,要找到根因。

问题二:压测时连接池大量"Connection is not available"报错

这个算是微服务压测的经典问题。报错信息:HikariPool-1 - Connection is not available, request timed out after 30000ms。

原因分析:

  • 数据库的最大连接数限制(max_connections=151)导致连接分配失败。
  • 各服务实例的连接池配置没有基于实际容量规划,叠加后超过数据库上限。
  • 慢SQL长时间占用连接,让情况雪上加霜。

解决步骤:

  1. 降低各服务实例的maximum-pool-size到合理值。
  2. 给慢SQL加索引,缩短单条SQL占用连接的时间。
  3. 数据库侧适当调高max_connections,同时评估是否需要连接复用中间件(如ProxySQL)。

问题三:JMeter自身成为瓶颈

当压到600并发时,JMeter的CPU已经接近100%,TPS曲线不稳定,结果失真。

识别方法:查看压测机CPU,如果JMeter所在进程CPU超过80%,基本就是压测机瓶颈。

解决:改用分布式压测,把负载分散到多台机器,或者降低每个线程组的线程数,增加线程组数量(不建议,会引入线程管理和调度的额外开销)。

5.2 压测结果数据可信度判断

压测结束,面对一堆聚合数据,要有基本的判断力。以下几个信号,只要出现一个,我就认为这轮压测的结果不可用:

  • 错误率超过1%(稳定性测试场景下超过0.1%就要警惕)。
  • 压测机的CPU超过80%,说明压测脚本本身限制了目标系统的压力。
  • 数据库连接池出现大量等待获取连接的日志,说明数据本身是在异常状态下跑出来的。
  • 被测服务的某个实例发生过重启(OOM或健康检查失败),这轮数据基本作废。

压测报告里,我习惯把p99、TP99.9和平均RT一起列出来,只给平均值的报告,在生产环境排障时几乎没什么用。平均值是"感觉",p99才是"真相"。比如平均RT是120ms,听着很健康,但p99是1.2s,说明有1%的用户体验已经明显卡顿了。

6. 脚手架性能调优的整体复盘

三轮压测做完,整个脚手架的调优动作汇总成了一张表:

优化项调整前调整后效果(同等并发下TPS提升)
网关Netty事件线程数1632约20%
数据库连接池10/实例6/实例数据库连接不再成为瓶颈
慢SQL索引无已添加单SQL耗时下降90%以上
JVM堆和GC参数-Xmx1g + ParallelGC-Xmx2g + G1Full GC频率显著降低,RT稳定
异步线程池参数队列100 + max10队列20 + max20 + CallerRuns高并发下异步任务延迟可控

这些优化不是一次完成的,而是压测→分析→调整→再压测的循环中逐步迭代的。每一轮压测都有一个明确的验证目标,改动也尽量控制在单一维度,避免多个变量叠加导致无法判断是谁起的决定性作用。

调优后,三条链路的最终数据:

  • 网关直通链路:在600并发下,TPS稳定在910,p99在380ms以内,错误率0%。
  • 服务间调用链路:在400并发下,TPS稳定在380,p99在620ms以内,错误率0.1%以内。
  • 限流降级链路:限流规则精准生效,降级逻辑正常,未出现服务雪崩迹象。

这个数据虽然谈不上极致,但对于微服务脚手架的基础能力来说,已经达到了可上线使用的标准。剩下更高吞吐的追求,需要在架构层面(比如缓存、异步化、读写分离)做更大手笔的改造,那就已经超出了这次压测的范畴。

从压测这件事本身来说,它从来不只是把JMeter跑起来看看数字,而是通过数据反向审视整个系统的设计合理性。这次压测让我对脚手架里每个组件的配置参数心里有了底,也摸清了哪些地方将来业务量增长时得优先扩容。如果你也在打磨微服务脚手架,强烈建议按同样节奏跑一轮压测,很多潜在问题早暴露永远比晚暴露好。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询