1. 从赛题说起:Web性能测试到底在考什么
全国大学生软件测试大赛里的Web性能测试赛项,跟平时做功能测试完全是两码事。功能测试关心的是"点下去有没有反应、数据对不对",性能测试关心的是"一千个人同时点下去,系统还能不能稳住"。这两个问题的技术栈、工具链、思维方式差得很远。很多同学第一次拿到赛题,看到"对某网站首页进行并发压测,要求TPS不低于XX、平均响应时间低于XX毫秒"这种描述,第一反应是打开JMeter点几下就跑,结果跑出来的数据要么波动巨大,要么根本达不到题目要求的量级,报告写出来也是一堆没有说服力的截图。
我在带队伍和复盘赛题的过程中发现,Web性能测试赛项的难点从来不是"会不会用工具",而是"能不能把一段模糊的业务描述,翻译成一套可执行、可复现、可解释的压测方案"。这个转化过程包含了需求拆解、指标建模、脚本开发、场景设计、结果分析五个环节,每个环节都有坑。这次就围绕这个实例(一)的题型,把我自己踩过的坑、验证过的做法,一段一段拆开讲清楚。不管你是第一次接触性能测试,还是已经会跑几个脚本但总是拿不到高分,这篇内容都能直接拿来对照操作。
简单说,这篇适合三类人:准备参赛但不知道从哪下手的在校生、学过JMeter但没做过完整压测项目的转行者、以及需要把性能测试流程梳理一遍的测试从业者。我会尽量用大白话解释原理,再配上可直接复制的配置和脚本,让你看完就能动手跑一遍。
2. 工具选型与环境准备:别让环境毁掉整场压测
2.1 压测工具怎么选,为什么我优先用JMeter
性能测试的工具不少,常见的有JMeter、LoadRunner、Locust、Gatling、k6这几类。赛场上时间有限,工具选型的第一原则不是"哪个最强",而是"哪个能让我在最短时间内把方案跑通、把数据讲明白"。我的习惯是优先用JMeter,理由很实在。
第一,它是纯Java实现,跨平台,装个JDK就能跑,Windows和Linux都不折腾。第二,图形界面和命令行两种模式都有,调脚本时用GUI,真正施压时用非GUI命令行模式,避免GUI本身消耗资源影响结果。第三,插件生态成熟,尤其是jpgc系列插件里的阶梯线程组(Stepping Thread Group)和并发线程组(Concurrency Thread Group),做阶梯加压非常顺手。第四,报告输出方便,命令行加-e -o参数直接生成HTML报告,赛题要求的图表基本都有。
LoadRunner功能确实强,但商业授权和安装体积是硬伤,学生环境下不划算。Locust用Python写脚本很灵活,但需要自己写代码组织场景,对新手不够友好,而且分布式部署的文档相对零散。Gatling适合做持续压测和CI集成,学习曲线偏陡。综合下来,JMeter在这个场景下性价比最高。
注意:JMeter的GUI模式只用来调试脚本,正式压测一定要切到命令行模式。GUI在压测时会实时绘制图形,这个渲染过程本身会吃掉大量CPU和内存,导致压测机先扛不住,测出来的数据完全不可信。
2.2 压测机与被测系统的资源隔离
这是新手最容易忽略的一点。很多人把JMeter和被测应用装在同一台机器上,一边压一边看,结果压测机CPU跑满了,被测系统却没怎么吃力,数据一看全是压测机的瓶颈。正确做法是至少分两台机器:一台跑JMeter(压测机),一台跑被测应用(被测机)。如果条件实在有限,也要保证压测机的CPU和内存有足够余量,压测过程中持续观察压测机的资源占用,一般建议压测机CPU使用率不要超过70%。
对于赛题里那种已经部署好的在线系统,你无法控制被测机,能做的是保证自己的压测机干净。关掉不必要的后台程序,尤其是浏览器、下载任务、云盘同步这类会占网络和磁盘的软件。我用过的一次教训是,压测过程中系统在后台自动更新,网络抖动直接把一组本该稳定的数据搅成了锯齿状,复盘时才发现问题出在自己这边。
2.3 JDK版本与JMeter安装的几个细节
JMeter 5.5以上版本建议搭配JDK 8或JDK 11,JDK 17虽然也能跑,但个别老插件会有兼容问题。安装步骤本身不复杂,解压后配置环境变量即可,这里说几个容易翻车的点。
一是JAVA_HOME不要指向JRE目录,要指向JDK目录,否则启动时会报找不到编译器之类的错误。二是JMeter的bin目录下要保证有可执行权限,Linux下记得chmod +x jmeter。三是中文乱码问题,修改bin/jmeter.properties里的sampleresult.default.encoding=UTF-8,同时在jmeter.bat或jmeter.sh里加上-Dfile.encoding=UTF-8,这样响应数据和报告里的中文才正常。
内存配置也要调。默认的堆内存偏小,压测线程一多就容易OOM。修改bin/jmeter脚本里的HEAP参数:
# Linux 下编辑 jmeter 启动脚本 HEAP="-Xms2g -Xmx4g -XX:MaxMetaspaceSize=512m"这个值根据压测机实际内存调整,一般压测机8G内存以上,给JMeter分4G比较稳妥。压测线程数上千时,还要考虑每线程栈内存,可以通过-Xss参数微调,但更推荐的做法是改用分布式压测,而不是在一台机器上硬堆线程。
3. 需求拆解与性能指标建模:把题目翻译成人话
3.1 从赛题描述里抠出真正的测试目标
赛题通常会给一段业务描述,比如"模拟500名用户登录系统并查询订单列表,持续运行10分钟"。这里面的关键词每一个都要落到具体参数上。"500名用户"对应的是并发用户数还是在线用户数?"登录系统并查询订单列表"对应几个请求、有没有依赖关系?"持续运行10分钟"是压测时长还是稳定运行时长?这些都得先明确。
我一般的拆解顺序是:先列出所有涉及的业务操作,再判断每个操作之间的依赖(比如查询订单必须先登录拿到token),然后估算每个操作的请求数量和请求比例,最后才落到具体的线程数和持续时间上。举个例子,"登录"可能包含请求验证码、提交账号密码、校验token三个接口,只盯着登录接口压,忽略验证码接口,很容易在真实场景下因为验证码服务扛不住而整体失败。
提示:赛题描述里的数字不要直接当成参数用,一定要先做业务建模。直接拿"500用户"当线程数跑,是新手最常见的低级错误。
3.2 并发数、TPS、响应时间三者之间的关系
这三个概念必须理清,否则你根本不知道自己在压什么。并发数是指同一时刻向系统发请求的虚拟用户数量;TPS(Transactions Per Second)是系统每秒能处理的事务数;响应时间是从发出请求到收到完整响应的耗时。
它们之间有个近似关系,也就是常说的利特尔法则(Little's Law):并发数 ≈ TPS × 平均响应时间。注意响应时间的单位要换算成秒。比如目标TPS是100,平均响应时间要求200毫秒,那么需要的并发数约等于100 × 0.2 = 20。反过来,如果你设了50个并发,测出来平均响应时间是400毫秒,那实际TPS大约是50 ÷ 0.4 = 125。
这个公式的用处在于,它能帮你反推合理的线程数,也能用来交叉验证测试结果是否合理。如果测出来的TPS和并发数、响应时间对不上,说明中间有地方出问题了,可能是请求被拦截、事务定义不对,或者存在大量错误请求没被统计进去。
| 指标 | 含义 | 常用单位 | 关注点 |
|---|---|---|---|
| 并发用户数 | 同一时刻活跃的虚拟用户 | 个 | 决定施压强度 |
| TPS | 每秒完成的事务数 | 笔/秒 | 系统处理能力的核心指标 |
| 响应时间 | 请求到响应的耗时 | 毫秒 | 用户体验的直接体现 |
| 错误率 | 失败请求占比 | % | 一般要求低于0.5% |
| 吞吐量 | 单位时间传输的数据量 | KB/s | 判断网络是否成瓶颈 |
3.3 指标基线与通过标准怎么定
赛题一般会给出明确的通过标准,比如"平均响应时间不超过500毫秒,TPS不低于200,错误率为0"。如果没有给,就要自己设定基线。我通常的做法是先用少量并发(比如5个用户)跑一轮基准测试,记录下单用户或低并发下的响应时间,作为基线值。然后按照经验设定目标,一般要求平均响应时间不超过基线的3到5倍,90%响应时间不超过基线的5到8倍。
为什么要看百分位数而不是只看平均值?因为平均值会被极值拉偏。一个系统99%的请求都在100毫秒内完成,但有1%的请求耗时10秒,平均值可能看着还行,实际上那1%的用户体验已经崩了。所以报告里一定要有90%、95%、99%这几个分位数值。JMeter的HTML报告默认会给出这些百分位数据,分布图也一目了然。
4. 脚本开发实操:从录制到可复用的完整链路
4.1 录制与抓包:先把请求摸清楚
做压测脚本,第一步永远是搞清楚系统到底发了哪些请求。JMeter自带HTTP(S) Test Script Recorder,可以配合浏览器代理录制。操作方式是:在JMeter里添加录制控制器和HTTP代理服务器,设置端口(比如8888),然后浏览器设置代理指向本机8888端口,访问目标系统,操作一遍完整业务流程,JMeter就会把所有请求录下来。
不过录制出来的脚本往往很臃肿,夹杂着大量静态资源请求(图片、CSS、JS)和无关的第三方请求。我的习惯是录制完之后手动清理,只保留核心业务接口。判断标准很简单:只保留返回JSON或XML的XHR请求,静态资源一般不需要单独压测,除非赛题明确要求测首页加载性能。
录制过程中有个细节要注意,浏览器可能会走系统代理或者忽略某些地址,导致录制不全。稳妥的做法是用F12开发者工具的Network面板同步观察,确保关键请求都被录到了。另外,HTTPS站点首次录制时JMeter会生成自签名证书,需要在浏览器里信任一下,否则请求会因为证书问题失败。
4.2 参数化:让脚本"活"起来
如果脚本里所有用户都用同一个账号、同一组参数,那这个压测其实是在测缓存,不是在测系统。参数化就是让每个虚拟用户使用不同的数据。JMeter里最常用的参数化方式有CSV Data Set Config、用户定义变量、函数助手三种。
以登录场景为例,准备一个CSV文件,内容如下:
username,password student001,Pass@123 student002,Pass@123 student003,Pass@123然后在JMeter里添加CSV Data Set Config,设置文件路径、变量名username,password,分隔符为逗号,其他保持默认。请求里用${username}和${password}引用即可。这里有几个配置项容易踩坑:
| 配置项 | 推荐值 | 说明 |
|---|---|---|
| Recycle on EOF | True | 文件读完后循环使用 |
| Stop thread on EOF | False | 不要因为读不到数据就停线程 |
| Sharing mode | All threads | 所有线程共享文件,避免重复读 |
| Delimiter | , | 与CSV实际分隔符一致 |
注意:如果设置了多线程共享同一个文件,一定要勾选"All threads"共享模式,否则每个线程都会重新打开文件从头读,导致数据重复使用。实测下这个坑很隐蔽,因为压测过程不报错,但数据分布完全不对。
参数化数据量也有讲究。如果并发500,数据文件里至少准备500条不重复数据,否则会出现多个用户抢同一账号的情况,可能触发系统的登录限制策略导致大量失败。
4.3 关联:处理token和Session这类动态值
Web系统为了安全,接口之间往往有依赖,最典型的就是登录后返回token,后续每个请求都要带上这个token。这个动态值不能写死,必须做关联,也就是从上个请求的响应里提取出来,传给下个请求。
JMeter里做关联主要用后置处理器,JSON Extractor适合处理JSON响应,正则表达式提取器适合处理HTML或非标准格式响应。以JSON接口为例,登录响应如下:
{ "code": 0, "data": { "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.xxxxx", "userId": 10086 } }用JSON Extractor提取token,配置如下:变量名填auth_token,JSON路径表达式填$.data.token,匹配数字填1,默认值留空或填NOT_FOUND。后续请求的HTTP信息头管理器里加上Authorization: Bearer ${auth_token}即可。
正则表达式提取器的写法是"token":"(.+?)",其中(.+?)是提取的内容,括号表示要捕获的部分。正则写不对是新手最常犯的错误,常见问题包括贪婪匹配导致提取内容过长、转义字符遗漏、没有设置匹配数字导致随机取值。我建议优先用JSON Extractor,表达能力更强也更好调试。
4.4 断言与事务:让结果能说明问题
光跑脚本不看结果没有意义。断言的作用是判断请求是否真正成功。JMeter默认只看HTTP状态码,但很多系统即使业务失败也返回200,所以必须加业务断言。常用的是响应断言和JSON断言。比如判断响应里"code":0,用JSON断言配置JSON路径$.code,期望值0即可。
事务控制用事务控制器(Transaction Controller)。把一组有依赖的请求包在一个事务控制器里,勾选"Generate parent sample",这样JMeter会把这组请求合并成一个事务来统计响应时间和TPS。这一点直接影响你报告里的TPS数据是否合理。如果登录包含三个接口,不合并的话TPS会被算成三个独立事务,数字虚高,评委一眼就能看出问题。
4.5 定时器与控制器:模拟真实用户行为
真实用户不会像机器一样精确地每秒发一次请求,请求之间是有停顿的。定时器就是用来模拟这个停顿的。常用的有固定定时器(Constant Timer)、高斯随机定时器(Gaussian Random Timer)、同步定时器(Synchronizing Timer)。
固定定时器最简单,设置一个固定延迟,比如500毫秒。高斯随机定时器更真实,设置一个基准延迟和偏差范围,请求间隔会在范围内随机波动。同步定时器用于制造真正的并发峰值,它会阻塞线程直到达到指定数量再一起释放,适合模拟秒杀这种极端场景。
控制器方面,循环控制器用于重复执行,如果控制器用于条件判断,吞吐量控制器用于按比例分配请求。比如登录只占20%的请求量,查询占80%,就用吞吐量控制器设置百分比,避免所有用户都在不停登录。
5. 场景设计与执行:从单机到分布式的完整流程
5.1 阶梯加压与稳态施压的组合
场景设计决定测试的深度。我一般用两段式:先阶梯加压,再稳态施压。阶梯加压用来找拐点,也就是系统在多少并发下开始性能下降;稳态施压用来验证系统在目标并发下能否长时间稳定运行。
阶梯加压用jpgc插件的Stepping Thread Group,配置大致是:初始延迟30秒,起始线程数0,每30秒增加50个线程,增加到500后持续运行5分钟。这样能看到TPS随并发上升的变化曲线。点击"Add"按钮下载jpgc插件后重启JMeter,就能在Threads菜单里找到。
稳态施压再开一个线程组,直接设置目标并发数,持续时间10分钟以上。两段场景可以放在同一个测试计划里顺序执行,中间加个固定定时器隔开,避免相互影响。
5.2 分布式压测怎么搭
单机压不动的时候就要上分布式。JMeter的分布式架构是主从模式:一台控制机(master)负责分发脚本和汇总结果,多台执行机(slave)负责实际施压。搭建步骤大致是:
- 所有机器安装相同版本的JMeter和JDK,版本必须完全一致,否则会出现通信问题。
- 执行机上启动jmeter-server,控制机上修改
jmeter.properties里的remote_hosts,写上所有执行机的IP和端口,默认端口1099。 - 控制机运行
jmeter -n -t test.jmx -R IP1,IP2 -l result.jtl -e -o report。
注意:分布式压测时,CSV数据文件要同步到每台执行机相同路径下,或者使用共享目录。否则执行机读不到数据,参数化直接失效。我见过一次就是因为这个,三台执行机里有两台在报文件找不到,测出来的并发数是实际值的三分之一。
还有一点,分布式压测的总并发数等于各执行机线程数之和,脚本里的线程数要按执行机数量分配。汇总的报告里会把数据合并统计,但注意各执行机的时钟要同步,否则时间戳错乱会导致结果分析出错。
5.3 压测执行过程中的现场记录
压测不是点了开始就完事,执行过程中要盯着几个关键数据。我会同时开着被测机的资源监控(CPU、内存、磁盘IO、网络)和JMeter的聚合报告,每隔一段时间记录一次数据。一旦发现TPS突降、响应时间飙升或者错误率上升,立刻记下时间点,方便后续定位。
有一次压测跑到第4分钟,TPS从320突然掉到80,我以为是系统崩了,结果查了半天发现是压测机所在网段有个限流策略触发了。如果没有记录这个时间点,事后翻报告根本找不到原因。所以现场记录这一步千万别省,哪怕只是拿纸笔写几个数字。
6. 结果分析与报告撰写:让数据替你说话
6.1 核心指标怎么读
JMeter生成的HTML报告里图表很多,但真正要重点看的是这几个。聚合报告里的Average、Median、90% Line、95% Line、99% Line、Min、Max、Error%,响应时间分布图,TPS随时间的变化曲线,还有响应时间与并发数的关系图。
| 指标 | 健康表现 | 异常表现 | 可能原因 |
|---|---|---|---|
| 平均响应时间 | 平稳,随并发缓慢上升 | 陡增或剧烈波动 | 存在瓶颈或资源竞争 |
| TPS | 随并发线性上升后有平台期 | 上升后又下降 | 系统过载,开始退化 |
| 错误率 | 接近0 | 超过1% | 限流、超时、数据问题 |
| 90%响应时间 | 与平均差距小 | 远高于平均值 | 存在长尾请求 |
判断系统瓶颈的一个经验法则是:如果TPS达到某个值后不再上升,而响应时间开始快速上涨,说明系统达到了最大处理能力,这个点就是性能拐点。报告中要明确指出拐点对应的并发数,这个数字往往比最终通过标准更有分析价值。
6.2 瓶颈定位的排查思路
发现性能不达标后,要能说出瓶颈在哪。常见瓶颈分四类:应用层、数据库层、中间件层、网络层。排查顺序一般是自下而上。
先看被测机的CPU和内存。CPU持续接近100%,说明应用层计算密集;内存持续增长不释放,可能是内存泄漏;磁盘IO打满,往往是日志写入或者数据库落盘太频繁。再看数据库,慢查询、连接池打满、锁等待是常见问题。中间件层重点看连接数和线程池配置。网络层看带宽是否打满、TCP连接数是否达到上限。
有一次测一个查询接口,TPS上不去,应用服务器CPU才30%,数据库CPU也正常,最后发现是Tomcat的最大线程数默认只有200,我们的并发数超过了这个值,请求全在排队。把线程数调到500后TPS直接翻倍。所以排查不能只盯着机器负载,配置参数同样重要。
6.3 报告的结构与撰写要点
性能测试报告不是数据堆砌,而是一个有逻辑的论证过程。我通常按这个结构写:测试背景与目标、测试环境说明、测试方案设计、测试执行记录、测试结果分析、瓶颈定位与优化建议、结论。
环境说明要具体到硬件配置、软件版本、网络拓扑,这些信息决定结果的可复现性。方案设计要写清楚业务模型、并发策略、数据准备方式。结果分析是重点,每个指标都要有对应的图表和解读,不能只贴图不解释。优化建议要针对具体问题给出可落地的方向,比如"建议将数据库连接池从20调整到100,并增加慢查询日志监控"。
报告中要特别注意数据的一致性。如果同一组压测跑了三次,数据差异很大,要说明原因,是环境波动还是测试方法问题。评委往往很看重这一点,因为它反映了你对测试过程的可控程度。
7. 常见问题与避坑速查
7.1 脚本类高频问题
脚本问题占了新手失败原因的一大半。整理了下面这张速查表,遇到问题先对照排查。
| 问题现象 | 可能原因 | 解决方法 |
|---|---|---|
| 大量请求返回403 | token未关联或过期 | 检查关联提取,确认token传递正确 |
| 中文乱码 | 编码未设置为UTF-8 | 修改jmeter.properties和启动参数 |
| 参数化数据重复 | 共享模式配置错误 | 设置为All threads共享 |
| 事务TPS虚高 | 未使用事务控制器 | 将关联请求包入事务控制器 |
| 响应时间异常小 | 请求被缓存或未真正发出 | 关闭缓存管理器,检查断言 |
还有一个特别隐蔽的问题:JMeter默认会缓存DNS解析结果,如果被测系统有多台服务器做负载均衡,压测时可能所有请求都打到同一台上。可以在jmeter.properties里修改httpclient4.time_to_live参数,或者干脆在HTTP请求里直接用IP地址。这个问题在高并发场景下影响很大,会导致压测结果无法反映真实集群能力。
7.2 环境类问题与资源监控
环境问题往往表现为"数据看着正常但就是跑不上去"。除了前面说过的压测机资源瓶颈,还有几个点要注意。一是JVM堆内存不足,压测过程中会看到JMeter自己先卡住,日志里报OutOfMemory。二是文件描述符限制,Linux下默认是1024,高并发时要调大到65535,修改/etc/security/limits.conf。三是端口耗尽,客户端发起大量短连接时可能把本地端口用光,解决办法是开启连接复用或者调大端口范围。
监控方面,被测机如果是Linux,可以用top、vmstat、iostat、sar这些命令实时看。有条件的话上nmon或者Prometheus加Grafana,能出趋势图,分析起来更直观。压测过程中我一般每30秒截一次资源图,最后和JMeter的TPS曲线叠在一起看,瓶颈在哪一目了然。
7.3 数据准备与业务合理性
数据问题容易被忽略,但它直接影响结果的可信度。几个原则要记住:数据量要足够,至少是并发数的两到三倍;数据要分布均匀,不能所有请求都命中同一批热点数据;数据要符合业务约束,比如订单状态、金额范围要合理,否则可能被业务校验拦截。
还有一点是关于思考时间的设置。完全不设思考时间的压测,测出来的是系统的极限能力,不是真实场景下的表现。真实用户操作之间有停顿,一般设置在1到5秒之间。如果是赛题明确要求测极限能力,那可以不设;如果要求模拟真实场景,就一定要加上。这个细节在报告里要说明清楚,避免评委质疑场景的真实性。
7.4 赛场上的一些实战经验
最后说几个和比赛直接相关的经验。第一,赛题时间有限,不要一上来就追求完美脚本,先跑通主流程拿到基础数据,再逐步优化。第二,随时保存脚本和数据,我见过因为软件崩溃丢失整份测试计划的惨案。第三,报告里的每一个结论都要有数据支撑,不要写"系统性能良好"这种空话,要写"在300并发下平均响应时间230毫秒,TPS达到410,满足赛题要求的500毫秒和300TPS标准"。
第四,也是我踩过最深的坑:不要迷信工具默认值。JMeter的很多默认参数是通用场景下的折中值,直接拿来用往往不合适。比如HTTP请求的默认超时时间是无限的,一旦接口不响应,线程会一直挂着,压测提前结束都结束不干净。建议在HTTP请求默认值里统一设置连接超时和响应超时,比如连接5秒、响应30秒,超时就报错,这样问题能及时暴露出来。
这些内容基本覆盖了Web性能测试实例(一)这类赛题从准备到收尾的完整链路。工具操作本身不难,难的是每个环节背后那点判断力——什么时候该加并发、什么时候该怀疑数据、什么时候该停下来排查。这种东西看文档学不会,只能自己一轮一轮跑出来。我个人的体会是,把每次压测都当成一个完整的闭环来对待,从建模到报告走一遍,跑上七八轮之后,看到赛题描述心里基本就有底了。