本文属于《从入门到放弃》系列,但这次我建议你坚持到最后,因为不会压测的后端,就像不会验孕的产科医生——你永远不知道系统什么时候会"生"出问题。
前言:一个没做压测的项目上线后
产品经理:"这次大促我们预计有10万用户同时在线。"
你:"没问题,我优化过SQL了。"
大促当天,流量进来的瞬间,服务器CPU直接拉满,数据库连接池爆了,Redis也跟着躺平,整个系统以一种非常优雅的姿势——502 Bad Gateway。
老板在群里@所有人:"谁能解释一下?"
你开始疯狂翻日志,心里默念:"早知道做个压测啊..."
今天,我们就来聊聊如何用Gatling,在上线前把系统"打"一遍,让Bug在测试环境暴露,而不是在用户面前表演。
一、什么是Gatling?
Gatling是一款用Scala编写、基于Akka和Netty的高性能压测工具,主要用于HTTP服务的负载测试和性能分析。
测试脚本"] Core["Gatling Core
核心引擎"] Akka["Akka Actor
并发模型"] Netty["Netty
异步IO"] Report["HTML Report
可视化报告"] end DSL --> Core Core --> Akka Akka --> Netty Netty --> Target["目标服务器"] Core --> Report style DSL fill:#6c5ce7,color:#fff style Core fill:#00b894,color:#fff style Akka fill:#0984e3,color:#fff style Netty fill:#e17055,color:#fff style Report fill:#fdcb6e,color:#333
为什么选择Gatling而不是JMeter?
| 特性 | Gatling | JMeter |
|---|---|---|
| 编写方式 | 代码即配置(Scala DSL) | GUI拖拽 + XML |
| 资源消耗 | 低(异步非阻塞) | 高(每用户一个线程) |
| 并发能力 | 单机轻松上万 | 单机几千就开始吃力 |
| 报告质量 | 精美的HTML报告 | 需要插件美化 |
| 学习曲线 | 需要会点Scala | 上手简单 |
| 版本控制 | 代码友好,Git管理 | XML地狱 |
| CI/CD集成 | Maven/Gradle原生支持 | 需要额外配置 |
一句话总结:JMeter适合快速上手搞一搞,Gatling适合认真做性能测试并纳入CI流程。
二、性能测试的几种类型
在开始学Gatling之前,先搞清楚性能测试到底在测什么:
负载测试"] ST["💥 Stress Testing
压力测试"] SK["⏱️ Soak Testing
浸泡测试"] SP["📈 Spike Testing
尖峰测试"] end LT --> LT_DESC["预定用户数测吞吐量
验证系统能否扛住预期负载"] ST --> ST_DESC["不断加压找断点
看系统什么时候会崩"] SK --> SK_DESC["长时间稳定流量
发现内存泄漏等问题"] SP --> SP_DESC["突发流量冲击
模拟秒杀、抢购场景"] style LT fill:#1dd1a1,color:#fff style ST fill:#ff6b6b,color:#fff style SK fill:#54a0ff,color:#fff style SP fill:#feca57,color:#333
详细解释:
| 类型 | 目的 | 场景 | Gatling实现方式 |
|---|---|---|---|
| Load Testing | 验证系统在预期负载下的表现 | 日常流量、常规业务 | constantUsersPerSec(100) |
| Stress Testing | 找到系统的极限和断点 | 容量规划、扩容依据 | rampUsersPerSec(10) to 500 |
| Soak Testing | 发现长时间运行的问题 | 内存泄漏、连接池耗尽 | constantUsersPerSec(50).during(4.hours) |
| Spike Testing | 测试突发流量的承受能力 | 秒杀、大促、热点事件 | heavisideUsers(1000).during(10.seconds) |
三、安装与配置
3.1 环境要求
- JDK 8+ (推荐JDK 11或17)
- Maven 或 Gradle
- IDE:IntelliJ IDEA + Scala插件(可选但推荐)
3.2 方式一:下载独立版
# 下载地址:https://gatling.io/open-source/
# 解压后目录结构
gatling-charts-highcharts-bundle-3.9.5/
├── bin/
│ ├── gatling.sh # 运行测试
│ └── recorder.sh # 录制脚本
├── conf/ # 配置文件
├── lib/ # 依赖库
├── results/ # 测试报告
└── user-files/
├── simulations/ # 测试脚本放这里
└── resources/ # 测试数据文件运行内置示例:
./bin/gatling.sh
GATLING_HOME is set to /opt/gatling-3.9.5
Choose a simulation number:
[0] computerdatabase.BasicSimulation
[1] computerdatabase.advanced.AdvancedSimulationStep01
...
0
Select run description (optional)
my first test
Simulation computerdatabase.BasicSimulation started...
================================================================================
---- Global Information --------------------------------------------------------
> request count 130 (OK=130 KO=0)
> min response time 112 (OK=112 KO=-)
> max response time 846 (OK=846 KO=-)
> mean response time 265 (OK=265 KO=-)
> std deviation 138 (OK=138 KO=-)
> response time 50th percentile 224 (OK=224 KO=-)
> response time 75th percentile 334 (OK=334 KO=-)
> response time 95th percentile 539 (OK=539 KO=-)
> response time 99th percentile 761 (OK=761 KO=-)
> mean requests/sec 6.842 (OK=6.842 KO=-)
================================================================================
Reports generated in 0s.
Please open the following file: /opt/gatling-3.9.5/results/basicsimulation-20240115/index.html3.3 方式二:Maven项目集成(推荐)
这是正经项目的做法,创建Maven项目:
mvn archetype:generate \
-DarchetypeGroupId=io.gatling.highcharts \
-DarchetypeArtifactId=gatling-highcharts-maven-archetype \
-DarchetypeVersion=3.9.5或者手动配置pom.xml:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>gatling-demo</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<gatling.version>3.9.5</gatling.version>
<gatling-maven-plugin.version>4.3.0</gatling-maven-plugin.version>
<scala.version>2.13.10</scala.version>
</properties>
<dependencies>
<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-maven-plugin.version}</version>
</plugin>
</plugins>
</build>
</project>项目结构:
src/
└── test/
├── scala/
│ └── simulations/
│ └── MyFirstSimulation.scala
└── resources/
├── gatling.conf
├── logback.xml
└── data/
└── users.csv运行测试:
# 运行所有测试
mvn gatling:test
# 运行指定测试
mvn gatling:test -Dgatling.simulationClass=simulations.MyFirstSimulation3.4 方式三:Recorder录制
如果你懒得手写脚本,可以用Recorder录制浏览器操作:
./bin/recorder.shRecorder界面主要配置项:
使用步骤:
- 启动Recorder,设置本地代理端口(默认8000)
- 浏览器配置HTTP代理指向
localhost:8000 - 点击 Start 开始录制
- 在浏览器中执行业务操作
- 点击 Stop 停止并生成Scala脚本
配置代理后,浏览器的所有请求都会被记录下来,自动生成Scala脚本。适合复杂业务流程的快速录制,但生成的代码通常需要手动优化。
四、Gatling DSL 详解
这是Gatling的精华部分,理解了DSL,你就掌握了Gatling的80%。
4.1 脚本结构总览
HTTP配置"] SC["scenario
用户场景"] INJ["inject
注入策略"] end subgraph Scenario["📋 Scenario(场景详情)"] EXEC["exec
执行动作"] PAUSE["pause
思考时间"] LOOP["repeat/foreach
循环"] COND["doIf/doSwitch
条件"] end subgraph Request["📡 HTTP Request"] GET["http('name')
.get/post/put/delete"] HEADER["header
请求头"] BODY["body
请求体"] CHECK["check
响应校验"] end HTTP --> SC SC --> INJ SC --> EXEC EXEC --> GET GET --> HEADER GET --> BODY GET --> CHECK EXEC --> PAUSE EXEC --> LOOP EXEC --> COND style HTTP fill:#6c5ce7,color:#fff style SC fill:#00b894,color:#fff style INJ fill:#e17055,color:#fff
4.2 第一个完整示例
package simulations
import io.gatling.core.Predef._
import io.gatling.http.Predef._
import scala.concurrent.duration._
class MyFirstSimulation extends Simulation {
// ============ HTTP 协议配置 ============
val httpProtocol = http
.baseUrl("https://api.example.com") // 基础URL
.acceptHeader("application/json") // Accept头
.acceptEncodingHeader("gzip, deflate")
.acceptLanguageHeader("zh-CN,zh;q=0.9")
.userAgentHeader("Gatling/3.9.5")
.contentTypeHeader("application/json")
// ============ 场景定义 ============
val scn = scenario("用户登录并查询订单")
.exec(
http("登录接口")
.post("/api/login")
.body(StringBody("""{"username":"test","password":"123456"}"""))
.check(
status.is(200),
jsonPath("$.token").saveAs("authToken") // 保存token
)
)
.pause(1, 3) // 随机暂停1-3秒,模拟用户思考时间
.exec(
http("查询订单列表")
.get("/api/orders")
.header("Authorization", "Bearer ${authToken}") // 使用保存的token
.check(
status.is(200),
jsonPath("$.data[*]").count.gte(0)
)
)
// ============ 注入策略 ============
setUp(
scn.inject(
atOnceUsers(10), // 立即注入10个用户
rampUsers(100).during(30.seconds), // 30秒内逐渐增加到100用户
constantUsersPerSec(20).during(1.minute) // 保持每秒20用户持续1分钟
)
).protocols(httpProtocol)
.assertions(
global.responseTime.max.lt(3000), // 最大响应时间小于3秒
global.successfulRequests.percent.gt(95) // 成功率大于95%
)
}4.3 HTTP请求详解
GET请求
http("获取用户信息")
.get("/api/users/${userId}") // 路径参数
.queryParam("page", "1") // 查询参数
.queryParam("size", "10")
.header("X-Custom-Header", "value")
.check(status.is(200))POST请求(JSON)
http("创建用户")
.post("/api/users")
.body(StringBody(
"""{
"name": "${userName}",
"email": "${email}",
"age": ${age}
}"""
)).asJson
.check(
status.is(201),
jsonPath("$.id").saveAs("newUserId")
)POST请求(表单)
http("表单登录")
.post("/login")
.formParam("username", "${username}")
.formParam("password", "${password}")
.formParam("remember", "true")
.check(status.is(302))文件上传
http("上传文件")
.post("/api/upload")
.bodyPart(RawFileBodyPart("file", "data/test.pdf")
.fileName("report.pdf")
.contentType("application/pdf"))
.check(status.is(200))4.4 Check(响应校验)—— 压测不是只看能不能跑通
Check是Gatling的灵魂,它决定了一个请求是"成功"还是"失败":
http("查询订单")
.get("/api/orders/123")
.check(
// 状态码检查
status.is(200),
status.in(200, 201, 204),
status.not(500),
// 响应时间检查
responseTimeInMillis.lt(1000),
// Header检查
header("Content-Type").is("application/json"),
// Body检查 - JSON
jsonPath("$.code").is("0"),
jsonPath("$.data.orderId").exists,
jsonPath("$.data.items[*]").count.gte(1),
jsonPath("$.data.totalAmount").ofType[Double].gt(0.0),
// Body检查 - 正则
regex(""""orderId":"(\d+)"""").saveAs("orderId"),
// Body检查 - 子串
substring("success").exists,
// 保存响应体
bodyString.saveAs("responseBody")
)Check操作符速查:
| 操作符 | 说明 | 示例 |
|---|---|---|
is | 等于 | status.is(200) |
not | 不等于 | status.not(500) |
in | 在列表中 | status.in(200, 201) |
exists | 存在 | jsonPath("$.id").exists |
notExists | 不存在 | jsonPath("$.error").notExists |
lt / lte | 小于/小于等于 | responseTimeInMillis.lt(1000) |
gt / gte | 大于/大于等于 | jsonPath("$.count").ofType[Int].gte(1) |
saveAs | 保存为变量 | jsonPath("$.token").saveAs("token") |
transform | 转换 | jsonPath("$.price").transform(_.toDouble * 100) |
4.5 Session变量 —— 请求之间传递数据
val scn = scenario("完整购物流程")
// 登录,保存token
.exec(
http("登录")
.post("/api/login")
.body(StringBody("""{"username":"test","password":"123456"}"""))
.check(jsonPath("$.token").saveAs("token"))
)
// 使用token查询商品
.exec(
http("查询商品")
.get("/api/products")
.header("Authorization", "Bearer ${token}")
.check(jsonPath("$.data[0].id").saveAs("productId"))
)
// 使用token和productId下单
.exec(
http("创建订单")
.post("/api/orders")
.header("Authorization", "Bearer ${token}")
.body(StringBody("""{"productId":"${productId}","quantity":1}"""))
.check(status.is(201))
)
// 手动设置Session变量
.exec(session => {
val newSession = session.set("customVar", "customValue")
println(s"当前用户Token: ${session("token").as[String]}")
newSession
})4.6 Feeder(数据源)—— 让每个虚拟用户都不一样
如果所有虚拟用户都用同一个账号登录,那不叫压测,叫"集体作弊"。Feeder让每个用户拥有不同的测试数据:
users.csv"] JSON["JSON Feeder
data.json"] JDBC["JDBC Feeder
数据库"] CUSTOM["Custom Feeder
代码生成"] end CSV --> SESSION["Session"] JSON --> SESSION JDBC --> SESSION CUSTOM --> SESSION SESSION --> REQUEST["HTTP Request
${username}, ${password}"] style CSV fill:#1dd1a1,color:#fff style JSON fill:#54a0ff,color:#fff style JDBC fill:#feca57,color:#333 style CUSTOM fill:#ff6b6b,color:#fff
CSV Feeder
准备数据文件 src/test/resources/data/users.csv:
username,password,email
user001,pass001,user001@test.com
user002,pass002,user002@test.com
user003,pass003,user003@test.com使用:
// 定义Feeder
val userFeeder = csv("data/users.csv").circular // circular: 循环使用
val scn = scenario("多用户登录测试")
.feed(userFeeder) // 每个虚拟用户取一行数据
.exec(
http("登录")
.post("/api/login")
.body(StringBody(
"""{"username":"${username}","password":"${password}"}"""
))
.check(status.is(200))
)Feeder策略:
| 策略 | 说明 | 使用场景 |
|---|---|---|
queue | 顺序取,用完报错 | 数据量已知,不允许重复 |
random | 随机取 | 数据充足,随机模拟 |
circular | 循环取 | 数据少,用户多 |
shuffle | 打乱后顺序取 | 需要随机但不重复 |
JSON Feeder
val jsonFeeder = jsonFile("data/products.json").random代码生成Feeder
// 动态生成数据
val randomFeeder = Iterator.continually(Map(
"username" -> s"user_${java.util.UUID.randomUUID()}",
"email" -> s"${System.currentTimeMillis()}@test.com",
"amount" -> (scala.util.Random.nextDouble() * 1000).formatted("%.2f")
))
val scn = scenario("动态数据测试")
.feed(randomFeeder)
.exec(
http("注册")
.post("/api/register")
.body(StringBody(
"""{"username":"${username}","email":"${email}"}"""
))
)五、注入策略(Injection)—— 流量怎么打
这是设计压测场景的核心,决定了"用多少人、怎么进来":
立即注入n个用户"] A2["rampUsers(n).during(d)
d时间内线性增加到n用户"] A3["constantUsersPerSec(r).during(d)
每秒r个用户,持续d"] A4["rampUsersPerSec(r1).to(r2).during(d)
每秒用户数从r1增到r2"] A5["heavisideUsers(n).during(d)
S曲线注入n用户"] end subgraph 封闭式注入["Closed Model(封闭式)"] B1["constantConcurrentUsers(n).during(d)
保持n个并发用户"] B2["rampConcurrentUsers(n1).to(n2).during(d)
并发数从n1增到n2"] end style A1 fill:#6c5ce7,color:#fff style A2 fill:#00b894,color:#fff style A3 fill:#0984e3,color:#fff style A4 fill:#e17055,color:#fff style A5 fill:#fdcb6e,color:#333 style B1 fill:#a29bfe,color:#fff style B2 fill:#55efc4,color:#333
常用注入策略示例
setUp(
scn.inject(
// ===== 基础策略 =====
atOnceUsers(100), // 立即100个用户冲进来(模拟瞬时流量)
nothingFor(5.seconds), // 等5秒(过渡期)
rampUsers(500).during(30.seconds), // 30秒内逐渐增加到500用户
// ===== 恒定速率 =====
constantUsersPerSec(20).during(2.minutes), // 每秒20个新用户,持续2分钟
// ===== 递增速率(找断点用)=====
rampUsersPerSec(10).to(100).during(1.minute), // 每秒用户数从10增到100
// ===== S曲线(更真实的流量增长)=====
heavisideUsers(1000).during(20.seconds), // S曲线分布1000用户
// ===== 阶梯式增长 =====
incrementUsersPerSec(10) // 每次增加10用户/秒
.times(5) // 增加5次
.eachLevelLasting(30.seconds) // 每级持续30秒
.separatedByRampsLasting(10.seconds) // 级间过渡10秒
.startingFrom(10) // 从10用户/秒开始
)
)典型场景配置
场景一:负载测试(Load Testing)
// 模拟日常流量:逐渐上量 -> 稳定 -> 逐渐下降
setUp(
scn.inject(
rampUsers(100).during(30.seconds), // 热身:30秒上到100用户
constantUsersPerSec(50).during(5.minutes), // 稳定:保持每秒50用户
rampUsers(0).during(30.seconds) // 冷却:逐渐停止
)
)场景二:压力测试(Stress Testing)
// 阶梯式加压,找到系统断点
setUp(
scn.inject(
incrementUsersPerSec(20)
.times(10) // 加压10次
.eachLevelLasting(1.minute) // 每级1分钟
.separatedByRampsLasting(10.seconds)
.startingFrom(20) // 从20用户/秒开始,最高200用户/秒
)
).assertions(
global.failedRequests.percent.lt(1) // 失败率超过1%就是断点
)场景三:尖峰测试(Spike Testing)
// 模拟秒杀场景:瞬时大量用户涌入
setUp(
scn.inject(
nothingFor(5.seconds), // 平静期
atOnceUsers(1000), // 突然1000人冲进来
nothingFor(30.seconds), // 观察系统反应
atOnceUsers(2000), // 再来一波
nothingFor(1.minute) // 继续观察
)
)场景四:浸泡测试(Soak Testing)
// 长时间稳定流量,发现内存泄漏等问题
setUp(
scn.inject(
rampUsers(100).during(1.minute), // 上量
constantUsersPerSec(30).during(4.hours) // 稳定跑4小时
)
)六、流程控制 —— 不只是线性执行
6.1 暂停(模拟用户思考时间)
.pause(3) // 固定暂停3秒
.pause(1, 5) // 随机暂停1-5秒
.pause(2.seconds, 5.seconds) // 同上,更明确
.pause(normalPausesWithStdDevDuration(3.seconds, 1.second)) // 正态分布6.2 循环
// 固定次数循环
.repeat(10, "index") { // 循环10次,index为计数器(0-9)
exec(
http("第${index}次请求")
.get("/api/items/${index}")
)
}
// 遍历列表
.foreach(List("apple", "banana", "cherry"), "fruit") {
exec(
http("查询${fruit}")
.get("/api/search?q=${fruit}")
)
}
// 条件循环
.asLongAs(session => session("hasMore").as[Boolean]) {
exec(
http("获取下一页")
.get("/api/list?page=${page}")
.check(jsonPath("$.hasMore").saveAs("hasMore"))
)
}
// 永远循环(配合duration使用)
.forever {
exec(http("心跳").get("/api/ping"))
.pause(5)
}6.3 条件分支
// if-else
.doIf(session => session("userType").as[String] == "VIP") {
exec(http("VIP专属接口").get("/api/vip/benefits"))
}.doElse {
exec(http("普通接口").get("/api/common/info"))
}
// 基于Session变量
.doIf("${isLoggedIn}") {
exec(http("已登录用户").get("/api/profile"))
}
// switch
.doSwitch("${userLevel}")(
"bronze" -> exec(http("青铜").get("/api/bronze")),
"silver" -> exec(http("白银").get("/api/silver")),
"gold" -> exec(http("黄金").get("/api/gold"))
)
// 随机选择
.randomSwitch(
60.0 -> exec(http("60%概率").get("/api/path1")),
30.0 -> exec(http("30%概率").get("/api/path2")),
10.0 -> exec(http("10%概率").get("/api/path3"))
)
// 均匀随机
.uniformRandomSwitch(
exec(http("选项1").get("/api/opt1")),
exec(http("选项2").get("/api/opt2")),
exec(http("选项3").get("/api/opt3"))
)6.4 错误处理
// 出错后退出当前场景
.exitHereIfFailed
// 尝试-恢复
.tryMax(3) { // 最多重试3次
exec(http("可能失败的请求").get("/api/unstable"))
}.exitHereIfFailed
// 出错时执行备用逻辑
.doIfOrElse(session => session.contains("token")) {
exec(http("正常请求").get("/api/data"))
} {
exec(http("重新登录").post("/api/login"))
}七、多场景组合
真实业务往往有多种用户行为,可以组合多个场景:
class MultiScenarioSimulation extends Simulation {
val httpProtocol = http.baseUrl("https://api.example.com")
// 场景1:浏览用户(只看不买)
val browseScn = scenario("浏览用户")
.exec(http("首页").get("/"))
.pause(2, 5)
.exec(http("商品列表").get("/api/products"))
.pause(1, 3)
.exec(http("商品详情").get("/api/products/1"))
// 场景2:购买用户(完整购买流程)
val buyScn = scenario("购买用户")
.exec(http("登录").post("/api/login").body(StringBody("""...""")))
.pause(1)
.exec(http("加入购物车").post("/api/cart/add"))
.pause(2)
.exec(http("结算").post("/api/checkout"))
.exec(http("支付").post("/api/pay"))
// 场景3:管理员(后台操作)
val adminScn = scenario("管理员")
.exec(http("管理后台").get("/admin/dashboard"))
.exec(http("订单管理").get("/admin/orders"))
// 组合:70%浏览,25%购买,5%管理员
setUp(
browseScn.inject(rampUsers(700).during(1.minute)),
buyScn.inject(rampUsers(250).during(1.minute)),
adminScn.inject(rampUsers(50).during(1.minute))
).protocols(httpProtocol)
}八、断言(Assertions)—— 设置性能基线
setUp(scn.inject(...))
.protocols(httpProtocol)
.assertions(
// 全局断言
global.responseTime.max.lt(5000), // 最大响应时间 < 5秒
global.responseTime.mean.lt(1000), // 平均响应时间 < 1秒
global.responseTime.percentile(95).lt(2000), // P95 < 2秒
global.responseTime.percentile(99).lt(3000), // P99 < 3秒
global.successfulRequests.percent.gt(99), // 成功率 > 99%
global.requestsPerSec.gte(100), // QPS >= 100
// 针对特定请求的断言
details("登录接口").responseTime.max.lt(1000),
details("查询订单").failedRequests.count.lt(10),
// 针对分组的断言
forAll.responseTime.max.lt(3000) // 所有请求都要满足
)断言失败时,Gatling会以非0状态码退出,非常适合CI/CD流水线。
九、报告解读 —— 这些曲线到底在说什么
运行完测试后,打开生成的HTML报告:
总体统计"] TIMELINE["📈 Response Time Ranges
响应时间分布"] STATS["📋 Statistics
详细统计"] ACTIVE["👥 Active Users
活跃用户数"] RPS["⚡ Requests per Second
每秒请求数"] RESP["⏱️ Response Time Distribution
响应时间详情"] end GLOBAL --> TIMELINE TIMELINE --> STATS STATS --> ACTIVE ACTIVE --> RPS RPS --> RESP style GLOBAL fill:#6c5ce7,color:#fff style RPS fill:#00b894,color:#fff style RESP fill:#e17055,color:#fff
9.1 关键指标解读
| 指标 | 含义 | 健康标准 |
|---|---|---|
| Total Requests | 总请求数 | - |
| OK / KO | 成功/失败数 | KO越少越好 |
| Mean Response Time | 平均响应时间 | 根据业务,一般<500ms |
| Std Deviation | 标准差 | 越小越稳定 |
| Percentile 50/75/95/99 | 响应时间百分位 | P99是重点 |
| Requests/sec | QPS/TPS | 越高越好 |
| Response Time Distribution | 响应时间分布 | 看是否有长尾 |
9.2 响应时间范围
报告中的颜色含义:
🟢 t < 800ms - 优秀
🔵 800ms < t < 1200ms - 良好
🟡 t > 1200ms - 需关注
🔴 Failed - 失败请求9.3 典型问题诊断
十、实战案例:网关性能对比测试
假设我们要对比Kong、Nginx、Spring Cloud Gateway的性能:
class GatewayComparisonSimulation extends Simulation {
// 测试参数
val testDuration = 2.minutes
val maxUsers = 500
// 三种网关配置
val kongProtocol = http.baseUrl("http://kong-gateway:8000")
val nginxProtocol = http.baseUrl("http://nginx-gateway:80")
val scgProtocol = http.baseUrl("http://scg-gateway:8080")
// 统一的测试场景
def createScenario(name: String) = scenario(name)
.exec(
http("健康检查")
.get("/api/health")
.check(status.is(200))
)
.pause(100.milliseconds, 500.milliseconds)
.exec(
http("获取数据")
.get("/api/data")
.check(
status.is(200),
jsonPath("$.code").is("0")
)
)
// 统一的注入策略
val injection = Seq(
rampUsers(maxUsers).during(30.seconds),
constantUsersPerSec(maxUsers / 10).during(testDuration)
)
// 分别测试(每次只运行一个,避免相互影响)
setUp(
createScenario("Kong Gateway").inject(injection: _*).protocols(kongProtocol)
// createScenario("Nginx Gateway").inject(injection: _*).protocols(nginxProtocol)
// createScenario("Spring Cloud Gateway").inject(injection: _*).protocols(scgProtocol)
).assertions(
global.responseTime.percentile(95).lt(500),
global.successfulRequests.percent.gt(99)
)
}十一、CI/CD集成
Jenkins Pipeline
pipeline {
agent any
stages {
stage('Build') {
steps {
sh 'mvn clean compile'
}
}
stage('Performance Test') {
steps {
sh 'mvn gatling:test -Dgatling.simulationClass=simulations.LoadTestSimulation'
}
post {
always {
gatlingArchive() // 需要Gatling Jenkins插件
}
}
}
}
}GitHub Actions
name: Performance Test
on:
push:
branches: [main]
schedule:
- cron: '0 2 * * *' # 每天凌晨2点跑
jobs:
gatling:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up JDK 11
uses: actions/setup-java@v3
with:
java-version: '11'
distribution: 'temurin'
- name: Run Gatling tests
run: mvn gatling:test
- name: Upload Report
uses: actions/upload-artifact@v3
if: always()
with:
name: gatling-report
path: target/gatling/*/十二、常见问题与最佳实践
Q1: Scala不会怎么办?
别慌,Gatling的DSL设计得很直观,你只需要掌握:
- 链式调用(一直
.下去) - 变量插值(
${变量名}) - Lambda表达式(
session => ...)
基本上照着例子改改就能用。
Q2: 单机能模拟多少用户?
取决于:
- 测试场景复杂度
- 机器配置(CPU、内存、网络)
- 目标服务器响应速度
一般来说,一台普通服务器(8核16G)模拟5000-10000并发用户没问题。需要更高并发可以用分布式模式。
Q3: 压测结果波动很大怎么办?
- 多跑几次取平均值
- 避免在共享环境压测
- 检查是否有其他程序抢资源
- 压测前先热身(warmup)
Q4: 生产环境能压测吗?
谨慎! 建议:
- 使用与生产隔离的环境
- 如果必须压生产,选择低峰期
- 准备好快速停止的手段
- 监控各项指标,异常立即停止
最佳实践清单
✅ 测试脚本版本控制(Git管理)
✅ 使用参数化数据,避免缓存命中
✅ 设置合理的思考时间(pause)
✅ 定义明确的断言(assertions)
✅ 监控目标服务器资源(CPU、内存、网络)
✅ 逐步加压,不要一上来就满载
✅ 保存每次测试报告,便于对比
✅ 压测前确认环境一致性
❌ 不要在共享环境压测
❌ 不要只看平均值,要看P99
❌ 不要忽略错误率十三、总结
Gatling不只是一个工具,它是一种质量保障的思维方式:
- 代码即配置:测试脚本可以Git管理、Code Review
- 持续验证:集成到CI/CD,每次发布都跑一遍
- 数据说话:用P99、QPS这些数字来衡量性能
- 提前暴露:在测试环境发现问题,而不是在生产环境被用户发现
记住,不做压测的上线,就是在生产环境做压测,而用户就是你的测试数据。
这代价,你承受得起吗?