Gatling 完整实战——Scala DSL 性能测试、场景设计、报告解读
Gatling 完整实战——Scala DSL 性能测试、场景设计、报告解读
适读人群:Java/Scala开发工程师、对代码化压测脚本感兴趣的测试工程师 | 阅读时长:约15分钟 | 核心价值:掌握Gatling从环境搭建到场景设计到报告解读的完整工作流
从XML配置到代码:那次工具迁移的契机
2022年我们团队有个需求:要对一个推荐系统的接口做持续性能测试,而且这个接口的请求参数非常复杂,有嵌套的JSON结构,还需要根据上一个接口的响应动态计算下一个请求的参数。
我尝试用JMeter的BeanShell脚本实现,写出来的代码很难维护。同事看了看说:"你试试Gatling吧,直接用Scala写场景,逻辑复杂的时候比JMeter好维护多了。"
那次迁移的过程并不轻松——Scala语法对Java工程师来说有学习成本,但当我写出第一个能跑的Gatling脚本时,我立刻理解了它的价值:整个压测场景用代码描述,复杂的参数逻辑直接写Scala函数,可以复用、可以测试、可以用Git管理。
Gatling生成的HTML报告也是业内最好看的,颗粒度细到每个请求的响应时间分布直方图。
Gatling 安装与项目结构
方式一:独立安装
# 下载
wget https://repo1.maven.org/maven2/io/gatling/highcharts/gatling-charts-highcharts-bundle/3.10.5/gatling-charts-highcharts-bundle-3.10.5-bundle.zip
unzip gatling-charts-highcharts-bundle-3.10.5-bundle.zip -d /opt/
# 目录结构
/opt/gatling-charts-highcharts-bundle-3.10.5/
├── bin/
│ ├── gatling.sh # 运行压测
│ └── recorder.sh # 录制HTTP请求
├── conf/ # 配置文件
├── user-files/
│ ├── simulations/ # 放你的Scala脚本
│ ├── resources/ # 放CSV等测试数据
│ └── results/ # 测试结果
└── lib/方式二:Maven/Gradle项目(推荐)
<!-- pom.xml -->
<build>
<plugins>
<plugin>
<groupId>io.gatling</groupId>
<artifactId>gatling-maven-plugin</artifactId>
<version>4.9.6</version>
<configuration>
<simulationClass>simulations.OrderSimulation</simulationClass>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>io.gatling.highcharts</groupId>
<artifactId>gatling-charts-highcharts</artifactId>
<version>3.10.5</version>
<scope>test</scope>
</dependency>
</dependencies>核心 DSL 概念
Gatling的代码从外到内是这样的层次:
Simulation(模拟类,继承自Simulation)
└── scenario(场景,定义用户行为序列)
└── exec(执行一个动作)
├── http(HTTP请求)
├── pause(等待时间)
├── check(响应断言与提取)
└── feed(注入数据)
└── setUp(设置注入策略)
└── inject(注入用户)
├── atOnceUsers(瞬间注入N个用户)
├── rampUsers(在X秒内逐步注入N个用户)
└── constantUsersPerSec(每秒注入N个用户,持续X秒)
└── protocols(协议配置,如http baseUrl等)一个完整的 Gatling 压测脚本
以电商订单场景为例,实现"登录→查询商品→创建购物车→下单"完整链路:
package simulations
import io.gatling.core.Predef._
import io.gatling.http.Predef._
import scala.concurrent.duration._
import scala.util.Random
class OrderSimulation extends Simulation {
// ============ 1. HTTP协议基础配置 ============
val httpProtocol = http
.baseUrl("https://test-api.example.com")
.acceptHeader("application/json")
.contentTypeHeader("application/json")
.acceptEncodingHeader("gzip, deflate")
.userAgentHeader("Gatling/3.10.5")
.shareConnections // 模拟Keep-Alive复用连接
// ============ 2. 数据供给器 ============
// 从CSV文件读取测试用户数据
val userFeeder = csv("users.csv").circular
// circular: 循环读取,不会耗尽数据
// random: 随机读取
// queue: 顺序读取(耗尽后报错)
val productFeeder = csv("products.csv").random
// 也可以用代码生成数据
val dynamicFeeder = Iterator.continually(Map(
"orderId" -> s"ORDER_${System.currentTimeMillis()}_${Random.nextInt(99999)}",
"quantity" -> (Random.nextInt(5) + 1).toString
))
// ============ 3. 场景定义 ============
val orderScenario = scenario("电商下单完整链路")
// Step1: 登录
.feed(userFeeder)
.exec(
http("POST 用户登录")
.post("/api/auth/login")
.body(StringBody(
"""{"username":"${username}","password":"${password}"}"""
))
.check(
status.is(200),
jsonPath("$.code").is("200"),
jsonPath("$.data.accessToken").saveAs("accessToken"),
jsonPath("$.data.userId").saveAs("userId")
)
)
.pause(1, 3) // 模拟1-3秒的用户思考时间
// Step2: 查询商品列表
.feed(productFeeder)
.exec(
http("GET 商品详情")
.get("/api/product/${productId}")
.header("Authorization", "Bearer ${accessToken}")
.check(
status.is(200),
jsonPath("$.data.skuId").saveAs("skuId"),
jsonPath("$.data.price").saveAs("productPrice")
)
)
.pause(2, 5)
// Step3: 加入购物车
.exec(
http("POST 加入购物车")
.post("/api/cart/add")
.header("Authorization", "Bearer ${accessToken}")
.body(StringBody(
"""{"userId":"${userId}","skuId":"${skuId}","quantity":${quantity}}"""
))
.check(
status.is(200),
jsonPath("$.data.cartItemId").saveAs("cartItemId")
)
)
.pause(1, 2)
// Step4: 创建订单
.feed(dynamicFeeder)
.exec(
http("POST 创建订单")
.post("/api/order/create")
.header("Authorization", "Bearer ${accessToken}")
.header("X-Request-ID", "${orderId}")
.body(StringBody(
"""{
"userId": "${userId}",
"cartItemIds": ["${cartItemId}"],
"addressId": "${__random(1001,1020)}",
"couponId": null,
"remark": "Gatling压测订单"
}"""
))
.check(
status.is(200),
jsonPath("$.code").is("200"),
jsonPath("$.data.orderId").saveAs("createdOrderId"),
responseTimeInMillis.lte(2000) // 断言响应时间 <= 2000ms
)
)
// ============ 4. 负载注入策略 ============
// 方案A:阶梯加压(用于找瓶颈)
val stressTest = setUp(
orderScenario.inject(
nothingFor(5.seconds), // 等待5秒(预热服务)
atOnceUsers(10), // 瞬间注入10个用户(冷启动测试)
rampUsersPerSec(1).to(50).during(60.seconds), // 60秒内从1到50用户/秒
constantUsersPerSec(50).during(5.minutes), // 保持50用户/秒,持续5分钟
rampUsersPerSec(50).to(100).during(60.seconds) // 继续加压到100用户/秒
)
).protocols(httpProtocol)
// 方案B:标准负载测试(用于验证系统在正常负载下的表现)
val loadTest = setUp(
orderScenario.inject(
rampUsers(200).during(60.seconds), // 60秒内启动200个虚拟用户
nothingFor(10.minutes) // 保持这200个用户运行10分钟
)
).protocols(httpProtocol)
.assertions(
global.responseTime.percentile(99).lt(500), // 全局P99 < 500ms
global.successfulRequests.percent.gte(99.5), // 成功率 >= 99.5%
forAll.responseTime.mean.lt(200) // 每个请求平均 < 200ms
)
// 使用负载测试方案
setUp(
orderScenario.inject(
rampUsers(200).during(60.seconds)
)
).protocols(httpProtocol)
.assertions(
global.responseTime.percentile(99).lt(500),
global.successfulRequests.percent.gte(99.5)
)
}运行 Gatling
# 方式一:独立安装运行
/opt/gatling/bin/gatling.sh -s simulations.OrderSimulation
# 方式二:Maven
mvn gatling:test -Dgatling.simulationClass=simulations.OrderSimulation
# 带参数运行(覆盖脚本里的配置)
mvn gatling:test \
-Dgatling.simulationClass=simulations.OrderSimulation \
-DTARGET_HOST=test-api.example.com \
-DTHREADS=200
# 脚本里读取系统属性
val targetHost = System.getProperty("TARGET_HOST", "localhost")
val threads = System.getProperty("THREADS", "100").toInt报告解读
Gatling生成的HTML报告是业内最好看、最详细的,几个关键图表:
1. Response Time Distribution(响应时间分布直方图)
横轴是响应时间区间,纵轴是请求数量。正常的形状是左偏的钟形——大多数请求集中在200ms以内,右侧有长尾。
如果图形有双峰(比如一个峰在100ms,另一个峰在2000ms),说明有两类请求:正常请求和极慢请求,需要找出慢请求的原因。
2. Response Time Percentiles over Time(百分位数时间序列)
最重要的图。横轴是压测经过的时间,纵轴是响应时间,图上有P50/P75/P95/P99四条线。
正常情况下四条线应该保持相对平稳。如果P99线在某个时刻突然飙升,说明那个时刻发生了某件事(比如GC、慢SQL、外部依赖超时)。
3. Number of Requests per Second(TPS曲线)
展示压测过程中的TPS变化。如果TPS曲线在加压阶段持续增长,说明系统还有余量。如果TPS增长停滞甚至下降,而并发还在增加,说明达到了性能瓶颈。
4. Requests/Responses(请求/响应统计)
表格形式,列出每个请求的:
- 总请求数
- 成功/失败数
- 最小/最大响应时间
- P50/P75/P95/P99
踩坑实录
坑1:check().saveAs() 变量作用域问题
现象: 登录接口提取了accessToken,但下单接口里用${accessToken}时报错"No attribute named accessToken is defined"。
原因: Scala DSL里变量保存在Session里,每个虚拟用户有独立的Session。但如果在exec里用了.doIf或.tryMax等条件控制,内部的exec产生的新变量可能不在外层作用域。
解法: 确保saveAs和引用在同一个虚拟用户的会话链里,不跨场景。如果需要全局共享数据,用Feeder注入。
坑2:circular feeder在高并发下出现数据竞争
现象: 200并发跑CSV feeder,偶尔有用户收到"No more user to inject"错误,然后请求用了空的userId。
原因: Gatling的circular feeder在内部用了一个AtomicInteger计数器,极高并发下偶尔有线程安全问题(旧版本有此bug)。
解法: 升级到Gatling 3.9+版本,或者在脚本里用random代替circular,随机读取避免顺序竞争。
坑3:assertions导致测试立即失败但不报明确错误
现象: 加了global.successfulRequests.percent.gte(99.5)断言,压测开始1分钟后立刻停止并报失败,但报告里成功率显示是99.7%。
原因: Gatling的断言是在压测结束后计算的,但如果脚本里有语法错误的断言会在启动时就失败。另一个原因是百分比的精度问题——gte(99.5)比较的是double值,有时浮点精度导致99.50000001被判为未达到99.5。
解法: 把断言阈值设略低一点:gte(99)而不是gte(99.5)。Gatling 3.10+版本修复了浮点精度问题。
总结
Gatling的核心优势:Scala DSL让复杂场景的脚本可维护,HTML报告详细而直观,Maven/Gradle集成方便纳入CI。
主要学习门槛:需要了解Scala基本语法(不需要精通,但得会看懂和写基础代码),以及Gatling的DSL惯用法。
如果你的团队有Scala/Java基础,复杂场景建议用Gatling;简单场景或团队没有开发背景,JMeter的GUI上手更快。
下一篇就是两者的系统对比:Gatling vs JMeter,2024年选型完整参考。
