Gatling 3.0.0 压测工程化实战:脚本、注入模型与避坑指南
简介:Gatling 3.0.0 性能测试工具包面向开发者、测试工程师及需要验证 Web 应用高并发表现的团队,用于快速构建负载场景、执行压测并定位响应时间与吞吐量瓶颈。压缩包共 114 个文件,约 57.17MB,以 97 个 jar 依赖、6 个 scala 脚本、3 个 conf 配置及 bat/sh 启动脚本为主,涵盖运行依赖、场景脚本、参数配置和命令行入口。已有 238 人学习,适合作为性能测试入门与进阶的实操参考。包内提供可运行的 Gatling 环境,可结合 Scala DSL 编写可读性较强的测试场景,利用录制回放降低脚本创建成本,并通过 Highcharts 图表报告查看每秒请求数、平均响应时间和百分位分布;同时保留命令行执行与持续集成接入所需的基础结构,便于读者理解压测工程的组织方式、报告解读和常见问题排查。
1. gatling3.0.0 性能测试:一份能进代码仓库的压测工程
第一次把压测脚本从图形化工具里搬出来,是因为一份两百多行的聚合报告没人愿意逐行核对。gatling3.0.0 带来的变化很直接:压测脚本变成一份可编译、可 diff、可 review 的 Scala 代码,报告也不再是一堆只敢截图的数字,性能测试从此可以纳入 CI 门禁。这一版并非小修小补,Expression[T]类型、#{}与${}的语义拆分、open/closed 注入模型都在 3.0 里重新定义过,从 2.x 迁过来的脚本大概率直接编译失败。它适合两类人:需要把响应时间和错误率变成流水线卡点的后端团队,以及被图形化工具版本漂移反复坑过的同学。下面按「装起来 → 写对脚本 → 配好压力模型 → 看懂报告 → 绕开坑」的顺序拆。
2. 工程骨架:JDK、Scala 2.12 与第一个可跑的 Simulation
先把运行环境钉死。Gatling 3.0.0 这一代工具链的核心约束是 JDK 8 与 Scala 2.12,这两个条件不满足,后面所有报错都会显得莫名其妙。我在一台装了多个 JDK 的机器上吃过亏:命令行java -version指向 11,Maven 用的是另一套JAVA_HOME,结果是 Simulation 编译通过、运行阶段在序列化 Session 时抛出反射相关的异常,排查了半天才想起是 JDK 的问题。压测工具的运行环境应该和被测服务一样被当作配置管理,不该靠机器上的默认值碰运气。
2.1 JDK 与 Scala 版本对 3.0.0 的约束
3.0.0 的 HTTP 引擎基于 AsyncHttpClient,底层依赖 Netty 4.x,这套组合在 JDK 8 上是最稳的。JDK 11 上跑不是必然失败,但你会遇到一堆Illegal reflective access告警,日志被刷屏之后真正的错误反而被淹掉。我的做法是给压测工程单独配一个.sdkmanrc或者直接用容器镜像,把 JDK 锁在 8,这样换机器不会出现「我这能跑你那不能跑」。
Scala 侧的坑在于版本冲突。Gatling 3.0.0 编译产物是 Scala 2.12 的二进制格式,如果被测项目本身是 Scala 工程,且依赖里带了scala-library:2.11.x,Maven 的最近优先原则可能把 2.11 的库拉到测试类路径上,运行时表现为NoSuchMethodError或IncompatibleClassChangeError,堆栈指向 Gatling 内部,看着像是工具的 bug。实际是版本错配。
提示:压测代码放在
src/test/scala下,依赖 scope 用test,不要污染主工程的编译类路径,这是隔离 Scala 版本冲突最省事的办法。
2.2 Maven 依赖与目录骨架
Gatling 官方提供 Maven 插件,3.0.0 时代插件版本与核心库同号。下面这份 pom 片段是我常用的最小集合,gatling-charts-highcharts负责生成 HTML 报告,gatling-http提供 HTTP DSL,gatling-core是 DSL 与运行时。
<properties> <scala.version>2.12.6</scala.version> <gatling.version>3.0.0</gatling.version> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <!-- 核心 DSL:Scenario / Session / Feeder / 注入模型 --> <dependency> <groupId>io.gatling</groupId> <artifactId>gatling-core</artifactId> <version>${gatling.version}</version> <scope>test</scope> </dependency> <!-- HTTP 协议层:http() / check() / HttpProtocol --> <dependency> <groupId>io.gatling</groupId> <artifactId>gatling-http</artifactId> <version>${gatling.version}</version> <scope>test</scope> </dependency> <!-- 报告生成:Highcharts 版 index.html --> <dependency> <groupId>io.gatling.highcharts</groupId> <artifactId>gatling-charts-highcharts</artifactId> <version>${gatling.version}</version> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>io.gatling</groupId> <artifactId>gatling-maven-plugin</artifactId> <version>${gatling.version}</version> <configuration> <!-- 只想跑某个类时,用 -Dgatling.simulationClass 覆盖 --> <simulationClass>com.example.BasicSimulation</simulationClass> </configuration> </plugin> </plugins> </build>这份配置里有两个点值得说明。scope全部设为test,是因为压测脚本只服务于测试阶段,打包产物里不该出现 Gatling 的类,否则和 Spring Boot 的spring-boot-maven-plugin打包一起用时会因为缺少Main-Class或类冲突报错。simulationClass配置项是默认值,实际执行时通过-Dgatling.simulationClass=全限定类名覆盖更灵活,CI 里一个 job 跑多个场景就靠这个参数切换。
目录结构按 Maven 的约定来,src/test/scala放 Simulation 类,src/test/resources放数据文件和配置:
src/ └── test/ ├── scala/ │ └── com/example/ │ ├── BasicSimulation.scala │ └── modules/ # 可复用的请求片段 └── resources/ ├── users.csv # feeder 数据 ├── bodies/ # POST body 模板 ├── gatling.conf # 覆盖默认配置 └── logback-test.xml # 控制日志噪音gatling.conf必须放在src/test/resources。放在src/main/resources是不生效的,因为 Maven 插件执行时测试类路径优先,而且 3.0.0 加载配置的顺序是「内置默认 → classpath 下的gatling.conf→ 命令行-D」,顺序搞错就会出现「我明明改了配置但报告目录还是老地方」。
2.3 第一个 Simulation:从 HttpProtocol 到 setUp
Gatling 的脚本结构是固定的三段式:定义协议、定义场景、在setUp里把场景和注入模型绑起来。3.0.0 的HttpProtocol把baseURL改成了baseUrl,这是从 2.x 迁移时第一个编译错误,别怀疑自己记错了。
package com.example import io.gatling.core.Predef._ import io.gatling.http.Predef._ import scala.concurrent.duration._ class BasicSimulation extends Simulation { // 协议层:所有请求共享的 baseUrl、请求头、连接池行为 val httpProtocol = http .baseUrl("http://127.0.0.1:8080") .acceptHeader("application/json") .contentTypeHeader("application/json") .userAgentHeader("gatling-3.0.0-demo") .shareConnections // 所有虚拟用户复用连接池,压测本机服务时更接近真实网关行为 // 场景层:一组按顺序执行的请求,等价于一个业务用例 val scn = scenario("basic-api") .exec( http("GET /health") .get("/health") .check(status.is(200)) ) .pause(1, 3) // 每个用户请求之间随机停 1~3 秒,避免所有用户同步锤 // 负载层:注入模型 + 协议绑定 setUp( scn.inject( atOnceUsers(10), // 立刻起 10 个用户 rampUsers(50) during (30 seconds) // 30 秒内线性加到 50 个 ).protocols(httpProtocol) ) }这段代码里shareConnections值得单独说。默认情况下每个虚拟用户有自己的连接池,本机压测时文件句柄会迅速打满,表现为Too many open files;打开共享连接后所有用户走同一个池,更接近 Nginx 后面的真实流量形态。但如果你的目的是压被测服务的连接建立能力,就该关掉它,让每个用户独立建连。这是个语义选择,不是性能开关。
pause(1, 3)是均匀分布的随机停顿,两个参数是下界和上界,单位秒。不要写成pause(1)之后再pause(2)来模拟思考时间,那样所有用户的节奏会同步,报告里的响应时间曲线会出现人为的锯齿。
跑起来:
# 编译并执行,报告默认输出到 target/gatling/ mvn -q gatling:test # 指定场景类和结果目录 mvn gatling:test \ -Dgatling.simulationClass=com.example.BasicSimulation \ -Dgatling.resultsFolder=target/gatling执行完打开target/gatling/basicsimulation-<时间戳>/index.html。如果这一步 404 或者目录是空的,先看控制台有没有Simulation ... started,没有就是类名写错或者包路径不在src/test/scala下。Maven 插件扫不到 Simulation 类时不会报错,只会安静地什么都不做,这是我见过的新手第一个卡点。
3. 表达式与 Session:把参数化从字符串拼接收回来
2.x 时代最让人头疼的是${}既能取 Session 属性又能取 Gatling 配置属性,同一个占位符在不同上下文里有不同含义,调试时只能靠猜。3.0.0 把这件事拆开了:${}只认 Session 属性,#{}只认 Gatling 属性(配置文件、-D参数、系统属性)。这个改动看着小,实际是把「运行时每个用户独立的数据」和「启动时全局固定的配置」彻底分成了两个命名空间,脚本可读性提升很明显。
3.1 Expression[T] 与 EL 的区别
3.0.0 引入了Expression[T]类型,所有接受动态值的 DSL 方法签名都从String变成了Expression[String],靠隐式转换把普通字符串字面量包装成常量表达式。这意味着你在.get("/user/${id}")里写的占位符会被解析,而在 Scala 的s"..."插值里写的${id}会被 Scala 编译器先处理掉——这是迁移期最常见的翻车点,报错信息是not found: value id,看着像变量没定义,实际是字符串插值把 Gatling 的占位符吃了。
// 正确:三引号字符串不触发 Scala 插值,${} 留给 Gatling 解析 exec(http("GET /user").get("""/user/${userId}""")) // 错误:s 插值会在编译期把 ${userId} 当 Scala 变量求值 // exec(http("GET /user").get(s"/user/${userId}")) // 也可以用 Expression 的显式构造,适合需要默认值的场景 import io.gatling.core.session.el._ val path: Expression[String] = "/user/${userId}".elExpression[T]还带来一个实用能力:.validate和.map。当 Session 里的值可能缺失时,"${token}".el.validate会在属性不存在时直接让该用户失败,而不是把字面量${token}拼进 URL 发出一个必然 404 的请求。默认行为是「缺失就保留原样」,这在调试时很有迷惑性——你会看到一堆 404,但日志里请求路径长得完全正确。
3.2 Feeder 的四种读取策略与数据准备
Feeder 是 Gatling 的参数化核心,3.0.0 提供queue、circular、random、shuffle四种迭代器。queue是默认行为,用完即弃,数据耗尽时抛No more records found;circular循环读;random每次随机取一条;shuffle打乱后循环。
// 用户数据:id,name,token val userFeeder = csv("users.csv").circular // 程序化生成:适合只需要唯一 ID、不需要外部文件的场景 val rnd = new scala.util.Random val syntheticFeeder = Iterator.continually( Map( "orderId" -> f"ORD-${rnd.nextInt(1000000)}%07d", "amount" -> (rnd.nextInt(5000) + 100) ) ) val scn = scenario("order-flow") .feed(userFeeder) // 每个用户进入场景时取一行 .feed(syntheticFeeder) // 可以叠加多个 feeder,键不冲突即可 .exec(http("POST /order") .post("/order") .body(StringBody("""{"userId":"${id}","orderId":"${orderId}"}""")) .check(status.is(201)))csv的默认分隔符是逗号,遇到字段里本身含逗号的数据(比如带逗号的地址)就需要换分隔符,3.0.0 支持.csv("users.csv", '\t')传自定义分隔符。文件编码固定按 UTF-8 读,如果 CSV 是 Excel 导出的 GBK,中文会变成乱码并且不会报错,只会在报告里看到请求体是一串问号。
还有一个容易忽略的点:3.0.0 的 CSV feeder 在 Simulation 初始化阶段就把整个文件读进内存,运行期修改文件不会生效。有一次我在压测过程中往 CSV 里追加数据,期望能动态扩容,结果当然是毫无变化。需要动态数据就别用文件,用Iterator.continually程序化生成。
3.3 check 与 saveAs:把响应写回 Session
压测脚本和普通 HTTP 客户端的区别在于,前者必须把响应中的值提取出来传给下一个请求。Gatling 用check做提取,saveAs把结果写回 Session。
val scn = scenario("login-then-query") .exec(http("POST /login") .post("/login") .body(StringBody("""{"user":"${user}","pwd":"${pwd}"}""")) .check( status.is(200), jsonPath("$.code").is("0"), // 业务码校验,不通过则该请求 ko jsonPath("$.data.token").saveAs("authToken"), // String 类型 jsonPath("$.data.expireIn").saveAs("expireIn") // 数字会被存成字符串,取值时手动转 )) .exec(http("GET /profile") .get("/profile") .header("Authorization", "Bearer ${authToken}") .check(jsonPath("$.data.level").saveAs("level")))saveAs存进去的值类型是String,即使原始 JSON 里是数字。取值时用session("expireIn").as[Int],直接as[Int]在 3.0.0 里能工作是因为内部做了转换,但如果 JSON 里是null,转换会抛异常并让该用户失败。稳妥的写法是加.validate或者在exec(session => ...)里做判空。
check的顺序也会影响结果:status.is(200)放在最前面,响应码不对时后面的 JSON 提取会因为响应体是错误页而报解析失败,日志里就是一堆 JSON 解析异常,掩盖了真正的 500 错误。把状态码校验放第一位,能少走很多弯路。
3.4 属性注入与命令行传参
#{}取的是 Gatling 属性,来源有三处:gatling.conf、命令行-D、JVM 系统属性。这个机制适合把并发数、目标地址这类「每次跑都可能不同」的配置抽到脚本外面。
// gatling.conf 里可以写 gatling.test.targetUrl = "http://127.0.0.1:8080" val target = "#{targetUrl}".el // 取配置值 val users = Integer.getInteger("users", 10) // 取 JVM 参数,带默认值 val ramp = java.lang.Long.getLong("ramp", 30L) val httpProtocol = http.baseUrl(target.toString)# 命令行覆盖,注意 -D 要放在 goal 之后、插件才能读到 mvn gatling:test -Dusers=200 -Dramp=120 -DtargetUrl=http://127.0.0.1:9090Integer.getInteger只认 JVM 系统属性,Maven 的-D会传给 fork 出来的 JVM,所以能读到;但如果你的gatling-maven-plugin配置了fork=false,参数传递路径会变,读不到时默认值兜底,表现为「参数改了但压测力度没变」。CI 里我一般直接看报告头部的并发数确认参数生效,而不是相信命令行回显。
4. 注入模型:open 与 closed 到底该选哪个
注入模型是 Gatling 和大多数图形化压测工具差异最大的地方。图形化工具习惯让你填「线程数」,这隐含的是 closed 模型——固定并发用户数,每个用户做完一轮再开始下一轮。但真实的线上流量更像 open 模型:每秒来的请求数由外部决定,服务慢了请求会堆积,而不是用户排队等着。选错模型,压出来的容量结论会差好几倍。
4.1 closed 模型:atOnceUsers 与 rampUsers
closed 模型的语义是「维持 N 个并发用户」。atOnceUsers(10)是立刻启动 10 个用户,rampUsers(100) during (60 seconds)是 60 秒内线性把并发从 0 加到 100。这两者的压力施加方式是「用户做完一轮立刻开始下一轮」,所以实际 RPS = 并发数 / 单轮耗时。服务变慢时 RPS 会自动下降,压力反而减小——这就是 closed 模型最反直觉的地方,它压不出服务的极限。
setUp( scn.inject( nothingFor(5 seconds), // 预热期,等被测服务 JIT 完成 atOnceUsers(20), // 冷启动冲击,看连接建立能力 rampUsers(200) during (120 seconds) // 缓慢加压,观察响应时间拐点 ).protocols(httpProtocol) )这段配置里有个关键语义:inject的多个参数之间是并行的,不是先后。上面三个步骤会在同一时刻启动三个独立的 population,nothingFor(5 seconds)产生的那个 population 什么都不做,另外两个立刻开始。很多人以为inject是流水线,写了atOnceUsers(10), rampUsers(100) during(...)期望「先 10 个再慢慢加」,实际是两拨用户同时打。
要真正串行,得用andThen拆成多个setUp或者用splitUsers:
// 方案一:多个场景串行,注意这会重新初始化场景状态 setUp( scn.inject(atOnceUsers(10)).protocols(httpProtocol), scn.inject(rampUsers(100) during (60 seconds)).protocols(httpProtocol) ) // 方案二:splitUsers 把同一批用户切成时间段 setUp( scn.inject( splitUsers(200) into (rampUsers(50) during (10 seconds)) separatedBy (30 seconds) ).protocols(httpProtocol) )splitUsers(200) into (rampUsers(50) during (10 seconds)) separatedBy (30 seconds)的意思是:总共 200 个用户,每 30 秒启动一批,每批 50 个用户在 10 秒内线性加压。这个写法在模拟「一波一波的营销活动流量」时特别顺手。
4.2 open 模型:constantUsersPerSec 与 rampUsersPerSec
open 模型直接控制「每秒启动多少个新用户」,和服务的响应时间解耦。服务慢了,用户照样按节奏进来,请求在服务端堆积,这才是压测容量该有的行为。
setUp( scn.inject( constantUsersPerSec(5) during (30 seconds), // 稳定 5 用户/秒 rampUsersPerSec(5) to (100) during (120 seconds) // 从 5 线性涨到 100 用户/秒 ).protocols(httpProtocol) )注意constantUsersPerSec(5)里的 5 是「每秒新启动的用户数」,不是「并发数」。如果单轮请求耗时 200ms,5 用户/秒大概对应 1 个并发;如果耗时 2 秒,就对应 10 个并发。这种设计让压力曲线可预期,但也意味着你需要根据单轮耗时估算实际并发,否则可能出现「RPS 设得很高但并发很低」的情况。
rampUsersPerSec(5) to (100) during (120 seconds)是最有用的加压曲线,配合报告里的响应时间分位图,能直观看到在哪个 RPS 上 P99 开始抬头。我一般会在这个曲线上找一个「P99 超过 SLA 的 RPS 点」,作为容量结论。
4.3 stressPeakUsers 与脉冲流量
电商场景的秒杀、定时任务触发的批量请求,都是典型的脉冲流量。stressPeakUsers专门做这个:在指定时长内以正弦曲线先升后降,模拟一次流量尖峰。
setUp( scn.inject( stressPeakUsers(1000) during (30 seconds) // 30 秒内峰值 1000 用户/秒 ).protocols(httpProtocol) )这个注入器内部按正弦分布发用户,峰值在时长的中点。用它测限流、熔断、队列堆积特别合适,因为它给的是「先慢后快再慢」的真实冲击,而不是atOnceUsers那种方波。
4.4 throttle 与 inject 不能混用
throttle是另一种控制压力的方式,它限制的是「全局每秒请求数」,和 open 模型的语义重叠,所以 3.0.0 里两者不能同时用。混用会直接抛配置异常,报错大意是「throttling 和 open 注入模型不能共存」。
setUp( scn.inject(atOnceUsers(50)).protocols(httpProtocol) // closed 模型 ).throttle( reachRps(200) in (10 seconds), // 10 秒内爬到 200 RPS holdFor(60 seconds), // 保持 60 秒 jumpToRps(500), // 直接跳到 500 holdFor(30 seconds) )throttle适合「我只想给被测服务固定 RPS,不管它快慢」的场景,比如验证「200 RPS 下 P99 是否小于 500ms」。它的限制是只能配 closed 模型,而且 throttle 到达上限后用户会被阻塞等待,报告里的响应时间不包括这段等待,看起来会比实际乐观。
注意:
throttle的reachRps(...).in(...)里的时长必须是整数秒,写0.5 seconds在 3.0.0 里会编译不过或者被截断,别在这里玩精度。
5. 避坑清单:五个把压测结果跑废的现场
压测工具最大的风险不是学不会,而是「跑出来了、结果不对、还信了」。下面这五条是我在 3.0.0 上真实踩过的,每条都按现象、原因、解决来写,能省掉你半天以上的排查时间。
5.1 报错 No more records found,但 CSV 明明有数据
现象:运行几十秒后大量用户失败,日志里刷java.lang.IllegalStateException: No more records found,打开 CSV 看还有几千行没读完。
原因:默认的csv("users.csv")是queue迭代器,每个用户取一行,取完就丢。Gatling 在 Simulation 初始化时按「文件行数」判断能支持多少用户,一旦注入的用户数超过行数,即使这些行在时间上还没被消费完,也会因为迭代器耗尽而报错——它不区分「取完」和「取过」。
解决:改成csv("users.csv").circular循环读,或者用random;如果业务要求用户 ID 唯一,就别用有限文件,改用Iterator.continually生成。另一个隐蔽情况是 CSV 末尾有空行,Gatling 会把空行当成一条记录,读到时报字段缺失,用wc -l看一眼行数对不对得上。
// 有限数据 + 超出用户数:必炸 val bad = csv("users.csv") // 循环读:用户数不受行数限制 val good = csv("users.csv").circular5.2 报告里响应时间很漂亮,但业务全挂了
现象:压测跑完 index.html 里的响应时间分位数看着很好,P99 只有几十毫秒,成功率 100%,但业务方说那段时间接口全返回「余额不足」。
原因:check(status.is(200))只验证了 HTTP 状态码。很多后端把业务错误包装成 200 +{"code": 500, "msg": "..."},Gatling 认为请求成功,响应时间也确实很短——错误响应往往比正常响应快得多。这种「假成功」是压测报告最常见的失真来源。
解决:每个请求都要加业务码校验,jsonPath("$.code").is("0")或者jsonPath("$.success").is(true)。校验失败的请求会被标记为 ko,进入报告的失败统计。更进一步,可以把「业务码非 0」的响应单独提取出来做分布统计,看是哪类错误在涨。
5.3 脚本里写了 ${userId},运行时报 not found: value userId
现象:编译阶段就失败,错误指向脚本里那一行,说是变量未定义。
原因:用了 Scala 的s"..."字符串插值。s"/user/${userId}"会先被 Scala 编译器处理,把${userId}当成 Scala 变量求值,而这个变量在作用域里不存在。
解决:三引号"""..."""或者普通双引号不加s前缀,把${}原样留给 Gatling 的 EL 解析。如果确实需要在字符串里拼 Scala 变量,用+拼接或者把 Scala 值放进 Session 再用占位符取。
5.4 本地跑没问题,CI 上启动即报 NoSuchMethodError
现象:开发机上mvn gatling:test一切正常,流水线里跑同样的命令,启动阶段抛java.lang.NoSuchMethodError,堆栈指向io.gatling.core.session.Session。
原因:类路径上有多个版本的 Gatling 或者 Scala 库。CI 的构建缓存可能带了旧版本,或者父 pom 的dependencyManagement锁了另一个版本。3.0.0 的Session类在 2.x 上签名完全不同,混用必然失败。
解决:mvn dependency:tree -Dincludes=io.gatling,org.scala-lang打印出来核对,把所有 Gatling 组件的版本对齐到 3.0.0,Scala 库统一到 2.12.x。CI 里加一步dependency:tree输出到日志,出问题时不用重新构建就能看出端倪。
5.5 改了 gatling.conf 但日志和报告目录纹丝不动
现象:在src/main/resources下放了gatling.conf,改了outputDirectoryBaseName,跑完报告还是输出到默认目录。
原因:Gatling 只从测试类路径加载gatling.conf。Maven 项目里src/main/resources进的是主类路径,测试阶段虽然也能读到,但gatling-maven-plugin的类加载顺序把内置默认配置放在了前面。
解决:把gatling.conf挪到src/test/resources,并且确认文件名是小写、没有 BOM。改完跑一次,报告目录名变了就说明生效。另外gatling.conf的键是 HOCON 格式,缩进和=号都可以,但键名不能拼错,拼错的键会被静默忽略,不会报错——这是我见过最没脾气的失败方式。
6. 进阶:把场景拆成可复用模块,先跑冒烟再谈容量
写多了会发现,重复的exec(http(...))链会迅速变成一坨。3.0.0 的ScenarioBuilder是普通对象,可以抽到object里复用,这一点比 DSL 长得像代码更值钱。
package com.example.modules import io.gatling.core.Predef._ import io.gatling.http.Predef._ object Auth { // 返回一个可复用的 exec 链,调用方直接 .exec(Auth.login) val login = exec(http("POST /login") .post("/login") .body(StringBody("""{"user":"${user}","pwd":"${pwd}"}""")) .check( status.is(200), jsonPath("$.code").is("0"), jsonPath("$.data.token").saveAs("authToken") )) val logout = exec(http("POST /logout").post("/logout").check(status.is(200))) }调用方.exec(Auth.login)即可,公共的 header、断言、失败处理只维护一份。进一步可以把httpProtocol的公共部分也抽成object Protocols,环境差异(本地、预发)通过-D参数切换。
这里补一个我坚持了很多年的习惯:任何压测脚本上线前,先跑一次「1 用户 × 1 迭代」的冒烟。命令不用改,把注入模型临时换成atOnceUsers(1),或者单独写一个SmokeSimulation。冒烟要验证的不是性能,是脚本本身——feeder 能读到数据、登录能拿到 token、后续请求能带上正确的 header、业务码校验全部通过。冒烟不过就跑容量压测,得到的所有数字都是废的,而且你还得花时间分辨哪些是脚本问题哪些是服务问题。
# 冒烟:1 个用户,跑完看报告里 ko 是否为 0 mvn gatling:test -Dgatling.simulationClass=com.example.SmokeSimulation这个步骤看起来多余,实际上它把「脚本正确性」和「服务性能」两个变量彻底分开了。我见过太多次直接上 200 并发、报告一片红、然后团队花两天争论是服务扛不住还是脚本写错了的场面。冒烟的代价是 30 秒,收益是后面几小时的排查时间。
还有一种情况值得单独提:check里用jsonPath提取数组时,$.items[*]拿到的是列表,saveAs存进去再取出来是个字符串形式的列表,想做聚合统计得在exec(session => ...)里手动解析。我一般不在压测脚本里做复杂的数据加工,那属于测试后分析的事,脚本里做得越多,出问题时越难定位是脚本 bug 还是服务 bug。
从那以后我每次动压测脚本,都强制走一遍「冒烟 → 单机小并发 → 目标并发」三段,任何一段 ko 不为 0 就停下。压测结论的可信度,比压测数字本身重要得多。希望帮到你。
本文还有配套的精品资源,点击获取