先说一个我实际踩过的坑。去年给某线上活动做压测,同事交接了一份现成的JMeter脚本,线程数拉到500,TPS却卡在900左右上不去,服务端CPU利用率只有十几个百分点,怎么看都是压测机自己先撑不住了。后来打开JMeter线程Dump,发现大量线程堵塞在正则提取和响应断言的处理逻辑上,才意识到一个关键问题:性能测试里,脚本本身执行效率低下,完全可能让压测机先于被测系统成为瓶颈。
性能测试脚本优化这件事,很多团队是忽视的。大家关注并发数、TPS、响应时间、服务端监控,却很少人认真审视JMeter脚本里那些“看不见的开销”。但实际上,同样的目标系统、同样的压测机,脚本优化前后的差距可以非常夸张。我前面提到的那个项目,在关闭无关监听器、替换提取器、调整参数化方式、重置JVM参数之后,单机TPS从900多提升到了5200左右,刚好接近5倍的提升。这篇文章就把我在JMeter脚本优化方面用过的招数整理出来,覆盖监听器、断言、参数化、关联提取、并发模型、JVM与操作系统调优,以及一整套常见问题排查思路。不管你是刚入门没多久的测试工程师,还是已经写过上百个JMeter脚本的老手,后边的内容应该都有可以直接抄作业的地方。
1. 压测性能的两端:脚本就是被测系统的一部分
1.1 一个容易被忽略的事实:JMeter线程做完多少事,决定了你能压多高的负载
很多人理解压测链路,脑子里只有“JMeter发请求 -> 服务端返回”这么简单。但实际上,JMeter内部在一个虚拟用户(一个线程)发出请求之前和之后,还要做大量额外工作。以最基础的脚本为例,一次请求的生命周期包括:读取参数化数据、执行前置处理器、构造HTTP请求、连接管理、发送请求、读取响应、执行后置处理器(提取器)、执行断言、写入监听器结果、判断循环条件……这些步骤每一步都要消耗CPU、内存和IO。
打个比方,压测就像去餐厅评估厨师的出餐能力。你派了一群顾客(虚拟用户)去点菜,但服务员(JMeter)每接一单都要先绕着餐厅跑三圈、填五张表单、跟后厨反复确认,最后顾客还没吃上菜,服务员自己已经累趴了。这时候你怪后厨出餐慢,其实搞错了对象。
所以优化脚本的第一步,是把“JMeter内部处理开销”当成一个独立的性能瓶颈来看。别总觉得“任务加给服务端才算压力测试”,如果JMeter每秒钟自己能处理的最大请求数只有2000,那你就算把线程数调到一万,打到服务端的请求最多也就是2000左右,TPS曲线会早早出现平台期,而且CPU全烧在了压测机自己身上。
1.2 我惯用的“三看”定位法:先判断瓶颈到底卡在哪一侧
接手一个慢脚本,我不会一上来就闷头优化,而是先花十来分钟判断瓶颈到底出在压测机、网络还是服务端。方向错了,后面的优化全是白费。
第一看:看压测进程的CPU和GC。在压测机上执行top -Hp <jmeter_pid>或jstat -gcutil <jmeter_pid> 1000,如果JMeter进程CPU已经逼近100%,或者Full GC频繁触发,那么大概率脚本本身的开销太大,虚拟用户的线程已经被JMeter内部的活拖住了。第二看:看服务端利用率。通过监控平台查看被测服务的CPU、内存、连接数、队列长度等指标,如果服务端利用率普遍不高,但TPS也不高,说明瓶颈根本不在服务端。第三看:看JMeter线程状态。执行jstack <jmeter_pid>抓一份线程快照,搜索线程堆栈里出现频率最高的方法名。如果大量线程卡在正则表达式的匹配逻辑、等待监听器写入结果、或者阻塞在CSV文件读取上,那就非常明确地指向脚本结构问题。
这套“三看”跑完之后,基本就能锁定优化方向。操作上有一个细节,线程Dump不要只抓一次,而是在压测运行过程中每隔几秒抓一份,多抓几份才能看到线程状态的分布趋势。单份快照有时候只是巧合,三份以上才能看出稳定的共性。
2. 给脚本做减法:哪些组件正在偷偷吃掉你的性能
2.1 监听器:压测过程中最该关掉的“显示器”
如果让我只给一条优化建议,那就是:压测期间,把测试计划里所有的“查看结果树”和“聚合报告”监听器全部禁用,尤其是这两个一起用的情况,性能损耗惨重。
原因在于监听器的工作方式。JMeter每完成一个取样器请求,都会把结果对象推送给所有挂载的监听器。查看结果树会把每个请求的URL、状态、响应体、请求体全量保存到内存中,跑上几分钟就可能累积几十万个响应体,内存直接被打满。聚合报告虽然没有把所有响应体都存在内存里,但它需要实时计算每个取样器的统计值,一线程一刷新,在高并发下也会吃掉不少CPU。GUI模式下再加上界面绘制刷新,压测机就变成了“画图机器”,根本没力气继续发请求。
那我压测时想知道情况怎么办?两个方案。一是完全不用监听器,压测结束后用生成的jtl结果文件离线生成Dashboard报告,命令是jmeter -g result.jtl -o report_dir,该有的指标一条不少。二是如果非要实时看,用命令行模式跑压测,配合后端监听器把指标写入外部时序数据库,再在Grafana上看曲线。但是注意,后端监听器本身也是一种监听器,它同样会对每个样板做一次异步上报,该省的时候还得省。
还有个常被人忽略的细节:结果文件写入的字段选项。默认的jtl格式会保存一堆用不上的信息,其中响应数据(response_data)和请求头字段写进日志,会让结果文件体积爆炸式增长,磁盘IO成为新的瓶颈。我通常会在jmeter.properties里把用不到的字段关掉,至少把jmeter.save.saveservice.response_data=false、jmeter.save.saveservice.request_headers=false这些选项配好。压测结束后如果需要调试单条失败请求,再临时打开字段针对性抓包,而不是全程全量保存。
2.2 断言要谨慎:每多一次匹配就多消耗一次CPU
响应断言是另一个常见的性能杀手。很多人出于“确保压测请求有效”的好心,在每个请求后面挂了一个响应断言,默认匹配整个响应体。一旦接口返回的JSON体有几百KB,这个断言在每次请求时都要把响应体转成字符串做模式匹配,性能开销直接翻倍都不是稀奇事。
这里要明确一个定位问题:性能压测不是单元测试,压测脚本的核心目标是持续制造稳定负载,而不是逐条校验业务正确性。正确率校验可以放到压测之外去解决,比如通过服务端日志统计、通过业务监控平台观察错误数。如果希望对每个请求做存活校验,尽量用开销最小的断言方式。按开销从低到高排:状态码断言 < 响应头断言 < 响应体Contains匹配 < 响应体Equals全量匹配。能断言状态码就断状态码,能Contains关键字就不要Equals整个Body。
还有一个更彻底的做法,在压测脚本里完全不使用断言,只使用少量“探针用户”带断言统计成功率。具体操作是把线程组分两类:一类主力压测线程组不带任何断言,负责施加负载;另一类小比例验证线程组带简单断言,通过JMeter的变量区分或者专用监听器统计错误率。这样既能掌握大概的正确性,又不拖累整体压测性能。我在实际项目中压大多数纯性能验证场景都是这个策略。
2.3 后置处理器与作用域:提取器不是越多越好
关联提取器是JMeter脚本里又一个大头。很多脚本一上来就是好几个正则表达式提取器,每个请求的响应体都做一遍正则匹配,正则引擎本身就是CPU密集型的活,碰到大响应体直接雪崩。
如果确实需要做关联提取,组件选型有讲究。常用的几类提取器性能差别很明显,越靠前的优先级越高:
| 提取器类型 | 适用场景 | 性能特征 |
|---|---|---|
| 边界提取器(Boundary Extractor) | 响应体结构简单、目标值附近有明确边界 | 最快,纯字符串定位 |
| JSON Extractor | 响应体是JSON | 较快,基于Jayway解析,但每次都要反序列化 |
| 正则表达式提取器 | 各类文本提取 | 最慢,正则引擎CPU消耗高 |
| XPath Extractor | XML响应 | 慢,还要额外维护解析器 |
我现在的习惯是优先用边界提取器。比如响应里有"token":"abcd1234"这种固定结构,边界提取器把左边界写成"token":",右边界写成",性能比正则快得多。只有在边界不稳定、需要正则语法兜底的时候才考虑正则提取器。
另一个非常容易被忽视的是作用域问题。JMeter的提取器一旦放在某个父级Sampler下面,默认会对父作用域内所有子Sampler执行。比如一个循环控制器下面挂了两个HTTP请求,提取器放在循环控制器层级,那每个HTTP请求跑完都会去执行一次提取逻辑,白白浪费了一半的提取开销。正确做法是把提取器放在真正需要它的那一个Sampler子节点下面,而且尽量用-1下标只取第一个匹配结果,避免正则引擎从头到尾把整个响应体扫完。还有一类更极限的做法是:token、session这类可以在压测启动前批量拿到的数据,就不要在压测脚本里“边压边拿”了,往下翻你会看到具体的预制方案。
3. 参数化和时间控制:少一点随机,多一点确定
3.1 CSV参数化比Random函数高效在哪
许多脚本喜欢用${__Random(1,100000)}、${__time()}这类函数直接在请求里拼参数,看着方便,实际上每次请求执行都要动态计算函数值。单看一次请求,这个开销微乎其微,但在500并发、每秒几千请求的场景下,函数展开的CPU累计开销相当可观。更尴尬的是,有的请求URL里同时嵌着五六个Random函数,每次都要做函数表达式解析、随机数生成、字符串拼接,压测机的一部分CPU就在这种无意义的工作中烧掉了。
用CSV Data Set Config做参数化,本质上是在脚本启动时一次性把数据文件读入内存,后续请求按行取用,比每次动态生成随机数便宜得多。更关键的是,CSV里的数据是提前准备好的,可控性更强。压测时我们经常需要构造一批合法的用户ID、订单号、手机号,这些数据很难靠随机函数生成出业务上合法的值。提前用造数脚本批量生成一批数据写到CSV,压测脚本直接读,效率和数据质量都更好。
不过CSV Data Set Config也有需要注意的坑。默认的“共享模式”是所有线程共享同一个文件指针,高并发下线程之间会抢文件读取的锁,反而产生额外开销。我常用的做法有两种:一是把数据文件按线程组拆分成多份,每个线程组挂独立的CSV配置,互不干扰;二是如果数据量实在太大,就在启动压测前用JSR223预处理把数据加载到一个全局Map里,脚本运行时按索引取用,这个方案性能最优,但实现复杂度稍高。大多数项目用方案一就足够了,简单、稳定、可控。
3.2 思考时间的正确用法:别让Timer毁掉你的TPS
模拟用户思考时间时,很多人会在每个Sampler下挂一个“固定定时器”,结果整个脚本被定时器串成了一个缓慢的串行链路。注意定时器的作用域规则:定时器作用于同一作用域内的所有Sampler,如果每个子请求都挂一个Timer,那每个请求执行前后都要多等一轮,TPS直接被拉低一大截。
那怎么正确模拟思考时间?我的建议是:如果压测的目标是探知系统最大处理能力,那就不加思考时间,让请求连续打;如果目标更接近真实用户行为,则只在“一个用户完整业务流程的某几个关键节点”加定时器,而不是为每一个HTTP请求都加。比如浏览商品列表、查看详情这类快速顺序操作之间不需要思考时间,但在“下单确认”这种高权重操作之前加一个300~800毫秒的固定延迟就足够了,既模拟了真实用户节奏,又不会把压测机线程白白挂在定时器上。
另外有一个很容易被误用的组件叫“常量吞吐量定时器(Constant Throughput Timer)”。它通过动态计算每个线程的延迟来实现目标吞吐量,本身就会额外占用CPU做定时计算,而且在多线程环境下,它的计算逻辑并不是精确的,会带来较大的抖动。想要限制RPS的时候,我宁愿用“外层控制层”的方式:把整个压测脚本的线程数、循环次数、运行时间控制好,让服务端自己表现出真实处理能力,而不是用定时器去人为“截流”。限流这种事,交给专业的压测平台或者客户端负载生成器去做,别把负担塞进脚本里。
3.3 高并发下的随机性与锁竞争:为什么有时候越“随机”越慢
除了__Random函数本身的计算成本,高并发下还存在一个隐藏问题:当你让大量线程同时执行随机函数或共享同一个随机种子时,JVM内部的随机数生成可能涉及锁竞争。Java的Math.random()在并发场景下会用CAS保证线程安全,CAS失败就得重试,重试越多开销越大。JMeter内部的函数实现未必直接用Math.random(),但这提醒我们一个原则:高并发压测时,任何“共享的可变状态”都可能成为性能瓶颈。
类似的还有共享的Cookie Manager、共享的HTTP Cache Manager以及共享的CSV文件指针。在这些组件上,JMeter需要额外的线程同步,线程越多竞争越激烈。所以我通常建议:每个线程组尽量有自己独立的配置元件副本;如果数据不敏感,宁可复制几份也不要所有线程共享一个。这个原则几乎可以推广到所有JMeter配置元件。
4. 并发模型与脚本结构:不要让线程组拖后腿
4.1 先算并发数,再开线程,别上来就500
JMeter里一个虚拟用户就是一个Java线程。线程数越大,JVM的线程调度、上下文切换、栈内存消耗就越大。很多新手有个错觉:线程数越大压得越狠,服务端就会暴露问题。但实际上线程数过高的第一个受害者往往是压测机自己。线程调度一频繁,TPS反而下降,响应时间反而拉高,测出来的数据全是假的。
合理的启动方式应该先算一笔账。一个简单估算公式是:并发数 ≈ 目标TPS × 平均响应时间(秒)。例如目标TPS是2000,服务端平均响应时间是200ms,那大致需要的并发数是 2000 × 0.2 = 400。这个公式是Little‘s law的简化版,用来粗估够用了,不过要记住响应时间会随着负载上升而变长,所以真正压测时要按梯度从低到高逐步加压,而不是一步到位。
我实际执行的时候,通常会先跑100并发5分钟,观察TPS和服务端指标;没问题再翻倍到200、400,每档跑5~10分钟,关注TPS曲线是否还能线性增长。如果加到400时TPS不再上升,而服务端CPU还有余量,这时候才需要怀疑是不是脚本或压测机瓶颈。一上来就填500不一定能测出更深的问题,反而会让排查无从下手。
4.2 业务场景拆分:一个线程组装不下所有真实行为
一个常见的不合理设计是:把所有业务接口按照真实用户操作顺序,全部塞进同一个线程组里,用循环控制器包裹。这样做的问题在于,JMeter线程组内的Sampler是顺序执行的,与真实用户不同业务的并发行为完全不匹配。真实场景里,有的用户在登录,有的用户在浏览,有的在下单,这些动作是同时发生的,不是每个用户按部就班把所有动作串完。
更合理的结构是按业务动作拆成多个线程组,每个线程组只负责一种或一类动作,线程组之间并行运行。比如A线程组专门跑登录接口,B线程组跑列表查询,C线程组跑下单操作。不同线程组可以通过自己的线程数和循环次数来表达业务占比,比如登录只占5%就开5个线程,下单占30%就开30个线程。如果希望更精确地控制占比,还可以用“吞吐量控制器(Throughput Controller)”,在一个线程组里按百分比分配请求类型。但注意不要为了这个功能把控制器嵌套好几层,嵌套越深,每个请求要计算的路径判断越多,性能损失也随之增加。
另外还要提醒一件事:如果压测脚本里包含登录后获取token的流程,而这个流程是为后续业务接口服务的,最好不要让每个线程从头登录一次。正确做法是先在压测启动前用一个小脚本批量登录,把token保存到CSV,压测脚本直接读取CSV;需要做会话保持的接口用HTTP Cookie Manager管理。这样做能砍掉大量与目标业务无关的登录开销,让更多线程资源投入到真正要评估的接口上去。
4.3 HTTPS与连接复用:细节决定压测机能跑多远
前面提到的都是脚本结构层面,再往外扩一步就是网络层。JMeter的每个HTTP Sampler都涉及TCP连接管理,如果脚本配置不当,每个请求都新建一条TCP连接,握手开销会吃掉大量压测机资源。尤其是HTTPS接口,一次完整的TLS握手需要多次加密计算和往返,开销远高于普通HTTP。
最基本也最有效的配置是勾选HTTP请求里的“KeepAlive”选项,让JMeter复用已建立的连接。对于HTTPS,还可以在JMeter属性里启用SSL会话缓存,减少TLS握手次数。具体参数是https.use.cached.ssl.context=true,配合JDK的SSL会话缓存机制,在高并发HTTPS压测中能明显降低开销。
压测机和目标机的网络环境也要注意。能压内网IP就压内网IP,不要走公网域名;公网压测时DNS解析延迟和网络抖动会让结果极不稳定,排障也困难。如果必须用域名,可以提前把目标IP写到压测机的hosts文件里,省掉DNS交互的耗时。压测机上TCP连接过多导致的TIME_WAIT堆积问题也很常见,表现为压测跑到中段突然大量连接失败,这时候需要适当调整本地端口范围和TIME_WAIT回收参数。不过调整系统内核参数前要确认环境是否适合,现代Linux内核下有些参数改了反而可能有副作用,建议先观察ss -s看TCP状态分布再决定。
5. 压测机资源调优:JVM、运行模式与网络
5.1 JVM参数:默认堆只是为了让你跑通Demo
很多人用JMeter压测,直接双击jmeter.bat或者运行jmeter命令就开始了,完全忽略了JVM本身的配置。JMeter默认的堆内存设置非常保守,对于几百并发、长时间压测来说远远不够。堆内存不够最直接的后果就是频繁触发Full GC,而Full GC发生时会暂停整个JVM,所有虚拟用户线程都会被冻住,TPS曲线就会出现周期性的“掉零”现象。
我常用的JVM参数配置长这样:
export HEAP="-Xms4g -Xmx4g -XX:MaxMetaspaceSize=1g" export GC_ALGO="-XX:+UseG1GC -XX:MaxGCPauseMillis=100"在JMeter的bin/setenv.sh里加上这些变量,或者直接用JVM_ARGS传入。有几个细节值得说清楚。第一,-Xms和-Xmx最好设为相同的值,避免运行中动态扩容带来的性能抖动。第二,堆内存也不是越大越好,堆设得过大,GC每次扫描的时间就会变长。我一般根据压测机物理内存来定,单实例压测机给4GB到8GB堆比较合适,内存更大的机器可以多开几个JMeter实例分摊负载,而不是把单个实例的堆无脑加到32GB。第三,压测期间要开启GC日志,不然出了问题复盘都不知道原因:
export VERBOSE_GC="-Xlog:gc*:gc.log:time,uptime,level -Xlog:gc*"跑完压测后打开gc.log文件,重点看Full GC的次数和耗时。如果Full GC频繁,优先检查是不是结果日志保存的响应数据太大,把堆空间塞满了;或者是不是响应体被大规模存到了内存里。
5.2 命令行模式才是压测的正确姿势
这条建议值得单独强调:长时间压测,一定用命令行模式,不要用GUI模式。GUI模式需要实时渲染界面、刷新图表、维护线程状态和界面的同步,这些操作本身占用大量CPU和内存。再加上如果在GUI里开着查看结果树,性能损失更大,基本等于让压测机一边干重活一边录屏。
命令行模式的标准用法是:
jmeter -n -t test.jmx -l result.jtl -e -o report_dir参数解释:-n表示非GUI模式,-t指定脚本,-l指定结果文件,-e表示压测结束后生成HTML报告,-o指定报告输出目录。跑完后直接打开report_dir里的index.html,聚合报告、响应时间分布、吞吐量曲线全都现成的。
这里还有一个细节:命令行模式虽然不吃UI资源,但结果文件result.jtl的写入仍然会消耗磁盘IO。如果结果文件每行都包含巨大的响应数据,磁盘IO会成为新的瓶颈。所以前面提到的保存字段裁剪,在命令行模式下同样重要。另外,如果压测目标是短时间极限试探,可以只保留必要的结果字段,甚至压测期间完全不落盘,结束后再补跑一轮带记录的压力验证,都是可行的做法,全看对数据的要求有多高。
5.3 压测机系统层参数:端口、网络与内核
压测机系统层的调优很多人完全没碰过,但影响其实很大。最常见的是TCP端口耗尽问题。JMeter发起大量短连接时,系统会在TIME_WAIT状态保留大量本地端口,默认的本地端口范围可能不够用,导致新连接无端口可用。压测机上的相关调整参数示例如下:
sysctl -w net.ipv4.ip_local_port_range="1024 65535" sysctl -w net.ipv4.tcp_fin_timeout=15 sysctl -w net.ipv4.tcp_tw_reuse=1需要说明的是,tcp_tw_reuse在不同内核版本和网络环境下的行为有差异,调整前最好先查阅当前系统的内核文档,或者先在低并发环境下验证;我的原则是够用就行,不要为了追求“极致性能”乱动内核参数。调整完可以通过ss -s观察TIME_WAIT数量是否明显下降。
还有文件描述符限制,高并发下JMeter需要打开大量socket和文件,默认的ulimit -n如果只有1024,线程数一高连接就会失败。压测机上执行ulimit -n 65535或者写入/etc/security/limits.conf,属于最基本的准备工作。压测机CPU型号和核数也直接决定了JMeter能榨出多少吞吐,一个4核的压测机无论怎么优化,单机也很难把高并发请求推到特别高,这时候就需要考虑分布式压测方案,也就是用多台压测机组成集群,每台分担一部分线程。但分布式压测又会引入调度器本身的性能问题,能用单机扛住优先用单机,实在不够再扩展。
6. 实战中常见的问题和排查思路
6.1 压测机CPU很高,但服务端很闲:先怀疑脚本,再怀疑压测机
这个组合是我见过最多的。第一反应不是加机器,而是检查脚本。先用top -Hp <jmeter_pid>看看JMeter进程内部线程的CPU占用分布,如果有个别线程长期占满CPU,再用jstack抓线程堆栈,定位到具体的方法名。如果堆栈里频繁出现正则表达式、监听器、断言相关的类名,那基本就是脚本组件选型的问题。我之前有一个项目就是这么发现问题的:几百个线程全部堵在一个正则表达式的匹配逻辑里,那个正则是用来从超大响应体里提取一个字段的,换了边界提取器之后,压测机CPU直接从95%降到了40%,TPS翻了一倍多。
还有一种情况是压测脚本用了大量${__Random}或${__UUID}函数,堆栈里可以看到函数求值的调用链。这种问题的排查方向就是参数化改造,把动态函数换成CSV预置数据,前面第3章已经详细说过。
6.2 TPS上不去,但压测机CPU也没跑满:可能卡在等待上
如果压测机CPU不高,TPS还是上不去,就要看向“等待”。用jstack抓线程状态,看到大量线程处于WAITING或者TIMED_WAITING,常见原因有两类。一是定时器太多,线程都在定时器上睡大觉;检查脚本里是否有多个Timer,按第3章的建议精简。二是结果日志写入太慢,线程在等IO写完成;这种时候去看压测机的磁盘监控,磁盘使用率经常能接近100%。我的处理办法是调整结果保存字段,或者把结果文件写到独立的磁盘上,不要和系统盘抢IO。如果线程是阻塞在远程调用上,比如从数据库或Redis取数做参数化,那就要把这个远程数据源从压测链路里摘除,压测数据应该提前准备好,而不是边压测边查。
还有一种相对隐蔽的情况:线程卡在 “DNS解析” 上。如果压测脚本用了公网域名且本地没有缓存,每次请求都要做DNS查询,遇到DNS服务器响应慢就直接拖垮TPS。解决方法是把域名和IP的映射写进压测机的 /etc/hosts,或者直接压IP。
6.3 TPS周期性掉零,过几秒又恢复:优先查Full GC
这种“锯齿形”的TPS曲线非常典型,中间深谷基本就是JVM在做Full GC。压测机上执行jstat -gcutil <jmeter_pid> 1000,观察FGC列的数字跳变。如果发现Full GC频率持续上升,再看GC日志,确认是不是堆空间不足。处理路径通常是:先砍掉脚本里所有吃内存的组件(监听器、保存响应数据、缓存大响应体),然后合理调大JVM堆,最后仍然频繁GC就考虑给压测机加内存或部署多实例分担。
顺便说一个容易忽略的内存大头:JMeter的HTTP缓存管理器。如果脚本里开启了对响应内容的缓存,并且接口返回体特别大,缓存会占掉大量堆内存。压测场景如果不需要测试缓存命中率,这个管理器可以不挂。类似的还有各种“结果树”监听器,它们在内存里保存的响应数据占比最夸张,要求全量保存尤其危险。
6.4 常见问题速查表
| 现象 | 可能原因 | 优先排查手段 | 推荐处理方案 |
|---|---|---|---|
| 压测机CPU高、服务端空闲 | 脚本组件开销太大 | top + jstack看堆栈 | 精简监听器/断言/提取器,换高性能组件 |
| TPS上不去、CPU也不高 | 定时器太多 / IO阻塞 / 远程数据源 | jstack看等待状态 | 精简Timer,裁剪结果字段,数据预制 |
| TPS锯齿形掉零 | Full GC频繁 | jstat -gcutil + gc.log | 关内存大户,调JVM堆,换G1收集器 |
| 压测中途连接大量失败 | 本地端口耗尽 | ss -s看TIME_WAIT | 调端口范围,开KeepAlive,减少短连接 |
| 登录流程占了大量时间 | 每个线程重复登录 | 查看压测日志请求占比 | 预生成token,压测脚本直接读取 |
| 断言失败请求很多 | 超时或响应慢导致 | 对比服务端日志 | 区分断言失败与业务真实错误,优化断言策略 |
这个速查表基本覆盖了我这些年遇到的大部分压测脚本问题。排障的时候记住一个原则:先看压测机自身,再看服务端,别一上来就怀疑目标系统。性能测试的前提是负载生成器本身可靠,如果脚本和压测机成了不可控变量,后面所有数据都站不住脚。
最后再分享一点个人经验。以前我优化脚本,总想着“能不能再压高一点”,后来发现更重要的其实是“脚本能不能稳定跑完一整轮压测”。每次正式压测前,我喜欢先跑一轮小并发、短时长的热身验证,专门观察JMeter进程的CPU和GC表现,确认脚本自身没有明显拖后腿,再切换到正式负载档位。这套流程看起来多花了几分钟,实际上帮我省掉了无数个“压测压到一半发现脚本有问题,全部推倒重来”的深夜。脚本优化这件事没有一劳永逸,不同业务场景的答案都不一样,但只要每次压测前都认真审视脚本本身的开销,你就不太可能被“服务端没跑满、压测机先罢工”这种最尴尬的问题绊住脚。