做压测这些年,我见过太多团队把“并发测试”和“持续性压测”当成一回事。排期表上写着“压测”,跑几轮脚本,拿一份聚合报告,看一眼TPS和平均响应时间,就宣布性能验收通过。结果系统上线后扛住了高峰流量,却在某个不起眼的凌晨突然OOM,或者连接池被打满,整个服务不可用。问题恰恰出在这里:你验证的是系统“短时间内的并发处理能力”,却没有验证它“连续高压运行时的稳定性”。JMeter这两个场景都能覆盖,但很多人只用了它一半的功力。这篇文章我会把并发测试和持续性压测分开讲明白,包括目标差异、脚本设计、参数设定、监控方案、报告解读和排坑经验,给正在规划压测方案、准备写压测脚本或者被线上性能问题折磨的测试和开发同学一套可以直接落地的参考。
1. 并发测试与持续性压测,两者到底在验证什么
1.1 并发测试:找拐点,验容量
并发测试的核心是回答一个问题:系统在给定并发用户数下,能不能扛住,响应是不是够快,有没有报错。
我通常把并发测试分成两类。一类是验证型,比如产品方说“我们要支持1000人在线”,那你就压1000个并发,看平均响应时间、P99、错误率、吞吐量是否达标。另一类是探索型,不知道系统极限在哪,于是从50并发、100并发、200并发逐步往上加,观察TPS和响应时间的变化曲线,找到系统的性能拐点。
拐点这个概念很关键。当并发数增加,吞吐量还能线性增长、响应时间保持平稳,说明系统还有余量;当并发数继续增加,吞吐量突然不再增长甚至下降,响应时间开始陡增,错误率开始出现,这个位置就是系统的瓶颈点。有人管它叫“膝盖”。找到这个膝盖,你才知道线上扩容的阈值在哪里,也能知道告警应该设到多少。
做这种测试时,JMeter的线程组就是并发模型的核心。一个线程模拟一个虚拟用户,Ramp-up时间决定这些用户是瞬间全部涌进来还是慢慢进场。两者的效果完全不同:瞬间涌进来测的是系统对突发流量的反应,慢慢进场测的是系统在负载爬坡过程中的表现。
1.2 持续性压测:抓慢病,漏不过的才是真问题
持续压测和并发测试最大的区别,在于时间维度。
并发测试跑几分钟、最多半小时,验证的是系统“短时间内的爆发力”。持续压测通常要跑几个小时甚至几天,验证的是系统的“耐久度”。很多问题不会在短时间压测里暴露,比如内存泄漏、线程池回收异常、数据库连接被缓慢耗尽、文件句柄没有释放、定时任务堆积、缓存命中率逐渐下降。这些问题有一个共同特点:它们都是“慢病”,需要时间积累才会显现。
我见过一个真实例子,有个系统单次请求大约泄漏几十KB内存,短压完全看不出来。但持续压测跑到24小时以后,JVM堆曲线的锯齿形状开始明显上移,Full GC频率越来越高,最后在36小时左右出现频繁的Full GC,TPS跌到只有峰值的一半。这种问题如果只做并发测试,永远发现不了。
用一个类比来理解:并发测试是百米冲刺,持续压测是马拉松。百米成绩好的人,马拉松不一定能完赛;系统能扛住瞬时高峰,不代表能稳定运行一周。
1.3 标准组合打法:先并发探顶,再持续验证
实际项目里,我建议把两者串成一个完整流程,而不是二选一。
第一步,先做并发测试。目的是快速摸清系统的容量上限、找到性能拐点、确认各项指标是否满足业务目标。第二步,根据并发测试的结果确定持续压测的负载水平。一般取峰值的70%到80%,比如并发测试发现500并发是拐点,那持续压测就跑350到400并发。跑低了你压不出问题,跑高了系统直接崩,你分不清是稳定性问题还是负载本身超出了能力。
第三步,持续压测过程中持续观察服务端指标。JVM堆内存、GC、数据库连接数、线程池状态、磁盘IO这些数据,配合TPS和响应时间趋势,才能判断系统是否在“缓慢恶化”。第四步,压测结束后再做一轮短时间的并发复核,确认系统在衰竭后还能不能恢复、有没有留下脏数据影响后续运行。
这套组合打下来,覆盖的已经不是单个技术点了,而是一个完整的性能验证闭环。
2. 压测目标设定与JMeter基础配置
2.1 目标参数怎么定:从业务指标反推
不少同学拿到压测任务,第一句话问我:“并发设多少?”我通常会反问:“你的业务指标是什么?”没有业务指标的压测,结果只能自己看,评审会上谁都能质疑你。
业务指标一般给的是日活、访问量和高峰时段。比如日均UV是10万,每人平均浏览20次,其中80%的流量集中在4个小时的高峰期。那高峰期的总请求数大约是10万乘以20乘以0.8,也就是160万次。4小时等于14400秒,平均TPS约111。如果要求系统能扛住3倍峰值,那目标TPS就是333左右。再根据平均响应时间反过来推需要的并发数,公式是:并发数约等于TPS乘以平均响应时间。
假设平均响应时间是200毫秒,那目标并发大约是333乘以0.2,约67。别被这个数吓到,它说明一个道理:高并发不是目的,TPS才是。压测之前,把目标TPS、目标响应时间P99、允许的错误率这三个值写在文档第一页,后面所有脚本和分析都围着这三个数转。
2.2 压测环境的几个硬性要求
很多人压测结果不可信,不是脚本写得差,是环境没准备好。
最重要的一点是压测环境必须尽量贴近生产。硬件配置、网络拓扑、数据库数据量、缓存策略都要和生产保持一致。哪怕做不到完全一致,也要明确记录差异,不然压测数据没参考意义。比如生产环境数据库有千万级数据,压测环境只有几万条,SQL执行计划完全不同,压出来的接口响应时间可能比生产快一个数量级。
数据要按生产比例构造。登录接口要有足够数量的账号,订单接口要有足够的商品和用户,查询接口要有足够的历史数据。很多人压测到一半发现CSV文件循环到头了,脚本报错,又回头补数据,浪费时间还容易污染结果。我的习惯是压测前专门写一个造数脚本,跑一遍把基础数据铺好。
还有一条容易忽略的:压测机和被测机器之间最好在同一个机房或同一个VPC网段,减少网络延迟对结果的干扰。如果跨地域压测,响应时间里的网络RTT就占了很大比例,压出来的数据既不真实又难排查。
2.3 JMeter运行模式和JVM参数
JMeter用起来很顺手,但很多人从第一步就埋了坑:用GUI模式跑正式压测。
GUI模式启动大量线程模拟高并发时,JMeter自身为了刷新界面图表会消耗大量CPU和内存。你本来打算压2000并发,结果压测机先把自己的线程卡死了,产生的结果根本不准。记住一个原则:调试脚本用GUI,正式压测一律用命令行模式。
jmeter -n -t /path/to/load-test.jmx -l /path/to/result.jtl -e -o /path/to/report -Jthreads=300 -Jduration=1800解释一下参数:-n表示非GUI模式,-t指定测试计划文件,-l保存原始结果到jtl文件,-e -o是压测结束后生成HTML报告。后面两个-J参数是自定义属性,对应脚本里的${__P(threads)}和${__P(duration)},这样同一个脚本可以在不同环境通过命令行覆盖参数,不用反复改jmx文件。
正式压测前,还要调JMeter的JVM堆内存。默认配置比较保守,高并发下容易报OutOfMemoryError。在jmeter.sh或者bin目录下新建setenv.sh,写上:
export JVM_ARGS="-Xms2g -Xmx4g -XX:MaxMetaspaceSize=512m"堆内存大小取决于你的场景复杂度和预估并发量,一般从2G开始,最大不建议超过物理内存的一半。因为JMeter本身还要留内存给连接池和采样结果缓冲。如果你压到单机极限还上不去,不要盲目调大内存,考虑上分布式压测更靠谱。
2.4 什么时候该上分布式
JMeter单机的能力并没有一些人想象的那么弱。4核8G的机器,脚本简洁、断言少、不大量记录响应体,压2000到3000个并发线程是没问题的。真的碰上单机加不上去的情况,再考虑分布式。
分布式压测的模式是:一台Master负责调度和汇总,多台Slave实际发请求。需要同步几个细节:所有Slave的JMeter版本要一致,测试脚本和参数化文件要同步到每台机器,结果最后汇总在Master一份jtl里。可以通过本地文件同步或配置管理工具分发,千万别手动一台台拷,版本漂移了压测结果对不齐,排查起来非常痛苦。
我在项目里遇到过分布式压测某个节点导数据慢导致整体拖尾的情况。排查时发现那台机器的磁盘IO比别的机器差很多,JMeter写日志文件的速度跟不上采样频率,产生了堆积。后来所有节点统一SSD,问题解决。分布式不是银弹,单机先跑、压实了再扩,才是正确路径。
3. 并发测试场景设计与脚本实操
3.1 线程组的参数设置
线程组的参数看起来简单,实际上每个字段都会影响压测曲线形态。
线程数就是虚拟用户数。Ramp-up Period是启动全部线程的耗时,我建议不要全部设为0。如果你的目标是模拟突发事件,比如大促秒杀,那可以集中拉升;如果是常规容量评估,建议阶梯式加压。比如200个并发,Ramp-up设置成60秒,每3秒新增10个用户,系统压力是慢慢上来的,你能在结果里观察到TPS随并发增长的整个过程,判断拐点更准。
循环次数如果是有限值,整个脚本跑到结束就停了。持续压测时勾选“永远”循环,然后用调度器(Scheduler)里的Duration控制时长,比如跑3600秒。这样比固定循环次数更精确,因为循环次数没法直接换算成时间,不同接口响应速度不一样,每轮循环耗时差很多。
新版本JMeter的线程组里还能设置Spike测试模式,让所有线程同一时刻启动。做突发流量冲击时这个功能非常有用。常规测试中偶尔用一次可以,别当成默认配置。
3.2 HTTP请求与超时设置
压测脚本里HTTP请求的配置,细节决定成败。
首先是超时时间。很多JMeter默认的Connect Timeout和Response Timeout是空值,代表无限制。高压下服务端处理不过来,大量请求挂在那里排队,压测机端的线程也得不到释放,继续堆积请求,最后压测机自己先崩了,或者压出来的响应时间虚高得离谱。我的习惯是Connect Timeout设置为1000到2000毫秒,Response Timeout从3000毫秒起步,再根据接口耗时特性调整。
其次是KeepAlive。默认JMeter的HTTP请求会复用连接,这符合大部分生产环境的行为。注意如果服务端并发连接数限制很低,KeepAlive长时间占用连接,会导致其他请求连接超时。这种问题在压测中非常常见,排查时把连接复用的开关关掉再压一轮,对比两种模式的指标,就能看出连接池配置是否合理。
再次是HTTP请求默认值。测试计划里如果有多个接口,一定要加一个HTTP Request Defaults,统一配置协议、域名、端口。后面每新增一个请求,只需要写路径和参数,不用一遍遍复制主机名,避免脚本维护时改一处漏一处。
3.3 参数化:CSV、函数与动态数据
性能测试最忌讳所有请求发同样的数据。一旦服务端有缓存,你测的是缓存命中率,不是真实处理能力。而且很多业务有唯一性约束,比如订单号、手机号、身份证,重复数据直接报错。
最常用的方案是CSV Data Set Config。在测试计划里加一个CSV配置元件,指定一个数据文件路径,设置好变量名和分隔符,线程组里的请求就可以引用这些变量。
比较隐蔽的坑在“遇到文件末尾EOF时”这个选项。默认是循环读取,意味着数据会重复使用。如果业务对数据唯一性要求很高,比如压测注册接口,循环读同一个手机号必然冲突。我的建议是构造足够大的数据集,保证压测过程中不会走到EOF,或者接受循环但把业务逻辑设计成可重复操作的,比如只有查询没有写入。
JMeter还提供内置函数做动态数据,非常方便。比如时间戳、随机数和UUID:
${__time(yyyyMMddHHmmss,)} ${__Random(100000,999999,randNum)} ${__UUID()}在BeanShell或者JSR223采样器里也可以用Java和Groovy做更复杂的数据生成。这里要提醒一句:JSR223里的代码能复用就去掉重复定义,每个采样器的脚本编译是有开销的。能用一个JSR223做批量数据处理逻辑,就别拆成五个。
3.4 断言和关联怎么写
脚本里没有断言的压测,结果很危险。你压出来的TPS可能很好看,实际接口全在返回错误码,错误率也许不高,但业务成功率是0。
核心接口必须加断言。最基础的是响应断言,检查HTTP状态码是不是200,响应体是否包含成功标识。再进一步用JSON断言,直接提取JSON字段判断业务码,比如code字段是否为0。逻辑复杂的可以用JSR223断言,写Groovy做多条件校验。
关联主要用于需要身份令牌或者依赖上一步返回值的场景。比如登录接口返回一个token,后续所有请求都要带上。JMeter用正则表达式提取器或JSON提取器来提取上一个请求的响应,通过变量传给下一个请求。这里注意作用域,提取器要放在对应的请求子节点下,引用变量时要在后续请求的路径、参数或HTTP头管理器里使用。
调试脚本阶段,加一个“查看结果树”监听器,逐个请求确认响应内容。确认没问题后,正式压测时一定移除这个监听器,它在高压下会保留大量响应数据,内存占用非常恐怖。这个习惯能帮你省掉很多JMeter自身的内存问题。
4. 持续性压测的进阶设计与监控
4.1 持续压测的脚本设计差异
持续压测不是把并发测试脚本的运行时间改长就完事。时间长意味着风险面完全不同。
首先是负载模式。持续压测通常采用平稳负载,保持一个相对稳定的并发数或TPS持续运行,整个过程中偶尔在固定时间点叠加一小段高峰负载,模拟定时任务或批处理对系统的影响。设计时用阶梯加压到目标负载水平,然后进入平台期。
其次是数据清理。并发测试跑完就结束,数据脏了就脏了。持续压测跑几个小时,数据库里可能积累大量测试数据。这些数据会影响后续查询的SQL执行计划、索引效率、甚至触发磁盘空间告警。需要在场景里设计数据维护机制,比如每完成一定轮次,通过脚本清理一部分测试数据,或者干脆在JSR223里写数据轮转逻辑。不然你压到第三天,系统慢的根因可能是数据膨胀,而不是代码质量。
再次是日志文件。JMeter的jtl结果文件会随着压测时间持续增大,一个多小时就能写满几个G。压测前检查压测机磁盘剩余空间,估算一下jtl文件增长速率,设置结果监听器的采样保存策略,可以只记录错误采样或按周期采样,减少数据量。实在要保留完整数据,考虑挂独立数据盘。
4.2 服务端资源与JVM监控
只看JMeter自己的结果做持续压测,等于盲人摸象。你必须时刻监控服务端的健康状态。
最简单的方案是开两个终端,一个用top或htop盯CPU和内存,一个用sar收集磁盘和网络的累计数据。这些能帮你判断服务端资源的整体水位,但看不到JVM内部情况。GC和堆内存的变化,必须用jstat或者可视化工具来看。
我常用的组合是Prometheus加node_exporter采集机器指标,JMeter通过Backend Listener把压测数据发到InfluxDB,最后统一在Grafana上看。这套全开源的组合好处是,TPS、响应时间、CPU、内存、IO在同一个时间轴上对齐,定位问题时不用来回切工具。
对Java服务来说,jstat看GC曲线非常关键:
jstat -gcutil <pid> 1000 120每秒钟打印一次,持续两分钟,重点看Full GC的开启时间点和频率。持续压测过程中如果发现Full GC出现得越来越勤、每次耗时越来越长,堆内存曲线整体抬升,基本可以判断有内存泄漏或对象分配异常。再配合heap dump分析,就能定位具体哪类对象堆积。
数据库监控也不能漏。连接数是否缓慢增长不回落、慢查询数量是否随时间上升、锁等待时间有没有变长,这些都是持续压测里常出的问题。
4.3 长时间运行的特殊问题
持续压测跑到一定阶段,会出现一些短时间压测永远不会遇到的奇怪问题。
定时任务干扰是最典型的。很多系统有整点任务、凌晨汇总任务。你持续压测跑着,突然在某个整点,TPS掉了一大截,错误率抬头。别慌,先看服务端日志,很可能就是定时任务占用了大量CPU或数据库资源。这种干扰本身是重要发现,说明线上同一时段也存在这类问题,需要业务层面做隔离或错峰。
时间戳边界问题也很隐蔽。脚本里如果用了固定格式的时间戳参数,长时间运行可能会跨过日期边界,业务逻辑对跨天数据校验失败,产生持续报错。压测时长超过24小时的话,脚本里的时间处理逻辑一定要自测几轮跨天场景。
还有一个常被忽略的是服务端连接数的“泄漏式增长”。短时间压测看不出,长时间压测会发现连接数缓慢增长,压测结束后也不回落。这通常意味着应用层在某种异常场景下没有正确释放资源。这种问题靠JMeter的线程数或TPS往往看不出来,必须依赖服务端连接数监控。
5. 结果分析、瓶颈定位与排坑手册
5.1 聚合报告的关键指标解读
压测跑完拿到聚合报告,先不要急着截图发周报。每一列数字背后的意义得先弄明白。
Samples是总请求数,Throughput是每秒吞吐量,Error%是错误率。这三个是基础,但不是重点。重点在响应时间那一组:Average是平均值,Median是中位数,90%Line、95%Line、99%Line是分位数。
平均值很容易骗人。举一个例子:100个请求里99个响应时间是100毫秒,1个是10秒,平均值约199毫秒,看着完全正常。但真实用户体验是1%的请求卡了10秒。所以评估响应时间,至少要盯P95和P99,这两个值才反映大多数用户感受到的实际体验和尾部延迟。
对比P50和P99的关系也能判断问题。如果P50平稳、P99持续抬高,说明少数请求在变慢。常见原因是GC停顿、连接池排队或者某台节点异常。如果所有分位数一起涨,那就是整体资源饱和,系统已经到容量瓶颈了。
再看错误率的分布。如果错误不是从压测一开始就出现,而是跑到某个时间点才集中爆发,那大概率是某个资源耗尽或者服务端状态异常,去翻服务端日志和监控曲线,往往能看到连接数打满、堆内存告急之类的对应事件。
5.2 瓶颈定位组合拳
压测最怕的不是有问题,是有问题了不知道怎么定位。我的经验是结合资源指标组合判断。
| 现象组合 | 可能瓶颈 | 排查方向 |
|---|---|---|
| CPU高,TPS低,RT高 | 应用代码效率低、锁竞争、GC频繁 | 火焰图、线程dump、GC日志 |
| CPU低,TPS低,RT高 | 等待IO、数据库慢SQL、远程调用卡顿 | 慢查询日志、依赖接口耗时分析 |
| 内存持续攀升,Full GC频繁 | 内存泄漏、大对象分配过多 | 堆dump、对象统计、代码走查 |
| 磁盘IO占用高 | 日志刷盘、数据读写量大 | IO top、日志策略、索引优化 |
| 线程数持续增长不回落 | 线程池泄漏、连接未释放 | 线程dump、连接池监控 |
这套组合拳比单看一个指标要靠谱得多。比如TPS低,如果CPU也低,基本可以排除计算密集问题,往IO和外部依赖方向查。如果CPU已经打满,就要看是业务代码问题还是GC占用过高。
还有一个建议:压测过程中发现问题,别急着停压测,先保留现场。让负载继续压着,这时候采集线程dump、堆dump,中间的各种错乱状态更容易抓住。停了压测,很多现场就消失了,排查难度会瞬间加大。用jstack保存线程快照,jmap导出堆快照,然后分析。这些现场信息加在一起,往往能直接定位到问题代码。
5.3 常见问题排查速查表
长期做压测,踩坑记录会越来越厚。我整理了一份高频问题速查表,每次压测前过一遍,能省不少时间。
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
| JMeter报OutOfMemoryError | JVM堆太小、监听器太多、结果数据量过大 | 调大堆内存,精简监听器,CLI模式压测 |
| 压测机CPU被打满,TPS上不去 | 压测机自身性能瓶颈 | 换更稳定机型,或转为分布式压测 |
| 大量ConnectTimeout | 服务端连接数已满或队列积压 | 增大服务端连接队列,优化连接释放逻辑 |
| 压测中TPS周期性下跌 | 服务端定时任务或GC周期 | 对齐GC日志、查看定时任务执行时间 |
| 长时间压测后TPS缓慢下滑 | 内存泄漏、连接泄漏、数据膨胀 | 查堆、连接数、表数据量趋势 |
| 断言失败但业务响应正常 | 断言条件写错或提取变量失败 | 用结果树单请求调试,检查提取器表达式 |
| 分布式压测部分节点无结果 | 脚本或数据未同步 | 核查各节点脚本版本、CSV文件一致性 |
| 压测数据重复触发唯一约束 | CSV数据量不够或EOF循环设置不当 | 扩大数据集,或调整数据生成逻辑 |
这些坑大多数不是一次性踩完的。每次压测前,花十分钟把速查表过一遍,然后对照你的场景挨个检查脚本、环境、监控,能避免大量返工。
我个人这几年最大的体会是,压测这件事,方案设计占比比脚本编写重要得多。很多人一上来就开JMeter,连业务目标都没有,数据构造和监控方案都没想好,压完一堆数据留着自己都解释不了。真正靠谱的做法是反过来的:先定业务目标,再推性能指标,然后设计场景,最后才动手写脚本。把并发测试和持续性压测当成一套组合拳来打,短时间找拐点,长时间验稳定,才是JMeter正确的打开方式。
最后分享一个小技巧:压测报告里除了放聚合指标,我习惯把服务端的资源曲线截图放在一起,每条指标都标上观测时间点。这样评审会上别人问“这个数怎么来的”,你能直接翻出同一时刻的服务端状态,有理有据,比单贴一张JMeter截图可信得多。压测不是跑个脚本那么简单,你能不能把问题说清楚,才是这个岗位真正的价值。