ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Gatling 3.0.0 压测实战:从JMeter迁移到Scala DSL的避坑指南

Gatling 3.0.0 压测实战:从JMeter迁移到Scala DSL的避坑指南 简介Gatling 3.0.0是一款面向现代Web应用的开源性能测试工具主要供开发人员与测试工程师模拟高并发访问、验证系统稳定性、定位性能瓶颈该压缩包内置完整运行环境共114个文件涵盖97个jar依赖含Scala编译器及常用库、6个scala脚本、3个conf配置文件和Windows/Linux下的bat/sh启动脚本整体约57.17MB解压后即可通过命令行或录制器快速创建测试场景包内还附有search.csv示例数据与默认配置文件配合录制功能可让新手在几分钟内生成首个脚本。已有233人学习下载该版本基于Scala DSL书写场景具备出色的可读性与可维护性并能生成Highcharts交互式图表报告直观呈现RPS、平均响应时间、百分位数等关键指标测试过程中支持实时监控结束后保留请求与响应日志方便快速定位异常请求同时支持与Jenkins、Travis CI等持续集成工具结合在每次代码提交后自动执行性能回归确保线上系统的稳定性。对需要将压力测试嵌入研发流程的团队来说这份开箱即用的发行包具有很高实用价值。1. gatling3.0.0从 JMeter 过来的人最容易在哪翻车gatling3.0.0 是我切换压测工具链的一个分水岭。很多从 JMeter 转过来的人第一反应是把它当成一个录制回放工具结果在 Simulation 类里反复翻车脚本写成了“一步一步的用例”一跑就报找不到请求或者并发数据完全不符合预期。Gatling 的本质是用 Scala DSL 把虚拟用户的行为仿真建出来每次执行都是一次仿真而不是一条测试用例。适合你的是接口/微服务压测、想把压测基线固定进 CI、不想再手工整理报表如果只想要一个黑匣子录制工具那它可以先放一放。下面按“原理 → 最小工程 → 报表调试 → 避坑 → 进阶”一步步讲透。2. 为什么「把压测写成代码」是 gatling3.0.0 的核心竞争力2.1 三次选择为什么是 Scala DSL而不是 XML/JSON 场景文件常见工具里用 XML 描述压测场景是最早的方案JMeter 的.jmx就是这种思路的代表。界面录完直接能看到树形结构对一个临时压测来说很友好。但场景一旦出现条件判断、循环、随机等待、从响应里提取数据、按顺序消费一批账号XML 就开始膨胀。一个登录态查询链路拉到上百个节点不是什么稀罕事Git diff 基本没法看。gatling3.0.0 选择的是代码优先。场景被写成 Scala 对象运行时被“编译”成一颗虚拟用户行为树每个节点代表一次请求、一次等待、一个循环或者一段数据操作。这颗行为树是静态构建出来的但每个节点内部的表达式是在虚拟用户执行时动态求值。这一层理解非常重要你在脚本里写一个字符串 URL它可能在构建期就被固定成了字面量你写一个${token}表达式它会在每个用户执行到该节点时才去 Session 里取值。对 Java 团队来说额外学一点 Scala 语法会有心理门槛但实际用到的部分非常窄链式调用、闭包、字符串插值再加一点集合操作就够了。3.0.0 的 DSL 是强类型的写错方法名或类型不匹配编译期直接红比跑到压测到一半才发现配置错了要省事得多。这也是我坚持在团队里推它而不是继续用录制工具的核心原因脚本要能 review、能版本化、能交给没参与录制的人改。2.2 六个核心抽象Scenario、HTTP、Checks、Session、Feeder、Assertion在 gatling3.0.0 里所有概念都围绕六个抽象转。第一次接触时不需要背定义把它们的协作关系看懂就行。抽象职责常见误解Scenario定义虚拟用户完整的行为链以为是一条“测试用例”HTTP描述一次请求的方法、路径、Header、Body以为只是一个请求发送器Checks校验响应内容并从响应中提取数据以为只是断言忽略 saveAsSession单个虚拟用户私有的数据容器以为可以用全局变量跨用户传数据Feeder批量向 Session 注入测试数据以为是一次性数组不管队列策略Assertion压测结束后的全局判定以为和 Check 是一回事关键在 Session 这一层。每个虚拟用户启动时有一个独立的 SessionFeeder 往里面放账密、商品 ID 之类的外部数据Check 通过saveAs把登录返回的 token 存进去后面的请求再用${token}从 Session 里取值。这套数据流是所有脚本的主干也是很多迁移者最容易写出错误代码的地方他们习惯在类里定义一个var token 然后在多个 exec 之间赋值。还有个容易忽略的点是 Assertion 和 Check 的分工。Check 管的是“这一次响应里有没有我要的东西”Assertion 管的是“整轮压测结束后全局指标是否达标”。前者没通过单个请求标 KO后者没通过整个gatling:test任务退出码非零。做回归时Assertion 才是那道门禁。2.3 与 JMeter 对比哪些团队该换到 gatling3.0.0这里只谈选型判断不给工具排座次。如果你面对的是一个要长期维护、每周都跑的接口压测Gatling 的优势非常明显脚本是纯代码改动点能被 review 和 diff报表自带 HTML 聚合页JMeter 则需要自己配聚合报告组件并发模型上JMeter 一个线程模拟一个用户Gatling 走异步引擎同样一台压测机能承载的虚拟用户数量通常更高。反过来如果团队只是要“今天下午压一下看看顶不顶得住”JMeter 的录制上手速度仍然是优势这时候迁移是不划算的。换工具永远有隐性成本脚本重写、组员学习、历史报告口径变化。我一般会建议先用一个小接口做两周试跑把线上回归报告和 CI 门禁跑顺了再决定要不要全量迁移。试跑期间重点观察的不是压测数值而是脚本维护成本每次需求变化后改 Gatling 脚本的时间是不是明显小于改 JMeter 脚本的时间。3. 用 gatling3.0.0 跑通第一个最小仿真工程结构与注入模型3.1 最小工程结构Maven 插件 src/test/scala 约定常见做法是用 Maven 插件拉起整个压测生命周期。你不需要手写独立的 Scala 编译和运行命令mvn gatling:test会完成编译、运行、生成报告三步。最小工程结构长这样src test scala LoginThenQuerySimulation.scala resources data users.csvresources/data是 Gatling 约定给 Feeder 放数据文件的位置resources/bodies放请求模板resources/conf放自定义配置。Simulation 类必须放在src/test/scala下插件默认从这个目录扫描。pom.xml 里只需要一个插件plugin groupIdio.gatling/groupId artifactIdgatling-maven-plugin/artifactId !-- 版本号与工程使用的 Gatling 运行时保持一致我一般用占位符统一管理 -- version${gatling.version}/version /plugin逻辑说明插件本身不做压测它负责把src/test/scala里的 Simulation 编译并执行。参数说明${gatling.version}建议在properties里统一声明这样升级 Gatling 时只改一处如果你直接用固定版本也要保证和核心依赖一致否则会出现 DSL 方法找不到的编译错误。首次跑通前不要急着配各种 JVM 参数先确认默认配置能出报告。3.2 注入模型怎么写rampUsers 与 constantUsersPerSec 的差别setUp里的inject是所有压测参数的入口也是新手误解最多的地方。看这段setUp( scn.inject( rampUsers(100).during(30.seconds), constantUsersPerSec(10).during(2.minutes) ) ).protocols(httpConf)逻辑说明rampUsers(100).during(30.seconds)表示在 30 秒内平滑启动 100 个虚拟用户而不是“每秒 100 并发”constantUsersPerSec(10).during(2.minutes)表示在这 2 分钟里每秒钟新启动 10 个用户持续向系统施压。两者可以组合使用模拟“先开门放量再稳定持续”的流量形态。参数说明里最容易踩的坑是把rampUsers(100)理解为固定 100 并发。实际上它只控制“启动多少用户、用多久启动完”在线用户数是一条先升后降的曲线。要模拟固定在线数常见的做法是用constantUsersPerSec配合较长的持续时间让在线用户数进入稳态区间。下面是三种最常用的注入写法写法含义适用场景atOnceUsers(n)一次性启动 n 个用户瞬时报文、秒杀摸底rampUsers(n).during(t)t 时间内均匀启动 n 个用户模拟早高峰流量爬坡constantUsersPerSec(n).during(t)每秒恒定新增 n 个用户稳定性测试、长时间浸泡3.3 一个可抄作业的登录业务查询场景下面这段是完整可跑的 Simulation你也可以直接复制改成自己的接口import io.gatling.core.Predef._ import io.gatling.http.Predef._ import scala.concurrent.duration._ class LoginThenQuerySimulation extends Simulation { val users csv(data/users.csv).circular val httpConf http .baseUrl(System.getProperty(base.url, http://127.0.0.1:8080)) .acceptHeader(application/json) val scn scenario(登录后查询库存) .feed(users) .exec(http(登录) .post(/login) .basicAuth(${username}, ${password}) .check(status.is(200), jsonPath($.token).saveAs(token))) .exec(http(查询库存) .get(/stock) .queryParam(sku, ${sku}) .header(Authorization, Bearer ${token}) .check(jsonPath($.items).exists)) .pause(1, 3) setUp( scn.inject(rampUsers(50).during(20.seconds)) ).protocols(httpConf) }逻辑说明feed(users)在虚拟用户启动时从 CSV 里取一行数据把username、password、sku三个字段写进 Session登录请求用basicAuth直接消费 Session 里的账密返回后把token通过jsonPath($.token).saveAs(token)存回 Session查询库存的请求再用${token}和${sku}动态取值。.pause(1, 3)表示两次请求之间随机等 1 到 3 秒模拟真实用户思考时间。运行命令mvn gatling:test -Dbase.urlhttp://127.0.0.1:8080参数说明-Dbase.url会覆盖脚本里System.getProperty的默认值换环境不用改代码。csv(data/users.csv)的路径相对src/test/resourcesCSV 第一行必须是列名顺序无关但列名要和${}里的字段名保持一致。.circular让数据循环使用避免用户数超过 CSV 行数后 Session 取不到值。提示如果你只是跑通一个 GET 接口可以把basicAuth和jsonPath相关部分全部去掉只保留http(请求).get(/health)最小案例能出报告后再逐步加链路。4. 把报表当调试工具gatling3.0.0 结果怎么看、怎么定位4.1 三个先看的指标并发用户曲线、响应时间分位、错误率Gatling 的 HTML 报告信息密度很高但打开后别急着看平均值。我一般先看三块响应时间分布、在线用户曲线、错误分布。指标看哪里常见坑响应时间分位Response Time Distribution平均响应时间会掩盖 p99 的尖刺在线用户曲线Active Users 图用它反推注入模型是否如预期错误率与状态码Global Information / StatusKO 率不是 0 不代表服务端有问题响应时间只看 mean 是典型的自欺欺人。某个接口 p50 只有 200ms但 p99 到了 3 秒对用户来说就是明显的卡顿。Gatling 报告默认给出 p50、p75、p95、p99p95 才是压测回归里最常盯的线。在线用户曲线用来验证注入模型如果设了rampUsers(100).during(30.seconds)曲线应该是一条平滑爬升再回落的弧线如果曲线出现锯齿或尖峰通常是脚本里的pause设置不合理或者用户数超过了对端连接池容量。4.2 从 Simulation 日志反查慢请求一次 TPS 掉崖的排查过程一次压测中TPS 前段 2000跑到第 3 分钟掉到 700p95 从 800ms 爬到 3 秒按顺序做三步排查。第一步先看 KO 请求grep KO target/gatling/*.log | head -n 20逻辑说明Gatling 的 log 文件按时间戳生成在target/gatling/下KO 行会记录失败请求对应的 Check 或异常信息。这一步能快速区分“服务端返回错误”和“脚本断言不通过”。如果 KO 里全是连接超时方向上先怀疑网络或连接池如果没有 KO说明每个请求都成功问题在别处。第二步看在线用户曲线是否出现堆积。那次压测前 2 分钟曲线正常第 3 分钟曲线大幅抬高请求全部排队。原因是脚本里有人把.pause(1, 3)改成了.pause(300.milliseconds)虚拟用户提交请求的密度瞬间变大服务端没变慢是压测脚本自己把漏斗口收窄了。改成随机等待后TPS 曲线恢复平稳。第三步检查压测机资源。如果压测机 CPU 跑满或者 GC 频繁TPS 掉崖是压测机本身在抖动不是服务端问题。这个坑很隐蔽报告看起来像是“服务端从第 3 分钟开始扛不住”实际上是被测系统什么都没发生。4.3 报表里最容易误读的三个地方第一个是“单个请求的响应时间”不等于“用户体感响应时间”。Gatling 报告把每个 HTTP 请求拆开统计如果一个页面有 5 个请求用户体感是最慢那个请求决定的。做容量评估时看端到端场景的时间分布不要只看单个请求。第二个是吞吐量掉的瞬间别急着下“服务端瓶颈”的结论。要先看是请求从哪一层开始变慢如果压测机和被测服务在同一台物理机上资源竞争会让数据完全失真。压测环境至少要保证压测机独立部署。第三个是断言失败不等于接口错误。global.responseTime.max.lt(1000)这类断言在样本很少时波动很大只跑了一个 30 秒小流量就断言失败很可能是阈值设置没留余量。先看样本量和趋势再决定是改脚本还是改阈值。提示压测报告要当调试工具用不要当最终结论。报告说“服务端慢”时先去日志里找对应时间点的报错再结合在线曲线下结论。5. gatling3.0.0 常见问题避坑闭包、Feeder 与资源泄漏5.1 踩坑 1用全局变量给用户编号结果编号全部错乱现象为了给每个虚拟用户生成唯一编号在 Simulation 类里定义了一个var counter 0然后在 exec 里counter 1后拼到请求路径里。压测跑起来后请求里的编号随机跳变同一个编号会被多个用户重复使用日志完全对不上。原因所有虚拟用户共享同一个 JVM 进程里的这个可变变量并发执行时相互覆写而且 Gatling 的 exec 块是运行时求值的counter的值无法在每个 Session 里隔离。解决编号要走 Session 或 Feeder不要用外层可变变量。最简单的做法是在 CSV 里放一列用户编号用 Feeder 注入val users csv(data/users.csv).circular scenario(带编号请求) .feed(users) .exec(http(请求).get(/v1/users/${userId}))逻辑说明${userId}从 Session 里取当前用户的编号不同虚拟用户拿到不同值这正是 Session 存在的意义。参数说明feed必须在请求之前否则第一次请求时 Session 里还取不到对应字段。5.2 踩坑 2Feeder 数据被拉空后半段全是空用户名现象压测开始阶段一切正常跑到后半段请求开始大量返回 400/401日志里用户名为空或密码为空。原因csv(data/users.csv)默认是顺序队列每个虚拟用户消费一行数据数据被读完后后续虚拟用户取不到值Session 里的字段变成空字符串。数据行数小于虚拟用户总数时这个坑几乎必现。解决根据数据性质选择队列策略。csv(...).circular循环复用数据适合账密和商品 ID 这种可重复使用的数据csv(...).random随机取一行适合互相之间没有依赖的数据一码一次的券码类数据必须用默认队列并保证行数大于压测总用户数。val reusableUsers csv(data/users.csv).circular val couponCodes csv(data/coupons.csv).queue逻辑说明.queue不写就是默认顺序消费数据耗尽就不再有值。一个实用习惯是运行前先wc -l数一下数据行数和注入模型里的用户总数做对比找不到这个问题再去查服务端日志。5.3 踩坑 3BaseUrl 硬编码换环境时误压到别的环境现象脚本里写死了https://api.somehost.com团队切到预发环境跑压测时忘了改结果把流量全部打到了线上环境线上出现大量异常报警。原因baseUrl以字符串字面量形式存在代码里换环境必须改代码重新编译。人总会忘忘一次就出事。解决用系统属性注入运行时通过命令行覆盖val httpConf http .baseUrl(System.getProperty(base.url, http://127.0.0.1:8080))运行mvn gatling:test -Dbase.urlhttp://pre.env.local:8080逻辑说明System.getProperty在场景构建时读取 JVM 参数缺省时用默认值。参数说明我还会在关键请求上多加一个针对环境标识字段的 Check比如有些接口会返回env字段断言它等于当前环境名。这个习惯成本很低但能成第一批发现“打错环境”的人属于血泪经验。5.4 踩坑 4压测机自己 OOM长压测跑到一半被中断现象模拟几百并发跑 40 分钟后压测任务中断日志里出现OutOfMemoryError报告阶段看到的曲线断在了中途。原因压测进程的堆内存默认值不够Feeder 数据量大、Session 里存的响应数据多时内存会被快速吃满。压测机是台 8G 内存的机器但 Maven 插件拉起 Java 进程时只给了默认堆。解决在 Maven 插件里显式配 JVM 参数plugin groupIdio.gatling/groupId artifactIdgatling-maven-plugin/artifactId configuration jvmArgs jvmArg-Xmx4G/jvmArg jvmArg-XX:UseG1GC/jvmArg /jvmArgs /configuration /plugin逻辑说明-Xmx4G把堆上限抬到 4GUseG1GC减少长压测中的 Full GC 停顿。参数说明内存不是越大越好要留出操作系统页缓存的空间我一般给压测机物理内存的一半左右最多不超过 3/4。还要顺手把日志级别调低Gatling 的 DEBUG 日志在高并发下会吞掉大量 CPU。5.5 踩坑 5把历史报表误当本次结果排查方向带偏现象跑完一轮压测打开报表看到失败率 17%立刻开始查服务端查了半天没找到问题最后发现看的是上一次压测留下的旧报告。原因Gatling 每次运行都在target/gatling/下生成一个带时间戳的新目录旧目录不会自动清理。浏览器里如果一直停在旧标签页刷新后看到的还是旧报告。解决运行前清理旧报告目录rm -rf target/gatling/* mvn gatling:test逻辑说明只清理target/gatling不要随手mvn clean否则编译产物也被清掉重跑会多花编译时间。参数说明如果团队有归档需求可以先把旧目录移动到带日期的归档目录再跑新压测避免历史数据丢失。6. 从「能跑」到「能回归」gatling3.0.0 的三个进阶习惯脚本能跑通只是第一步真正让压测产生价值的是“每次跑完结果可以对比、可以判定、可以进流水线”。下面这三个习惯是我从多次翻车里沉淀下来的。第一个习惯是写 Assertion。压测不该靠人肉看报表判断过没过断言应该写进 Simulation 里setUp(scn.inject(rampUsers(100).during(30.seconds))) .protocols(httpConf) .assertions( global.responseTime.percentile3.lt(1000), global.successfulRequests.percent.gte(99) )逻辑说明percentile3是 p95successfulRequests.percent.gte(99)要求成功率不低于 99%。断言失败时 maven 任务退出码非零CI 可以直接标红。参数说明第一次跑时先不加断言跑几天拿到基线再把阈值设为基线的 1.2 到 1.5 倍避免阈值拍脑袋。第二个习惯是把公共行为封装成函数。登录、查询、下单这类操作会被多个场景复用不要复制整段 exec用函数收敛def queryStock(skuExpr: Expression[String]): ChainBuilder exec(http(查询库存).get(/stock).queryParam(sku, skuExpr))逻辑说明Expression[String]表示这个参数是动态表达式运行时从 Session 取值。参数说明封装时把变化点全部设计成参数比如 sku、token、断言字段公共部分只写一次后续维护成本大幅下降。第三个习惯是全量压测前先冒烟。脚本里 Feeder 数据列写错、环境不通、鉴权失败这类问题用全量跑一遍代价太大。我一般先把注入模型降到总流量的十分之一跑两分钟确认没有低级错误再改回全量跑正式回归。这个习惯看似多了一步实际省的是服务端日志和报表排查的时间。我自己接新接口压测前一定先跑一次最小冒烟再看报表里有没有意外 KO最后才信脚本的输出。压测工具本质是建模工具模型错了数字再好看也不能信。希望帮到你。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进