第一次拿到 Web 性能测试赛项的题目,我盯着那张只有几行需求的纸坐了大概十分钟,脑子里想的不是"怎么压",而是"它到底想让我交出什么东西"。全国大学生软件测试大赛里的 Web 性能测试方向,和平时做功能测试、写几个用例跑一跑的感觉完全不一样——它考的是你能不能把一套负载模型搭起来、把数据读明白、把结论说清楚。这篇就当是我自己的一次完整复盘,从环境准备、脚本开发、场景设计,一直写到结果分析和报告收尾,重点放在"一个完整实例"上,而不是零散的知识点罗列。不管你是第一次报名、还在纠结 JMeter 装哪个版本,还是已经能跑通脚本但报告写不出结论,我想这些内容都能对上你的需求。软件测试这个行当里,性能测试一直是面试高频区,八股文背得再熟,真到压一个系统的时候能不能讲清楚"并发数怎么算出来的""TPS 掉在哪个环节",才是分水岭。
1. 赛项拆解:Web 性能测试到底在考什么
1.1 从任务书倒推考点分布
我习惯拿到题目先做一件事:把任务书里的动词圈出来。通常会出现"设计""执行""分析""提交"这几个词,它们其实对应了四个完全不同的评分维度。设计对应测试计划的结构是否合理,执行对应脚本能否稳定跑完、数据是否可信,分析对应你能不能从聚合报告里挑出真正的瓶颈,提交对应报告有没有把结论讲清楚。
很多同学栽在"执行"上,觉得脚本能跑通就万事大吉,结果跑到一半冒出几百个错误,最后交上去的数据自己都不敢信。也有人脚本写得漂亮,报告里只有一句"系统性能良好",评委看完完全不知道他测了什么。所以真正的考点不是工具会不会用,而是闭环能力:需求到模型,模型到脚本,脚本到数据,数据到结论。
还有一点容易被忽略:题目里给的业务场景通常只有一两个页面,比如登录加查询,看起来特别简单。但越是简单的场景,越考验你对参数化、关联、事务边界的处理是否规范。因为场景简单,评委的注意力就全落在细节上。
1.2 为什么 JMeter 是事实标准
这几年赛项里能用的工具其实不止一个,LoadRunner、Locust、k6 都有人用,但 JMeter 依然是绝大多数人的选择,原因很实在。第一,它是纯 Java 的,跨平台,Windows 和 Linux 上跑起来行为一致;第二,命令行模式加 HTML 报告的组合,天然适合"跑完就出报告"的比赛节奏;第三,插件生态成熟,阶梯加压、每秒事务数曲线这些都有现成组件。
我用 Locust 也做过几轮压测,Python 写脚本确实舒服,但在比赛这种时间紧、要反复调参的场景下,JMeter 的图形化元件树对排查问题的帮助更大——你能一眼看出哪个元件顺序放错了。Locust 一旦脚本里有个协程阻塞,排查起来就很费劲。
注意:工具选型不要临时改。见过有人前两天用 JMeter 写了一版,第三天觉得 k6 更酷,结果两边都半吊子。选定一个就深挖到底。
顺带说一句,性能测试相关岗位的面试题里,"你为什么选 JMeter 而不是 LoadRunner"出现的频率非常高,答案不是"因为免费",而是要说清楚协议支持范围、扩展方式、报告能力这几个维度的取舍。这段话你写进比赛报告里,也是加分的。
2. 环境搭建与工程目录规划
2.1 版本匹配这件小事的杀伤力
JMeter 对 JDK 版本有要求,新版一般要求 JDK 8 以上,某些版本在 JDK 17 上会有模块访问告警。我踩过的坑是:本机装了 JDK 17,JMeter 启动正常,但一挂上某些老插件就报反射相关的异常。后来统一换成 JDK 8 或者 JDK 11,问题就消失了。
具体做法是给 JMeter 单独指定 JDK,不去动系统的 JAVA_HOME。在jmeter.bat同目录下改环境变量,或者直接在命令行里临时设置:
export JAVA_HOME=/opt/jdk-11 export PATH=$JAVA_HOME/bin:$PATH ./jmeter -vWindows 下同理,用set JAVA_HOME=...临时覆盖。这样做的理由是避免污染系统环境,比赛机器往往是公用的,动全局变量容易出乱子。
内存参数也必须调。默认堆只有 1G,压测时如果开了聚合报告这种内存大户,很容易 OOM。在jmeter.bat或jmeter脚本里找到 HEAP 那一行,改成:
HEAP="-Xms2g -Xmx4g -XX:MaxMetaspaceSize=256m"Xms 和 Xmx 设成一样,可以避免堆在运行中反复扩容带来的抖动,这个细节在长时间稳态压测里影响不小。
2.2 目录结构决定你能不能在 5 分钟内重跑
我见过太多人把所有文件丢在桌面,result.jtl和plan.jmx混在一起,第二次跑的时候覆盖了第一次的数据,急得拍桌子。一个干净的结构应该是这样:
| 目录 | 用途 | 说明 |
|---|---|---|
plan/ | 存放.jmx脚本 | 按场景命名,如login_query.jmx |
data/ | CSV 参数文件 | 账号、关键词等 |
result/ | jtl 与 HTML 报告 | 每次执行建一个带时间戳的子目录 |
conf/ | 自定义 properties | 覆盖jmeter.properties里的关键项 |
执行的时候用绝对路径或者-p指定配置文件,避免"在我机器上能跑"的尴尬。
jmeter.properties里我必改的几项,顺便解释下为什么:
jmeter.save.saveservice.output_format=csv jmeter.save.saveservice.thread_counts=true sampleresult.default.encoding=UTF-8 CookieManager.save.cookies=true httpsampler.max_redirects=20thread_counts=true会在结果文件里记录当时的线程数,后面画 TPS 与并发的关系曲线就靠它,不打开的话你根本不知道某个时间点压了多少并发。CookieManager.save.cookies=true是把 Cookie 里的 JSESSIONID 之类当变量存下来,后面做关联判断时很方便。
提示:改 properties 之前先备份原文件。比赛现场如果要换机器,把
conf/整个目录拷过去就能复现环境,比重新配一遍省十几分钟。
3. 脚本开发实战:从录制到参数化关联
3.1 录制还是手写,别纠结太久
录制能省时间,但录出来的脚本通常是一坨——一堆自动命名的事务控制器、一堆用不上的监听器、固定的 Cookie 和 token。我的做法是折中:用浏览器代理或者 BlazeMeter 插件录一遍,只保留请求本身,然后手工重建元件树。
重建的时候按这个顺序往下摆:
- 测试计划
- 线程组
- HTTP 请求默认值(填服务器、端口、协议、内容编码 UTF-8)
- HTTP Cookie 管理器
- HTTP 信息头管理器
- 事务控制器(包住一个完整业务)
- 取样器 + 提取器 + 断言
顺序不是随便排的。HTTP 请求默认值要放在线程组下面、取样器上面,这样同级的取样器才继承得到。Cookie 管理器和信息头管理器是并列关系,谁先谁后不影响,但都必须在取样器之前。
事务控制器有一个选项叫Generate parent sample,勾上之后,聚合报告里只显示事务控制器这一条的统计,子请求被折叠进去。这样做的好处是报告干净,你一眼能看出"一个完整的登录业务"平均耗时多少;坏处是你看不到单个请求的耗时,定位问题时要临时取消勾选。我在比赛里的做法是:调脚本阶段不勾,出报告阶段勾上。
3.2 参数化:CSV Data Set Config 的几个开关
参数化是为了让不同虚拟用户用不同数据,避免缓存命中导致数据虚高。CSV 数据文件集配置里,参数含义要搞清楚:
| 参数 | 取值建议 | 原因 |
|---|---|---|
| Filename | 用相对路径data/accounts.csv | 换机器不用改路径 |
| Variable Names | username,password | 与请求体里的占位符对应 |
| Delimiter | 逗号 | 数据里别混入逗号 |
| Recycle on EOF | 视情况 | 数据够用就设 False |
| Stop thread on EOF | 视情况 | 数据不够时避免线程空转 |
| Sharing mode | All threads | 多线程下各取一行,不重复 |
Sharing mode这个选项特别容易忘,默认就是 All threads,如果误改成 Current thread,每个线程都会从头读文件,参数化就白做了。
还有一个隐藏坑:CSV 文件如果带 BOM 头,第一行的变量名会带着不可见字符,导致取值失败。用记事本另存的时候选"UTF-8 无 BOM",或者干脆用 VS Code 检查一遍。
3.3 关联:正则和 JSON 提取器怎么选
关联的本质是把上一个请求的响应内容取出来,喂给下一个请求。接口返回 JSON 就用 JSON 提取器,返回 HTML 或者混合文本才用正则提取器。选错的代价是表达式又长又容易断。
JSON 提取器的配置很直观:Names of created variables写变量名,JSON Path expressions写$.data.token这样的路径,Match No.填 0 或 1,Default Values一定要填一个兜底值。
注意:Default Values 千万别留空。留空的话,一旦提取失败,后续请求会带着空值发出去,服务端返回 401,你在聚合报告里看到的是大面积错误,但要排查半天才知道根因是提取器没配好。
正则提取器的模板务必写$1$,匹配数字写 1,默认值同样要填。
调试关联有个笨办法但很好用:加一个 Debug Sampler 加一个察看结果树,跑一次单线程,看看变量到底有没有取到。别一次开一百个线程去猜。
3.4 断言与事务边界的配合
断言是保证数据可信的底线。响应断言里,我一般同时检查响应码和一段关键文本。只检查响应码有个问题:有些系统出错时也返回 200,但页面内容是"系统繁忙"。只检查文本又可能因为文案微调而误报。两个一起上,稳。
持续时间断言也值得加,比如要求接口 500ms 内返回,就设Duration in milliseconds为 500。这样超时的请求会被标记为失败,你就能算出"响应时间达标率"这个指标,比单纯看平均值有说服力得多。
事务的边界要想清楚。我见过有人把一次页面加载拆成十几个请求,每个都单独算事务,报告里几十行数据,评委根本找不到重点。合理的做法是:一个完整的用户操作 = 一个事务。登录、查询列表、提交表单,各算一个。
4. 场景设计与负载模型
4.1 并发数、RPS、TPS 到底怎么换算
这是整个比赛里我最想强调的一段,因为太多人凭感觉填数字。三者关系用利特尔法则就能说清楚:
并发数 = TPS × 平均响应时间(秒)
举个例子。题目要求系统支撑每秒 100 笔业务,你测出来平均响应时间是 200 毫秒,那么需要维持的并发就是 100 × 0.2 = 20 个线程。如果响应时间涨到 800 毫秒,同样 100 TPS 就需要 80 个并发——这就是为什么响应时间一恶化,系统就雪崩。
反过来说,如果你填了 100 个线程,测出来 TPS 只有 50,平均响应时间 2 秒,套公式一算 100 × 2 = 200 ≠ 50,说明什么?说明有大量请求在排队或者失败,系统已经过载了。
把这段计算过程写进报告,比任何形容词都有分量。
4.2 阶梯加压与稳态保持
不要一上来就拉到最大并发。正确做法是阶梯加压,观察曲线拐点。用 Custom Thread Groups 插件里的 Stepping Thread Group,配置大概是:
- 初始并发 10
- 每 30 秒加 10 个线程
- 加到 100 停止加压
- 稳态保持 5 分钟
- 然后线性降下来
为什么要保持至少 5 分钟?因为系统在头一两分钟里有缓存预热、连接池填充、JIT 编译这些过程,数据不稳定。稳态期的数据才是可信的。
降速阶段也别省,有些系统在压力撤掉之后会有资源回收的抖动,这属于观察项。
如果比赛环境不让装插件,就用普通线程组配合Ramp-up Period,比如 100 个线程、ramp-up 写 200 秒,也勉强能模拟线性加压,只是看不出稳态平台。
4.3 思考时间和吞吐量控制器
思考时间决定负载的真实性。真实用户在两秒内连点五次和每两秒点一次,给系统的压力完全不同。用 Constant Timer 或 Gaussian Random Timer 模拟,一般设在 1 到 3 秒之间。
如果题目直接给了目标吞吐量,那就用 Constant Throughput Timer。有个坑必须说:这个元件的单位是每分钟,不是每秒。要跑 120 TPS,得填 7200。我第一次用的时候填了 120,跑出来 2 TPS,还以为是系统扛不住,白白排查了半小时。
同时要注意Calculate Throughput based on这个选项,默认是this thread only,意思是每个线程各自限速,多线程下总吞吐会被放大。想做全局限速,得选all active threads。
5. 执行与结果分析:把数据读成结论
5.1 命令行执行与 HTML 报告
正式压测一定用命令行,GUI 模式只用来调脚本。GUI 开着跑压测,JMeter 自己的界面渲染会吃掉大量 CPU,数据完全不可信。而且启动时会弹那句经典提示。
命令大概长这样:
rm -rf result/run_01 ./jmeter -n \ -t plan/login_query.jmx \ -l result/run_01/result.jtl \ -e -o result/run_01/html \ -Jthreads=100 -Jduration=300-J传的是自定义属性,在脚本里用${__P(threads)}引用,这样同一份脚本改参数就能跑不同场景,不用重新打开 GUI。
有个细节:-e -o生成 HTML 报告需要 jtl 里有足够的数据,数据量太少或者时间跨度太短(官方建议 30 秒以上)会直接报错不生成。所以哪怕只是试跑,也让它跑够一分钟。
5.2 聚合报告里真正该看的几列
聚合报告列很多,但重点就几个:
| 指标 | 含义 | 怎么用 |
|---|---|---|
| Average | 平均响应时间 | 容易被极值拉偏,参考即可 |
| 90% Line | 90% 请求的响应时间 | 比平均值更能反映用户体验 |
| 99% Line | 长尾请求 | 判断是否有慢请求拖后腿 |
| Error % | 错误率 | 超过 1% 就要警惕 |
| Throughput | 吞吐量(默认按秒) | 与目标 TPS 对比 |
| Received KB/sec | 网络吞吐 | 判断是否受带宽限制 |
如果平均值和 90% 线差距巨大,比如平均 300ms 但 90% 线是 2s,说明有一小撮请求特别慢,可能是慢 SQL 或者锁竞争,这时候要去响应时间分布图里看。
5.3 从曲线里找拐点
用 TPS 图加响应时间图叠着看,你会看到一个典型的形态:并发增加,TPS 线性上升,响应时间基本平稳;过了某个点,TPS 不再涨甚至下降,响应时间开始陡增。这个点就是系统的最大处理能力。
我在一次练习里测出来,并发从 10 加到 60 的时候,TPS 从 50 稳定涨到 280,响应时间一直是 200ms 上下;到 80 并发时 TPS 反而掉到 260,响应时间跳到 600ms。结论就很明确:这台环境在 60 到 80 并发之间存在性能拐点,推荐工作负载不超过 60 并发。
这个结论比"系统性能良好"有用一百倍。
6. 常见问题排查与独家避坑清单
6.1 中文乱码与编码
乱码的根源永远是编码不一致。按这个顺序检查:CSV 文件是不是 UTF-8 无 BOM;HTTP 请求默认值里的Content encoding有没有写 UTF-8;jmeter.properties里sampleresult.default.encoding是不是 UTF-8。三处都对上,基本就不乱码了。
还有一种乱码只在察看结果树里出现,但断言能过,那通常是响应头的 Content-Type 没带 charset,JMeter 按默认编码解析了。这种情况不影响压测结果,可以忽略,但如果要写进报告最好说明一下。
6.2 连接类报错速查
| 报错信息 | 常见原因 | 处理方式 |
|---|---|---|
| ConnectException | 目标端口未监听或防火墙拦截 | 先用 curl 从压测机验证连通性 |
| SocketException: Too many open files | 文件描述符耗尽 | 调大ulimit -n,同步调优内核参数 |
| BindException: Address already in use | 本地端口耗尽 | 缩短连接复用时间,或开启端口复用 |
| Read timed out | 服务端处理超时 | 确认超时阈值,区分是网络还是业务慢 |
| Non HTTP response code | 非 HTTP 协议返回 | 检查是否被重定向到登录页 |
最后一行那个特别常见:压测跑着跑着,一部分请求被重定向到登录页,返回的是 HTML 而不是 JSON。原因是会话过期或者 Cookie 没保持住。解决办法是把登录做成一次性前置,用setUp Thread Group单独跑,把 token 提取到全局属性里,后面所有线程引用同一个 token。
6.3 压测机自己成为瓶颈的识别
这是最阴的一类问题:你以为是服务端不行,其实是压测机扛不住。判断方法看两个地方。
一是看压测机的 CPU 和内存。如果压测过程中压测机 CPU 跑到 90% 以上,那数据基本不可信。二是看错误类型。如果错误集中在连接建立阶段(ConnectException、BindException),且数量随并发线性增长,八成是压测机的端口或者文件句柄到上限了。
我遇到过最典型的一次:800 并发的时候错误率突然跳到 15%,怎么调服务端参数都没用。后来在压测机上执行ulimit -n,显示 1024。改成 65535 之后,错误率直接归零。这个坑我记了很久。
提示:分布式压测能缓解压测机瓶颈,但会引入时钟同步、结果合并的问题。比赛时间紧的话,先把单机调明白。
7. 测试报告撰写与提分细节
7.1 一份能被评委读完的报告骨架
报告不用写得像论文,但要有一条清晰的主线。我的结构是:测试目标与范围、测试环境(含压测机与被测机配置)、业务模型与负载模型(含并发数推算过程)、测试执行记录、结果数据与分析、结论与建议、附录(脚本与原始数据)。
其中"并发数推算过程"和"结论与建议"是拉开差距的两节。前者证明你不是拍脑袋填数字,后者证明你真的读懂了数据。
数据部分只放关键图表,聚合报告截图、TPS 曲线、响应时间曲线各一张就够,剩下丢附录。我见过报告里贴了二十张图,正文一句话没有的,那种很难拿高分。
7.2 评委真正关心的三件事
第一,你的测试是不是可复现。脚本、数据文件、执行命令、参数配置都在,别人照着能跑出差不多的结果。
第二,你的结论有没有数据支撑。说"系统存在性能瓶颈"就要指出在多少并发、哪个事务、响应时间涨了多少。
第三,你有没有边界意识。比如说明白这次测试只覆盖了登录和查询两个事务,不涉及文件上传和批量导出,测试结果不能外推到整个系统。这种自我设限反而是专业的表现。
还有个小细节:报告里的时间、版本号、环境参数要前后一致。前面写的是 JDK 11,后面截图里显示 JDK 17,评委会觉得你不严谨。
我个人在几次练习和比赛里最大的体会是:性能测试的胜负手不在工具,而在"想清楚"。动手之前先花十分钟把负载模型算出来、把事务边界画出来,后面能省下几个小时反复调脚本的时间。另一个小技巧是,每次压测都把命令行参数完整记在一张便签或者 README 里,包括那次改了哪个配置、为什么改——过两天回头看数据,你会感谢当时的自己。这套流程跑顺之后,你会发现它不只是应付比赛,日常做接口压测、上线前评估容量,用的都是同一套路子。