JMeter自动化测试落地指南:分层设计、参数化、断言与流水线实践
上周有个朋友半夜给我发了个压缩包里面躺着一份四百多行的 .jmx 文件附言只有一句这玩意儿我本地跑得挺好的扔到服务器上就一堆报错你帮我看看。我打开一看心里大概就有数了——线程组里塞了三十多个 HTTP 请求参数全写在请求里硬编码断言只有一条响应码等于200CSV 文件用了绝对路径还是他 D 盘下的一个中文目录。这不是个例。JMeter 的自动化测试方案绝大多数人卡住的地方从来不是不会点界面而是从脚本能跑到方案能落地之间那一段没人讲清楚的路。我这些年做接口自动化、做性能压测、也帮团队做过持续集成的接入前后维护过几千个 JMeter 用例踩过的坑能写满两页 A4 纸。这篇文章我想把这些东西一次性讲透方案怎么分层、脚本怎么工程化、参数化怎么选、断言怎么写才不脆、录制脚本为什么必须洗、怎么接进流水线以及压测场景到底该怎么设计。不管你是刚下载完 JMeter 的新手还是已经在公司里扛着自动化测试这摊事的老手应该都能从里面抠出点能直接用的东西。1. 先划地盘JMeter 在自动化测试方案里该占哪块很多人一上手就陷入工具选择的纠结其实这个问题的答案取决于你要解决什么。自动化测试这个词太笼统了它底下至少藏着三件完全不同的事搞清楚这个方案才有骨架。1.1 回归测试、接口测试、性能测试是三个目标回归测试关心的是改动之后原来的功能还正常吗它的特点是用例数量多、执行频率高、单次执行快、结果判定必须自动化。接口测试关心的是这个接口在各种入参下行为是否符合契约重点是边界值、异常码、数据一致性。性能测试关心的是在多少并发下系统的响应时间和吞吐量是什么曲线重点是场景模型和资源观察。这三件事的判定标准、执行节奏、需要的观测数据完全不一样。我见过太多人拿一套压测脚本硬当回归用例跑结果每次执行要十几分钟开发根本不愿意等最后这套自动化就烂在仓库里了。JMeter 本身是能同时承担这三件事的但方案上必须把它们拆成三套独立的脚本集合和执行入口这是能长期跑下去的前提。1.2 JMeter 的甜区在哪雷区又在哪我在团队里推 JMeter 的时候会先把这张表贴在墙上避免有人拿它去干不该干的活。场景适合度说明HTTP/REST 接口功能与回归非常适合开箱即用断言和结果展示都很成熟多协议压测HTTP、TCP、JDBC、MQTT非常适合插件生态完整混合协议场景优势明显数据库层数据校验适合JDBC Request JDBC 断言能覆盖大部分校验复杂业务逻辑编排十几步依赖链路勉强可用模块控制器可以拆但可读性会快速下降移动端 UI 自动化不适合这类活交给专门的移动端框架别硬撑前端页面渲染类校验不适合它看不到页面长什么样只看到报文长周期数据构造与清理不适合用脚本更省事JMeter 排错成本高看清这张表之后你会发现自己团队里百分之六七十的自动化需求其实是可以交给 JMeter 的剩下那部分才是 UI 层的活。1.3 我通常采用的三层方案结构落到具体实施上我会把整个自动化体系分成三层每层有独立的触发频率和判定标准。第一层是冒烟层只覆盖核心链路的 5 到 10 条正向用例比如登录、下单、查询订单。它要求三分钟内跑完每次代码合并就触发一次失败立刻阻断。第二层是回归层覆盖全部接口的正向、异常、边界用例通常几百到几千条每晚跑一次第二天早上看报告。第三层是压测层按业务场景组织比如首页浏览 登录 下单的混合场景按需触发通常跟着版本发布走。这三层共用同一套 HTTP 请求配置元件和同一份环境配置但脚本文件、线程组参数、执行入口完全分开。分层之后有个很实际的好处新人加用例的时候知道自己该往哪放不会一股脑全塞进一个文件里。1.4 和代码级框架怎么分工经常有人问我公司里已经有 Pytest 或者 Java 的接口框架了还要不要上 JMeter我的看法是两者定位不同。代码级框架强在复杂数据构造、复杂断言、和业务代码共用工具类适合写那些需要精细逻辑的用例JMeter 强在协议层能力全面、并发模型简单直观、用例可视化程度高适合大批量的接口回归和性能场景。实际搞法上我会让两者各管一段数据准备和复杂校验用代码框架做跑完把结果落到数据库JMeter 负责发起请求和读取校验结果。两边通过一个共享的数据表衔接谁也不侵入谁。这种做法听起来有点笨但胜在两边团队可以并行推进不用互相等接口。2. 把 .jmx 从桌面文件变成可维护的工程资产脚本能跑通只是起点。真正决定一套 JMeter 方案能不能活过半年的是它的工程化程度。我接手过最糟糕的一个仓库二十多个 .jmx 文件堆在根目录文件名分别叫 1.jmx、测试.jmx、新建文件夹里的最终版.jmx打开一看每个人都在里面存了绝对路径换台机器全军覆没。2.1 目录结构与命名约定这是我目前比较顺手的一套目录约定可以直接拿去改jmeter-suite/ ├── cases/ # 用例脚本按业务模块分目录 │ ├── smoke/ # 冒烟用例 │ │ ├── user_login.jmx │ │ └── order_create.jmx │ ├── regression/ │ │ ├── user_module.jmx │ │ └── order_module.jmx │ └── perf/ │ └── home_page_mix.jmx ├── data/ # 参数文件按环境模块组织 │ ├── dev/ │ ├── test/ │ └── prod/ ├── config/ # 环境属性文件 │ ├── dev.properties │ ├── test.properties │ ── prod.properties ├── lib/ # 第三方 jar 与插件 ├── scripts/ # 启动脚本 │ ├── run-smoke.sh │ ├── run-regression.sh │ └── run-perf.sh └── reports/ # 输出目录不进版本库命名上我坚持两条脚本文件名用英文小写下划线业务模块名在前、动作在后测试计划里的元件名称必须写清楚业务含义不能出现HTTP请求1这种。原因很朴素测试报告里失败项显示的就是元件名如果元件名叫 HTTP请求1你早上看报告的时候根本不知道挂了哪条业务。2.2 参数外置-J、-G、-p 到底怎么选JMeter 有不止一种参数注入方式很多人用混了导致换环境的时候各种找不到变量。我把它捋一下-p xxx.properties用来指定属性文件里面的值通过__P(属性名)或者${__P(属性名,默认值)}读取。这个方式适合放环境相关的一大批配置比如 host、port、数据库连接串、超时时间。-J参数名值是命令行里传 JMeter 属性效果等价于往属性文件里加一行优先级更高适合临时覆盖某个值比如-Jthreads200。脚本里用${__P(threads)}读。-G参数名值也是传属性但它会被分发到分布式压测的所有节点上这点非常关键本地单机跑的时候你可能感觉不到差别一旦上了多台执行节点用 -J 传的参数只有主控节点知道从节点读到的还是默认值脚本里面就会出现有的线程用对了参数、有的线程用了默认值这种诡异现象。我当时排查这个问题花了整整一个下午。配置里我会写默认值兜底比如${__P(base_url,http://127.0.0.1:8080)}。这样即使属性文件传漏了脚本也不会直接崩而是跑到一个明显不对的地址上——至少报错信息能看懂。2.3 环境切换的三种做法对比做法优点缺点适用场景每个环境一份 .jmx直观改哪个一眼能看出来脚本会分叉改一处要改三份临时验证不建议长期属性文件 __P()一份脚本跑所有环境切换成本低需要约定好变量名新人容易漏配主推方案用户定义的变量 配置元件组合局部覆盖方便层次多了之后不好追踪取值顺序配合属性文件做局部覆盖我目前的实践是属性文件管环境级配置用户定义的变量管模块级默认值命令行 -J 管临时覆盖。三层优先级从低到高取值顺序清楚排错的时候照着往里查就行。提示JMeter 的变量替换是能取到就替换取不到就原样输出。也就是说如果你写了${token}但上游没提取到请求里发出去的真的就是字符${token}服务器可能返回 400而你盯着断言看半天想不通为什么。所以断言里加一条响应不包含 ${ 往往能救你半条命。2.4 版本库里该提交什么这个事我吃过亏所以有明确清单。要提交的.jmx 脚本、参数文件脱敏后、属性文件模板、启动脚本、lib 目录里必要的第三方插件 jar或者至少记录版本号。不要提交的结果文件 .jtl、HTML 报告目录、日志文件、任何包含真实账号密码的文件、jmeter.log。账号密码这块我倾向于放在环境变量或者 CI 平台的密钥管理里脚本里通过${__P(db_password)}读属性文件里写占位符。你可能会觉得内部环境不用这么讲究但一旦测试环境被外部人扫到日志里的明文密码就是实打实的风险。2.5 一个容易忽略的点脚本原子化模块拆分这件事值得单独说。我习惯把登录并获取 Token做成一个独立的 .jmx 或者一个 Test Fragment其他用例通过 Include Controller 或者 Module Controller 引用它。这样做的直接好处是登录接口改了我只改一处所有用例都跟着变间接好处是单条用例的意图变得非常清晰评审的时候一眼能看出这条用例在验什么。代价是脚本之间的依赖关系变复杂了需要有人维护一张引用关系表。我的做法是在仓库根目录放一个 README用表格列出每个模块被哪些脚本引用改之前先看一眼。3. 参数化四种取值路子怎么选各自埋着什么雷参数化是 JMeter 用例的真实工作量所在。我统计过自己写过的脚本真正花时间的地方八成都在参数化上。原因很简单请求报文本身几分钟就能搭好难的是让每条用例拿到不同的、有意义的数据而且这些数据在并发场景下不能互相打架。3.1 CSV Data Set Config 的 Sharing mode 决定一切CSV 参数化是最常用的方式但它的行为几乎全由两个配置项决定很多人是盲选的。Sharing mode共享模式有四个选项含义差别很大模式含义使用建议All threads所有线程共享同一个文件指针谁先取谁先用要求数据不重复时用这个Current thread group每个线程组各有一份独立的指针多线程组跑不同业务时用Current thread每个线程各有一份指针会从头开始读想让每个用户都从第一行开始用这个Identifier按标识符共享最灵活也最容易配错复杂场景才需要这里有个特别容易踩的坑如果你选了 Current thread 模式同时把 Recycle on EOF 设成 true那么每个线程都会独立地把整个文件从头到尾读一遍。听起来没毛病但如果你的用例是每个用户只能用一次优惠券这类业务就会出现所有线程都在用第一行数据的情况测出来的结果完全是假的。另一个坑是线程数大于数据行数。假设 CSV 里有 50 行数据你开了 100 个线程如果 Recycle on EOF 是 true第二个 50 线程会回头再读一遍如果是 false剩下的线程会拿到 EOF 并且参数为空请求直接发了个空值出去。这两种行为你必须明确知道自己要哪一种并且在脚本里做对应的处理。注意CSV 文件的编码问题常年高发。Windows 上用 Excel 保存的 CSV 默认是 GBKJMeter 默认按 UTF-8 读取中文参数就会变成乱码。处理办法是保存的时候明确选 UTF-8或者在 CSV Data Set Config 里把 File encoding 显式写上 UTF-8。3.2 JDBC Request 取值连接池和密码管理当参数需要从数据库里捞的时候JDBC Request 就派上用场了。配置上有几步不能省JDBC Connection Configuration 里要设好连接池的最大连接数、空闲回收时间、事务隔离级别SQL 语句里如果要引用变量用${变量名}但要选对 Query Type带参数的用 Prepared Select Statement否则变量替换可能不生效。这里有个细节值得说JDBC 返回的结果是数组形式取值要用${变量名_1}这种带下标的写法下标从 1 开始不是从 0。我见过有人写成${变量名_0}取出来一直是空查了一整天。密码管理上我从来不在 JDBC Connection Configuration 里写死密码而是走属性文件或者 CI 的密钥变量。另外连接池大小要跟线程数匹配我一般设成线程数的 20% 到 30%太少会出现等待连接的超时太多会把数据库连接数占满影响其他测试。3.3 函数助手随机与时间类参数的用法有些参数不需要真实数据只需要格式正确且不重复这时候函数助手最省事。__Random(最小值,最大值,变量名)生成随机整数适合订单号后缀、金额等。__RandomString(长度,字符集,变量名)生成随机字符串适合用户名、备注字段。__time(格式,变量名)生成时间戳__time(yyyyMMddHHmmss)这种写法在构造唯一单号时特别好用。__UUID()生成唯一标识适合需要全局唯一且不关心格式的场景。__threadNum返回当前线程编号配合${__threadNum}可以做每个线程用不同数据的效果比 CSV 更轻量。有个细节函数助手里生成的变量如果要在多个线程之间区分用${__threadNum}组合${__time()}是最稳的比如${__time(yyyyMMddHHmmss)}_${__threadNum}。这个组合我用了很多年几乎没出现过重复。3.4 关联参数JSON Extractor、正则、XPath 怎么选接口有依赖关系时就要从上一个响应里把值抠出来。JSON Extractor是我现在的首选JSONPath 语法直观$.data.token这种写法一眼能看懂而且支持多值提取。前提是响应确实是合法 JSON。正则表达式提取器兜底能力最强响应不是标准 JSON 或者格式比较脏的时候只能靠它。代价是模板和匹配规则写起来别扭(.*?)这种非贪婪匹配用错了会提取到一大段多余内容调试起来很痛苦。XPath Extractor适合响应是 XML 或者 HTML 的场景现在用得不多了。提取的时候我会顺手加上默认值比如token_not_found这样一旦提取失败下游请求就会带着这个明显异常的值发出去断言很容易命中比默默传个空字符串好排查得多。3.5 参数化翻车实录讲几个我真实遇到过的第一个是作用域问题。有个同事在 HTTP 请求下面挂了一个正则提取器然后在另一个线程组的请求里引用这个变量结果取不到。原因是 JMeter 的变量作用域是线程级的跨线程组要用属性__setProperty/__property传递。这个坑很经典记住一句话变量是线程私有属性是全局共享。第二个是取样器顺序。JMeter 默认按元件在树里的顺序执行提取器必须挂在被提取请求的子节点下且执行顺序要保证先请求后提取。有人把提取器拖到了请求外面看起来界面乱糟糟的执行结果自然不对。第三个是参数文件的行尾空格。CSV 最后一列后面多了一个空格导致拼出来的 SQL 多了个空格在某个数据库上直接报语法错误。排查的时候是打印了实际执行的语句才发现的。后来我养成了一个习惯参数化之后先打一次日志把实际参数值输出来看一眼。4. 断言怎么写才不脆从响应码 200到真正的业务校验断言是自动化测试的灵魂也是最容易被敷衍的部分。一个只有响应码 200的断言本质上只是验证了服务器还活着。4.1 三类断言各自管什么响应断言是最基础的可以校验响应文本包含什么、匹配什么、响应码等于什么、响应头里有什么。它的优势是通用缺点是只能做字符串级比对遇到字段存在但值不对这种情况就不够用了。JSON 断言专门处理 JSON 响应可以直接判断某个 JSONPath 的值是否等于预期、是否为空、是否是数组。我现在的接口用例里业务字段校验基本都用它因为可读性远好于正则。持续时间断言用来卡响应时间。这个断言要慎用因为响应时间受环境影响很大写得太严会导致脚本在开发和测试环境之间来回误报。我的做法是只在冒烟层设一个宽松上限比如 3 秒真正的时间指标交给性能报告去看。三个断言可以叠加而且叠加的时候要注意作用域。断言挂在测试计划上会对所有请求生效挂在某个请求下只对它生效我给你一个经验业务断言就挂在对应的请求下通用的响应码校验可以提到线程组级别这样既不会漏也不会互相干扰。4.2 用 JSR223 替代 BeanShell 的理由老脚本里大量使用 BeanShell 断言但新脚本我一律用 JSR223 Groovy。原因很实际BeanShell 是解释执行性能差在几百个线程的压测里会成为瓶颈Groovy 有编译缓存同样的逻辑执行开销小得多而且语法更接近现代编程语言团队里其他人看得懂。一段典型的业务断言长这样import groovy.json.JsonSlurper def resp prev.getResponseDataAsString() if (resp null || resp.trim().length() 0) { AssertionResult.setFailure(true) AssertionResult.setFailureMessage(响应体为空) return } def json new JsonSlurper().parseText(resp) if (json.code ! 0) { AssertionResult.setFailure(true) AssertionResult.setFailureMessage(业务码异常 json.code 响应 resp) return } if (json.data null || json.data.orderId null) { AssertionResult.setFailure(true) AssertionResult.setFailureMessage(缺少订单号字段) }这段代码里有三个细节值得注意先判断响应体是否为空再做解析否则 JsonSlurper 抛异常会直接让线程报错失败信息里带上完整响应这样看报告的时候不用再去翻日志用 AssertionResult 而不是抛异常抛异常会让整个线程结束后面的用例就不跑了。4.3 断言写太死的三个反例反例一把时间戳、流水号这类每次都会变的值写进断言。结果就是脚本永远失败谁也不知道是故意失败的还是环境问题。这类字段的断言应该用存在且符合格式而不是等于某个固定值。反例二用整段响应文本做完全匹配。后端加个字段、改个字断言立刻全挂维护成本极高。正确做法是只断言你真正关心的那几个字段。反例三断言失败信息里写断言失败。这种信息在生产环境排错时基本没有价值。我要求自己写的每条断言失败信息都要能回答三个问题哪个字段、期望什么、实际是什么。4.4 结果文件怎么读、报告怎么导调试阶段用察看结果树但要注意它非常吃内存正式跑的时候一定要关掉。需要保存结果的时候勾选Save Response Data和Save Request Data但这两个选项会显著增加结果文件体积压测场景下我不建议开。正式执行用-l参数输出 .jtl 结果文件跑完用-e -o生成 HTML 报告jmeter -n -t cases/regression/order_module.jmx \ -l reports/order_$(date %Y%m%d%H%M).jtl \ -e -o reports/html/order_$(date %Y%m%d%H%M) \ -p config/test.propertiesHTML 报告里的响应时间随时间变化和每秒事务数两张图是我最先看的前者看有没有毛刺后者看吞吐的连续性。失败的请求会在错误页里按类型聚合聚合成一类的话通常说明是同一个根因排查效率会高很多。5. 录制还是手写什么场景该怎么选关于脚本来源两派吵了很多年。我的态度很明确录像是起点手写是终点两者不是对立的。5.1 HTTPS 录制的完整流程录制适合的场景是接口数量多、链路长、你还没完全摸清业务交互顺序。JMeter 自带脚本录制器能够把浏览器发出的请求完整抓下来包括请求头、请求体、参数。流程大致是在测试计划里加一个 HTTP(S) 测试脚本录制器设置监听端口默认 8888指定目标控制器和分组方式然后在本机启动录制器它会在工作目录下生成一个测试用的根证书文件把这个证书导入到浏览器的信任列表里再把浏览器的网络出口指向本机 8888 端口之后在浏览器里正常操作系统所有请求都会被记录到目标控制器下。这里最关键的一步是证书导入。不导入证书的话HTTPS 请求的报文是加密的录制器只能看到一堆乱码录出来的脚本基本没用。导入之后重启浏览器再访问目标站点就不会有安全提示了。5.2 录出来的脚本必须洗一遍我从来没见过一份录出来的脚本能直接进用例库的原因有三一是静态资源混在里面图片、样式、脚本文件的请求要全部删掉二是请求头里带了一堆浏览器指纹信息比如 Accept-Encoding、User-Agent 的完整字符串、各种插件标识这些跟业务无关留着只会增加不确定性三是存在前后依赖但录制没体现出来比如第二个请求里的某段 token 是第一个响应里返回的录制下来是写死的必须改成提取器。清洗的顺序我一般是先删无用请求再统一删掉无关请求头然后处理依赖参数最后统一改域名和端口为变量。整个过程花的时间往往比直接手写还长所以对于链路短的接口我更愿意直接手写。5.3 几类特殊请求的处理方法文件上传。HTTP 请求里勾选对 POST 使用 multipart/form-data然后在文件上传页签里填文件路径和参数名。文件路径我习惯做成${__P(upload_file)}从属性文件读这样换个文件不用改脚本。另外要注意 MIME 类型要跟实际文件一致传 PDF 却写成 image/png某些服务端会直接拒绝。MVC 项目的防伪令牌。有些框架会在页面里埋一个隐藏字段作为令牌服务端校验这个值不提供就会返回 403 之类的错误。处理思路是先用一个请求获取页面然后从响应里用正则提取这个隐藏字段的值再带到后续请求里。这一步用正则提取器就能搞定关键是要保证提取和提交的会话一致也就是 Cookie 管理器必须正常工作。RESTful 路径参数。这类接口的参数在 URL 路径里比如/orders/{id}处理方式是直接把路径里的占位符换成变量${orderId}不需要额外配置。要小心的是有些路径参数包含特殊字符需要做 URL 编码JMeter 默认会自己编码重复编码会导致 404遇到这种问题可以打开请求里的编码选项逐个验证。MQTT 之类的自定义协议。这类场景需要额外安装插件把插件 jar 放进 lib/ext 目录重启 JMeter 才能在取样器列表里看到。插件版本一定要和 JMeter 主版本匹配版本不对会直接启动失败日志里能看到类加载异常。我建议把插件版本记录在仓库的 README 里避免换人之后一脸懵。5.4 一条完整的端到端用例长什么样拿一个电商下单链路举例我会这样组织第一登录请求从 CSV 取用户名密码提取响应里的 token 存为变量。第二查询商品请求带上 token提取商品 ID 和库存版本号。第三加入购物车请求带上商品 ID提取购物车条目 ID。第四提交订单请求带上购物车条目 ID 和收货地址提取订单号。第五订单查询请求用订单号回查做最终的状态断言。这五个请求串在一个事务控制器里事务控制器的作用是把它们合并成一个下单事务这样报告里看到的响应时间是整条链路的总耗时而不是五个独立的数字。整条链路跑通之后再给它加上一个 CSV 参数化就能批量跑不同用户的下单流程了。6. 接进流水线非 GUI 执行才是自动化的真正形态界面里点来点去的脚本本质上还是手工测试。只有能无人值守执行才算迈进自动化的门。6.1 非 GUI 命令的常用参数jmeter -n \ -t cases/smoke/user_login.jmx \ -p config/test.properties \ -Jthreads10 -Jrampup5 -Jloops1 \ -l reports/result.jtl \ -e -o reports/html \ -j reports/jmeter.log-n表示非 GUI 模式这是流水线里的必备项因为 GUI 模式会额外消耗大量内存并且不适合长时间运行。-t指定脚本-p指定属性文件-J传自定义属性-l指定结果文件-e -o生成 HTML 报告-j指定日志文件位置。注意-o指定的目录必须是空的或者不存在的如果上一轮的报告还留在那里执行会直接报错退出。这个坑我踩过不止一次后来在启动脚本里加了一行清理命令才解决。6.2 失败怎么判定和阻断JMeter 默认情况下即使有用例失败进程退出码也是 0这在流水线里是灾难——测试全挂了流水线还显示绿的。解决办法有两个一是在测试计划里勾选失败时停止测试Fail on failure另一种更通用的是用 Shell 脚本解析 .jtl 里的失败条数fail_count$(grep -c sfalse reports/result.jtl) if [ $fail_count -gt 0 ]; then echo 存在失败用例$fail_count 条 exit 1 fi这个判断逻辑简单粗暴但非常有效我几乎每个项目都会加。更精细一点的做法是区分错误类型比如把连接超时和业务断言失败分开统计前者可能是环境问题不该阻断合并后者才是必须处理的。6.3 定时任务与流水线的接入方式冒烟层我会绑在代码合并事件上用流水线的构建触发器实现跑完立刻出结果失败就阻断合并。回归层用定时任务每天凌晨跑一次早上把 HTML 报告链接发到群里。压测层手动触发一般在版本发布前或者有性能相关改动时执行。这里有个细节值得提报告归档。HTML 报告里的图片和样式是相对路径引用的如果只归档一个 index.html别人打开就是白屏。我一般用压缩包整体归档或者直接把报告目录传到一个静态服务上给链接。6.4 多机执行时要注意的事当单机压不出目标并发时就会用到多台执行节点。这里有几个坑必须提前知道。第一参数一致性前面提过的 -G 和 -J 的差别在这里会要人命所有需要被各节点读到的参数都必须用 -G 传或者写进每个节点都能访问的属性文件。第二参数文件要分发CSV 文件必须存在于每台执行节点上路径还要一致否则会报文件找不到。第三结果汇总每个节点产生自己的结果文件需要汇总到主控节点再生成报告。第四时钟同步各节点时间不一致会导致报告里的时间轴错乱看起来像是有请求集中在某一秒爆发。还有一点是不要把执行节点和被测服务部署在同一台机器上。我见过最离谱的一次压测机和被测服务跑在一台 4 核 8G 的虚机上测出来的 TPS 只有实际值的十分之一大家还以为是系统有性能问题排查了整整两天。7. 性能场景设计并发、加压节奏和观测点性能测试这块脚本只是工具真正决定结果价值的是场景设计。7.1 线程数、Ramp-up、循环次数的三角关系这三个参数经常被搞混。线程数是并发用户数Ramp-up是启动这些线程用的总秒数循环次数是每个线程执行多少轮。它们的关系是如果线程数 100、Ramp-up 10 秒那么平均每秒启动 10 个线程如果循环次数是 10那总请求数是 100 × 10 1000 次具体多久跑完取决于响应时间。很多人把 Ramp-up 设成 0 或者 1觉得这样才是同时并发。实际效果是这 100 个线程在极短时间内全部启动对被测系统来说是一个非常陡峭的冲击容易触发限流或者瞬间打满连接池。除非你就是要测这个极端场景否则 Ramp-up 至少要设成线程数的十分之一往上让加压过程平滑一些。循环次数建议用永远加上调度器的持续时间来控制这样测试时长是确定的报告也比较容易横向对比。7.2 阶梯加压怎么找拐点我判断系统容量最常用的方法是阶梯加压。做法是用一个线程组配合插件里的阶梯线程组把并发从 50 开始每 60 秒增加 50一直加到 500同时观察 TPS 和响应时间曲线。理想情况下并发增加时 TPS 会同步上升响应时间基本持平到了某个点TPS 不再上升甚至开始下降响应时间明显抬头这个点就是拐点。拐点之前的并发量可以作为容量参考。这里有个经验观察拐点要看趋势不要看单点。某一秒的响应时间突然飙高可能是垃圾回收或者外部调用抖动需要结合多轮测试确认。我一般至少跑三轮取稳定重现的那个值。7.3 压测里最常见的几种误判第一种把压测机当成被测系统。压测过程中要盯着压测机自己的 CPU、内存、网络如果压测机 CPU 已经打满那测出来的数据反映的是压测机的瓶颈不是被测系统的。JMeter 官方建议压测机的 CPU 使用率不要超过 80%。第二种用功能测试的数据跑压测。功能用例里通常有各种校验和断言这些都消耗压测机资源。压测脚本应该尽量精简把断言关掉或者只保留最关键的几条。第三种忽视数据准备。压测过程中如果参数是重复的被测系统的缓存命中率会异常高测出来的性能比真实情况好很多。正确的做法是准备足够量的参数数据让请求真正打到不同的数据分支上。第四种看平均值不看分位数。平均响应时间 200 毫秒听起来不错但如果 90 分位是 2 秒用户的真实体验就是有一部分请求非常慢。报告里我一定会看 90% 和 95% 分位。8. 排错地图这些年我在 JMeter 上踩过的坑这一章我列一些具体的报错和排查思路都是我实际遇到过并且印象深刻的。8.1 error writing to server 这类 IO 异常这个报错信息通常伴随java.io.IOException提示写入服务器失败。常见原因有连接被服务端提前关闭、请求体过大、超时设置过短、网络不稳定。排查顺序是先从服务端日志确认是否有对应时间点的连接重置记录然后检查 JMeter 的 connect timeout 和 response timeout 是否设得太小最后看请求体大小是否超过了服务端的限制。我遇到过一次是服务端配置了空闲连接回收时间 30 秒而 JMeter 的连接复用导致复用了已经被服务端关掉的连接于是下一笔请求就报了这个错。解决办法是在 HTTP 请求默认值里把使用 KeepAlive关掉或者缩短连接复用时间。8.2 提示文件已存在导致执行中断非 GUI 模式生成报告时如果目标目录里已经有东西JMeter 会直接报错退出。这在流水线里特别常见因为上一轮的产物没被清理。处理方式是在执行前先删除目标目录或者在目录名里加上时间戳。我用的是后者因为保留了历史报告还能做趋势对比。8.3 结果树里导出数据后打不开察看结果树支持把结果保存成 CSV 或 XML但要注意保存的字段。如果只保存了响应码没保存响应体后续想分析业务失败原因就没数据可用了。我一般的做法是调试阶段保存全部字段正式执行时通过命令行输出 .jtl需要看细节的时候再单独复现某条失败的用例。8.4 界面字体太小看不清这个属于用户体验问题但确实影响效率。JMeter 的界面字体可以在主题属性文件里调整找到对应主题的 properties 文件修改字体相关的配置项后重启生效。另外在高分屏上如果觉得模糊可以调整系统的缩放设置或者 Java 的启动参数。这小东西不值得单独开一章但每天都要盯着看的工具调顺手了效率差别不小。8.5 大数据量参数文件的性能问题参数文件超过几万行的时候CSV Data Set Config 会成为瓶颈因为每次读取都要做字符串分割。我一般控制在几千行以内需要更大数据量的时候改用 JDBC 取值或者把数据分批加载。另一个办法是用每次迭代取一行的模式配合循环控制器减少单次读取的数据量。8.6 关于脚本维护的一点经验最后说个不是技术问题的技术问题。自动化脚本的失败率如果长期高于 20%这套体系基本就废了因为没人会认真看报告。降低误报的关键在两点一是断言要聚焦业务字段而不是格式细节二是环境相关的东西全部外置成变量。我给团队定的规则是每条用例只有在自己确实在验证某个行为时才保留如果一条用例只是因为顺手加的而存在且经常误报直接删掉。用例数量不是指标通过率和有效性才是。另外每次因为环境问题或者被测系统改动导致的脚本调整都要在提交信息里写清楚原因。半年之后你回头看那些提交信息就是这套方案最好的文档。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →