SpringCloud微服务脚手架JMeter压测实战与调优全记录
SpringCloud微服务开发脚手架跑了一次完整的JMeter压测从脚本设计到结果分析踩了不少坑这篇文章把整个流程、关键参数和调优思路都整理出来给准备做微服务性能验证的同学一份能直接参考的实战记录。先说背景。我手上的这个脚手架是某项目团队内部沉淀的一套SpringCloud基础框架集成了网关、注册中心、配置中心、OpenFeign、Sentinel限流等常用组件业务方新服务基本都是基于这套脚手架来开发。此前只验证过功能和联调从没做过系统性的压力测试大家对它的容量上限、性能瓶颈和配置是否合理其实心里没底。所以这次压测的目标很明确验证脚手架在典型业务场景下的吞吐能力定位潜在瓶颈给出可落地的调优建议。1. 测试目标与场景设计1.1 先想清楚压什么再动手压很多人一上手就是建线程组、配并发数、跑聚合报告但微服务的压测和单体应用有一个很大的区别请求链路长、组件多如果不先明确测试目标和业务场景压出来的数据很难指导优化。我这次把测试目标拆成了三个层级功能验证级确认脚手架各组件在压力下不报错、不超时接口返回数据正确。容量评估级找到系统在可接受的响应时间比如RT p99 500ms下的最大吞吐量TPS。瓶颈定位级在压测过程中同步监控各节点的CPU、内存、GC、连接池等指标找出最先到达瓶颈的资源或组件。场景设计上我选择了三条具有代表性的链路网关直通链路客户端请求 → SpringCloud Gateway → 后端单服务这测试的是网关本身的基础转发能力。服务间调用链路客户端请求 → Gateway → 服务A → OpenFeign → 服务B → 数据库这是微服务最典型的调用场景重点看服务间调用的开销和数据库连接池的容量。限流降级链路客户端请求 → Gateway → 服务 → Sentinel验证限流规则在压力下是否能正常触发以及降级兜底逻辑是否正确。这三条链路分开压不要混在一起否则出问题了你很难定位是哪一段掉了链子。1.2 压测环境准备压测环境我建议单独部署尽量不要复用开发或测试环境。原因很简单开发环境有其他人在联调数据随时在变压测结果根本不可信。这次用的是三台独立机器压测机JMeter8核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。响应内容断言检查响应体里是否有预期的业务标识字段。这样能确认不只是网络通业务逻辑也正常处理了。监听器我用了三种配合聚合报告Aggregate Report看整体TPS、平均RT、错误率、p90/p99。用简单数据写入器Simple Data Writer把每次请求的响应时间落盘后面用脚本做分位数分析。后端监听器Backend Listener配合InfluxDBGrafana展示实时曲线。这个强烈推荐压测过程中看着实时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重传率链路追踪脚手架里带了SleuthZipkin压测的时候我采样了一部分请求看各节点耗时分布。这个非常有用能直接告诉你时间到底花在哪个环节——是网关转发耗时高还是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提升到了920RT也有下降。但这里要提醒一下事件循环线程数不是越大越好。每个事件循环线程绑一个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: - MethodGET,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:NewRatio2表示老年代是年轻代的2倍但在这个场景下年轻代偏小对象过早晋升到老年代导致老年代快速填满频繁触发Full GC。调整后的参数-Xms2g -Xmx2g -XX:NewRatio1 -XX:SurvivorRatio8 -XX:MaxTenuringThreshold2 -XX:UseG1GC -XX:MaxGCPauseMillis200核心思路堆从1g加到2g给老年代留更多空间。NewRatio1让年轻代和老年代大小相当给短生命周期对象更大空间。MaxTenuringThreshold2让对象在年轻代经历两次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_connections151导致连接分配失败。各服务实例的连接池配置没有基于实际容量规划叠加后超过数据库上限。慢SQL长时间占用连接让情况雪上加霜。解决步骤降低各服务实例的maximum-pool-size到合理值。给慢SQL加索引缩短单条SQL占用连接的时间。数据库侧适当调高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稳定在910p99在380ms以内错误率0%。服务间调用链路在400并发下TPS稳定在380p99在620ms以内错误率0.1%以内。限流降级链路限流规则精准生效降级逻辑正常未出现服务雪崩迹象。这个数据虽然谈不上极致但对于微服务脚手架的基础能力来说已经达到了可上线使用的标准。剩下更高吞吐的追求需要在架构层面比如缓存、异步化、读写分离做更大手笔的改造那就已经超出了这次压测的范畴。从压测这件事本身来说它从来不只是把JMeter跑起来看看数字而是通过数据反向审视整个系统的设计合理性。这次压测让我对脚手架里每个组件的配置参数心里有了底也摸清了哪些地方将来业务量增长时得优先扩容。如果你也在打磨微服务脚手架强烈建议按同样节奏跑一轮压测很多潜在问题早暴露永远比晚暴露好。