搜 索

加特林(Gatling)使用指南

  • 386阅读
  • 2022年05月14日
  • 0评论
首页 / 编程 / 正文
本文属于《从入门到放弃》系列,但这次我建议你坚持到最后,因为不会压测的后端,就像不会验孕的产科医生——你永远不知道系统什么时候会"生"出问题。

前言:一个没做压测的项目上线后

产品经理:"这次大促我们预计有10万用户同时在线。"

你:"没问题,我优化过SQL了。"

大促当天,流量进来的瞬间,服务器CPU直接拉满,数据库连接池爆了,Redis也跟着躺平,整个系统以一种非常优雅的姿势——502 Bad Gateway

老板在群里@所有人:"谁能解释一下?"

你开始疯狂翻日志,心里默念:"早知道做个压测啊..."

今天,我们就来聊聊如何用Gatling,在上线前把系统"打"一遍,让Bug在测试环境暴露,而不是在用户面前表演。

一、什么是Gatling?

Gatling是一款用Scala编写、基于AkkaNetty的高性能压测工具,主要用于HTTP服务的负载测试和性能分析。

graph LR subgraph Gatling架构 DSL["Scala DSL
测试脚本"] 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?

特性GatlingJMeter
编写方式代码即配置(Scala DSL)GUI拖拽 + XML
资源消耗低(异步非阻塞)高(每用户一个线程)
并发能力单机轻松上万单机几千就开始吃力
报告质量精美的HTML报告需要插件美化
学习曲线需要会点Scala上手简单
版本控制代码友好,Git管理XML地狱
CI/CD集成Maven/Gradle原生支持需要额外配置

一句话总结:JMeter适合快速上手搞一搞,Gatling适合认真做性能测试并纳入CI流程。

二、性能测试的几种类型

在开始学Gatling之前,先搞清楚性能测试到底在测什么:

graph TB subgraph 性能测试类型 LT["🏋️ Load Testing
负载测试"] 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.html

3.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.MyFirstSimulation

3.4 方式三:Recorder录制

如果你懒得手写脚本,可以用Recorder录制浏览器操作:

./bin/recorder.sh

Recorder界面主要配置项:

graph TB subgraph Recorder["🎬 Gatling Recorder"] subgraph 代理设置 LP["Local Port: 8000"] HTTPS["HTTPS Mode: Certificate Authority"] end subgraph 输出设置 PKG["Package: simulations"] CLS["Class Name: RecordedSimulation"] OUT["Output: user-files/simulations"] end subgraph 过滤设置 BL["Blacklist: .*\\.css, .*\\.js, .*\\.ico"] WL["Whitelist: 留空或指定域名"] end subgraph 控制按钮 START["▶️ Start"] STOP["⏹️ Stop & Save"] end end style Recorder fill:#1a1a2e,color:#fff style START fill:#00b894,color:#fff style STOP fill:#e17055,color:#fff

使用步骤

  1. 启动Recorder,设置本地代理端口(默认8000)
  2. 浏览器配置HTTP代理指向 localhost:8000
  3. 点击 Start 开始录制
  4. 在浏览器中执行业务操作
  5. 点击 Stop 停止并生成Scala脚本

配置代理后,浏览器的所有请求都会被记录下来,自动生成Scala脚本。适合复杂业务流程的快速录制,但生成的代码通常需要手动优化。

四、Gatling DSL 详解

这是Gatling的精华部分,理解了DSL,你就掌握了Gatling的80%。

4.1 脚本结构总览

graph TB subgraph Simulation["🎬 Simulation(测试场景)"] HTTP["httpProtocol
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变量 —— 请求之间传递数据

sequenceDiagram participant S as Session participant R1 as 登录请求 participant R2 as 查询请求 participant R3 as 下单请求 R1->>S: saveAs(token) S->>R2: 传递 token R2->>S: saveAs(productId) S->>R3: 传递 token, productId
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让每个用户拥有不同的测试数据:

graph LR subgraph Feeders CSV["CSV 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)—— 流量怎么打

这是设计压测场景的核心,决定了"用多少人、怎么进来":

graph TB subgraph 开放式注入["Open Model(开放式)"] A1["atOnceUsers(n)
立即注入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报告:

graph TB subgraph 报告结构 GLOBAL["📊 Global Information
总体统计"] 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/secQPS/TPS越高越好
Response Time Distribution响应时间分布看是否有长尾

9.2 响应时间范围

报告中的颜色含义:

🟢 t < 800ms        - 优秀
🔵 800ms < t < 1200ms - 良好
🟡 t > 1200ms       - 需关注
🔴 Failed           - 失败请求

9.3 典型问题诊断

graph TD A["响应时间陡增"] --> A1["连接池耗尽?"] A --> A2["数据库慢查询?"] A --> A3["GC停顿?"] B["错误率飙升"] --> B1["超时?"] B --> B2["限流?"] B --> B3["服务崩溃?"] C["QPS上不去"] --> C1["带宽瓶颈?"] C --> C2["CPU满了?"] C --> C3["线程池满了?"] style A fill:#ff6b6b,color:#fff style B fill:#feca57,color:#333 style C fill:#54a0ff,color:#fff

十、实战案例:网关性能对比测试

假设我们要对比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
❌ 不要忽略错误率

十三、总结

graph LR A["写好代码"] --> B["压测验证"] B --> C{"性能达标?"} C -->|是| D["安心上线"] C -->|否| E["优化代码"] E --> B D --> F["大促稳如狗"] style A fill:#6c5ce7,color:#fff style B fill:#00b894,color:#fff style D fill:#1dd1a1,color:#fff style F fill:#feca57,color:#333

Gatling不只是一个工具,它是一种质量保障的思维方式

  1. 代码即配置:测试脚本可以Git管理、Code Review
  2. 持续验证:集成到CI/CD,每次发布都跑一遍
  3. 数据说话:用P99、QPS这些数字来衡量性能
  4. 提前暴露:在测试环境发现问题,而不是在生产环境被用户发现

记住,不做压测的上线,就是在生产环境做压测,而用户就是你的测试数据。

这代价,你承受得起吗?


参考资料

评论区
暂无评论
avatar