关于 gatling-best-practices
gatling-best-practices 是用于编写和审查 Gatling 仿真的技能,支持 Java、Kotlin、Scala、JavaScript 和 TypeScript。它采用统一的五部分结构,并检查常见配置与负载测试错误。
主要功能
- 01支持五种 Gatling 语言
- 02采用五部分仿真结构
- 03按测试目标选择注入配置
- 04检查常见仿真错误
- 05提供运行命令与验证指引
适用场景
- 01创建完整的负载测试仿真
- 02审查现有 Gatling 项目
- 03为测试目标选择注入配置
- 04运行前发现配置问题
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ rcampos09/ performance-testing-skills --skill gatling-best-practices安装后在 Claude Code 中输入/gatling-best-practices确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
evals/evals.jsonreferences/DESIGN-PATTERNS.mdPROTOCOLS.mdscripts/scaffold.ps1scaffold.shvalidate.sh详细介绍
Gatling 场景构建器
为所有五种官方支持的语言强制采用一致、可用于生产环境的 Gatling 仿真模式:Java、Kotlin、Scala、JavaScript 和 TypeScript。
输出格式
生成或修复仿真时,始终提供以下三项:
- 一份完整、可运行的仿真文件,使用所选语言编写,绝不能只给出部分代码片段。用户应能直接复制并立即运行。
- 准确的运行命令,以及所需的环境参数(
baseUrl、users等)。 - 一行说明,解释所选注入配置及其如何满足指定的负载目标。
步骤 1 — 收集背景信息
只询问尚不明确的信息。常见问题:
- 语言: Java(最常见) · Kotlin · Scala · JavaScript · TypeScript
- 构建工具: Maven · Gradle · npm(仅限 JS/TS)
- 协议: HTTP/REST · WebSocket · MQTT · JMS
- 目标: 从零开始创建新项目 · 修复现有仿真 · 具体 DSL 问题
仅当用户提到 WebSocket、MQTT 或 JMS 时,才加载 references/PROTOCOLS.md。 其中包含这些协议的依赖声明和 DSL,以及用户可能会遗漏的非显而易见的设置步骤。
仅当用户询问文件夹结构、项目架构、关注点分离,或如何扩展到单个仿真文件之外时,才加载 references/DESIGN-PATTERNS.md。 其中包含模块化分层模式(Config → Requests → Scenarios → Simulations),以及 Java、Scala 和 TypeScript 示例。
步骤 2 — 新项目?使用脚手架脚本
脚手架脚本是交互式的——需要键盘输入,无法以非交互方式运行。请告知用户在自己的终端中运行脚本;不要尝试使用 Bash 工具执行:
# macOS / Linux
bash scripts/scaffold.sh
# Windows
.\scripts\scaffold.ps1
如果用户要求你直接生成项目(不运行脚本),请跳到步骤 3,按照 5 块模式手动创建文件。
Kotlin + Gradle — 必需的最少文件:
build.gradle.kts — 有三项遗漏会导致每次构建失败:
plugins {
kotlin("jvm") version "2.0.21" // ① must come BEFORE the Gatling plugin
id("io.gatling.gradle") version "3.14.5"
}
group = "perf"
version = "1.0.0-SNAPSHOT"
repositories {
mavenCentral() // ② required — often forgotten
}
gatling {
jvmArgs = listOf(
"-Xms512m", "-Xmx2g",
"--add-opens=java.base/java.lang=ALL-UNNAMED" // ③ Java 21+ module restriction
)
}
dependencies {
gatling("io.gatling.highcharts:gatling-charts-highcharts:3.14.5")
}
源代码目录(Gatling Gradle 插件使用的目录——不是 src/test/):
- Kotlin 仿真 →
src/gatling/kotlin/<package>/ - 资源(gatling.conf、feeder)→
src/gatling/resources/
对于 JS/TS 项目,必需的最少文件包括:
package.json,其中包含"type": "module"以及依赖项@gatling.io/cli、@gatling.io/core、@gatling.io/httpsrc/<SimulationName>.gatling.js(或.gatling.ts)——文件必须直接位于src/中,并使用.gatling.js/.gatling.ts后缀(CLI 要求)tsconfig.json——仅限 TypeScript,esbuild-plugin-tsc必需;省略后会在打包时导致failed to open 'undefined'
完成脚手架搭建后——或用户分享现有 Gatling 项目时——使用 Bash 工具执行验证器(不要读取验证器文件,只有输出会消耗 token):
bash scripts/validate.sh [project-dir]
报告结果。该脚本会捕获五种最常见的配置问题,避免用户在运行失败的项目上浪费时间。
步骤 3 — 应用 5 块模式
每个仿真都必须按以下顺序包含这五块。先为所选语言生成完整框架,再填入细节——从部分文件开始容易导致结构错误。
Block 1 → Protocol baseUrl, headers, connection settings
Block 2 → Feeders test data injected per virtual user
Block 3 → Scenario ordered chain of requests with pauses and checks
Block 4 → Injection how many users, at what rate, for how long
Block 5 → Assertions pass/fail thresholds (success rate, response time p95)
常见错误 — 检查每个仿真
这五种错误几乎出现在所有 Gatling 仿真的初稿中。交付代码前,请逐一检查。
1. 请求之间缺少 pause()
如果没有思考时间,所有请求都会以可能达到的最高速率发出,产生比真实用户高 10–100 倍的负载。这会使结果失去意义,还可能导致被测系统崩溃。
// Wrong
scenario("Flow").exec(http("A").get("/a")).exec(http("B").get("/b"))
// Correct — add realistic think time between actions
scenario("Flow").exec(http("A").get("/a")).pause(1, 3).exec(http("B").get("/b"))
2. 硬编码动态令牌
硬编码令牌意味着每个虚拟用户发送相同的会话——服务器看到的是同一个用户重复出现,而不是多个不同的用户。CSRF 令牌和 JWT 会在服务器端验证,必须来自实际的登录响应。
// Wrong — static token shared across all users
.header("Authorization", "Bearer eyJhbGciOiJIUzI1NiJ9.abc123")
// Correct — extract per user from the login response
.exec(http("Login").post("/auth/login")
.check(jsonPath("$.token").saveAs("token")))
.exec(http("API Call").get("/data")
.header("Authorization", "Bearer #{token}"))
3. 负载测试使用 atOnceUsers
atOnceUsers 会让所有用户同时发起请求。它只适用于冒烟测试(2–5 个用户)。在真实负载测试中使用它会产生不切实际的流量峰值,无法反映系统容量。
// Wrong — not a load test, just a spike
setUp(scn.injectOpen(atOnceUsers(100)))
// Correct — ramp up, then hold to measure steady-state capacity
setUp(scn.injectOpen(
rampUsers(100).during(60),
constantUsersPerSec(10).during(120)
))
4. 没有断言
没有断言时,即使每个请求都返回 500,Gatling 仍会以代码 0(成功)退出。这意味着 CI/CD 流水线永远无法发现性能回归。请在测试运行前定义怎样才算“通过”。
// Wrong — always exits 0 regardless of results
setUp(scn.inject(...).protocols(httpProtocol))
// Correct — fail the build if thresholds are breached
setUp(scn.inject(...).protocols(httpProtocol))
.assertions(
global().successfulRequests().percent().gt(99.0),
global().responseTime().percentile(95).lt(1000)
)
5. 长时间测试使用 .queue() feeder 策略
.queue() 会按顺序使用 CSV 中的每条记录,且每条只使用一次。文件中的记录耗尽后,测试会在运行中途失败。对于可能运行得比记录数量所允许的时间更长的测试,请使用 .circular()。
// Wrong — crashes when file is exhausted
FeederBuilder<String> f = csv("data/users.csv").queue()
// Correct for sustained tests — loops back to the start
FeederBuilder<String> f = csv("data/users.csv").circular()
// Use .queue() only when each record must be unique (e.g., user registration)
6. 检查失败会将请求标记为 FAILED
.check() 找不到目标时,会导致整个请求失败——即使服务器返回了 200。这会在不知不觉中抬高错误率,并掩盖真正的问题:字段缺失或路径错误。
// Wrong — if $.token is absent (e.g., login failed), request is marked FAILED
.check(jsonPath("$.token").saveAs("token"))
// Correct — validate existence first so the error message is meaningful
.check(status().is(200))
.check(jsonPath("$.token").exists())
.check(jsonPath("$.token").saveAs("token"))
// When the field is genuinely optional — use .optional() to avoid false failures
.check(jsonPath("$.refreshToken").optional().saveAs("refreshToken"))
7. 发送请求体时缺少 Content-Type
POST/PUT 请求忘记设置 Content-Type 会导致服务器以 415 Unsupported Media Type 拒绝请求。使用 .asJson()——一次调用即可设置 Content-Type 和 Accept 请求头。
// Wrong — server returns 415
.post("/api/users").body(StringBody("""{"name":"#{name}"}"""))
// Correct — use .asJson() shorthand (Java / Kotlin only)
.post("/api/users").body(StringBody("""{"name":"#{name}"}""")).asJson()
// Equivalent explicit form
.post("/api/users")
.header("Content-Type", "application/json")
.body(StringBody("""{"name":"#{name}"}"""))
**Scala 注意事项:**在 Scala 中,.asJson() 不能接在 .body() 后面调用,否则会导致编译错误。请在协议上设置 Content-Type:
// Scala — set contentTypeHeader on the protocol, not per-request
val httpProtocol = http
.baseUrl(sys.props.getOrElse("baseUrl", "https://api.example.com"))
.acceptHeader("application/json")
.contentTypeHeader("application/json") // applies to all requests
// Then just use .body() without .asJson():
http("POST Login").post("/api/auth/login")
.body(StringBody("""{"email":"#{email}","password":"#{password}"}"""))
.check(status.is(200))
5 块模式——参考
块 1:协议
// Java / Kotlin
HttpProtocolBuilder httpProtocol = http
.baseUrl(System.getProperty("baseUrl", "https://api.example.com"))
.acceptHeader("application/json")
.contentTypeHeader("application/json");
// Scala
val httpProtocol = http
.baseUrl(sys.props.getOrElse("baseUrl", "https://api.example.com"))
.acceptHeader("application/json")
// TypeScript / JavaScript
// File must be named *.gatling.ts / *.gatling.js and placed directly in src/
import {
simulation, scenario, rampUsers, csv, global,
StringBody, jsonPath, getEnvironmentVariable, // ← jsonPath and StringBody live here, NOT in @gatling.io/http
} from "@gatling.io/core";
import { http, status } from "@gatling.io/http"; // ← do NOT import jsonPath from here — it is not exported
// process.env is NOT available at GraalVM runtime — use getEnvironmentVariable instead
const httpProtocol = http
.baseUrl(getEnvironmentVariable("BASE_URL", "https://api.example.com"))
.acceptHeader("application/json");
块 2:Feeders
csv("data/users.csv").circular() // sustained tests: loops forever (recommended)
csv("data/users.csv").random() // picks records randomly, allows repeats
csv("data/users.csv").queue() // each record used once — only for unique data
csv("data/users.csv").shuffle() // random order, each used once
// Programmatic feeder — when each user needs a unique generated value
Iterator<Map<String, Object>> feeder =
Stream.generate(() -> Map.<String, Object>of("id", UUID.randomUUID().toString()))
.iterator();
块 3:场景
// Java
ScenarioBuilder scn = scenario("My Flow")
.feed(userFeeder)
.exec(http("POST Login")
.post("/auth/login")
.body(StringBody("""{"username":"#{username}","password":"#{password}"}"""))
.check(status().is(200))
.check(jsonPath("$.token").saveAs("token"))) // extract token for reuse
.pause(1, 3) // think time
.exec(http("GET Data")
.get("/data")
.header("Authorization", "Bearer #{token}") // inject extracted token
.check(status().is(200))
.check(jsonPath("$.id").saveAs("resourceId")))
.pause(1)
.exec(http("POST Action")
.post("/actions")
.header("Authorization", "Bearer #{token}")
.body(StringBody("""{"resourceId":"#{resourceId}"}"""))
.check(status().is(201)));
// TypeScript
const scn = scenario("My Flow")
.feed(userFeeder)
.exec(http("POST Login").post("/auth/login")
.body(StringBody('{"username":"#{username}","password":"#{password}"}')) // ← StringBody required; raw string causes NullPointerException
.asJson() // ← sets Content-Type: application/json
.check(status().is(200))
.check(jsonPath("$.token").exists())
.check(jsonPath("$.token").saveAs("token")))
.pause(1, 3)
.exec(http("GET Data").get("/data")
.header("Authorization", "Bearer #{token}")
.check(status().is(200)));
循环和条件语句:
repeat(3).on(exec(http("Poll").get("/status"))) // fixed iterations
during(Duration.ofSeconds(30)).on( // time-based loop
exec(http("Ping").get("/ping")).pause(5))
doIf("#{isPremium}").then(exec(http("VIP").get("/vip"))) // conditional branch
randomSwitch().on( // weighted paths
percent(60.0).exec(http("Browse").get("/products")),
percent(40.0).exec(http("Search").get("/search")))
group("Checkout Flow").on( // group for cleaner reports
exec(http("Cart").get("/cart"))
.exec(http("Pay").post("/pay")))
块 4:注入配置
根据测试目标选择匹配的配置——使用错误的配置会产生误导性结果。
injectOpen——控制到达速率(每秒新增用户数)。适用于 Web API 和无状态服务的默认选择。
| 配置 | 命令 | 使用场景 |
|---|---|---|
| 峰值(仅限冒烟测试) | atOnceUsers(5) | 验证测试能否运行——这不是负载测试 |
| 递增 | rampUsers(100).during(60) | 标准负载测试 |
| 稳定速率 | constantUsersPerSec(20).during(120) | 容量/浸泡测试 |
| 加速 | rampUsersPerSec(5).to(50).during(60) | 找出系统崩溃点 |
| 压力峰值 | stressPeakUsers(500).during(30) | 压力测试 |
| 阶梯式 | incrementUsersPerSec(5).times(5).eachLevelLasting(30) | 逐步测试容量 |
injectClosed——控制并发数量(同时处于活跃状态的用户数)。适用于具有连接池、队列或会话限制的系统。
| 配置 | 命令 | 使用场景 |
|---|---|---|
| 固定并发 | constantConcurrentUsers(50).during(120) | 固定连接池大小 |
| 递增并发 | rampConcurrentUsers(10).to(50).during(60) | 逐步提高并发数 |
| 阶梯式并发 | incrementConcurrentUsers(5).times(5).eachLevelLasting(30) | 逐步测试容量(闭合模型) |
**Scala 注意事项:**使用 .inject(...)——Scala 在调用处不区分 injectOpen/injectClosed;步骤类型决定模型。
节流——不受用户数影响,限制 RPS:
如果目标是以固定请求速率而非固定用户数进行测试,请使用 .throttle()。它会覆盖注入配置,适用于 SLA 合规性测试。
setUp(scn.injectOpen(constantUsersPerSec(50).during(Duration.ofMinutes(10))))
.throttle(
reachRps(100).in(Duration.ofSeconds(10)), // ramp to 100 RPS over 10s
holdFor(Duration.ofMinutes(5)) // hold at 100 RPS for 5 min
)
.protocols(httpProtocol);
暂停分布——根据所需的逼真程度进行选择:
.pause(1, 3) // uniform: between 1-3s (default)
.pause(Duration.ofSeconds(2),
PauseType.EXPONENTIAL) // exponential: closer to real user behavior
.pace(Duration.ofSeconds(5)) // cadence: fixed cycle regardless of response time
Block 5:断言
断言将测试变成通过/失败门槛。没有断言,测试就只是观察。至少包含第一行;对于关键路径,添加各端点的断言。
.assertions(
global().failedRequests().count().lt(1L), // minimum: zero errors
global().successfulRequests().percent().gt(99.0), // success rate
global().responseTime().percentile(95).lt(1000), // p95 < 1s
global().responseTime().percentile(99).lt(2000), // p99 < 2s
global().requestsPerSec().gt(50.0), // throughput floor
details("POST Login").responseTime().percentile(99).lt(500) // per-endpoint
)
使用 percentile(95) 和 percentile(99),而不是 mean()。平均值会掩盖长尾:当平均值为 200ms 时,p99 为 10 秒这一情况就会被隐藏。
Kotlin 说明: percentile() 需要一个 Double 参数——写成 .percentile(95.0).lt(1000),而不是 .percentile(95).lt(1000)(在 Kotlin 中,Int 会导致编译错误,尽管 Java 接受通过拓宽转换传入的值)。
Scala 说明: 断言方法(global、responseTime、successfulRequests、percent)是带有隐式 GatlingConfiguration 参数的零参数方法。在调用时不要加括号——在 Scala 中,global() 会导致编译错误:
// Wrong — compile error: "not enough arguments for method global"
global().successfulRequests().percent().gt(99.0)
// Correct — no () on zero-arg methods with implicit params
global.successfulRequests.percent.gt(99.0)
global.responseTime.percentile(95).lt(800)
details("POST Login").responseTime.percentile(99).lt(300)
运行命令
# Maven
mvn gatling:test -Dgatling.simulationClass=perf.MySimulation \
-DbaseUrl=https://staging.example.com -Dusers=50
# Gradle — plugin 3.14+ supports Gradle 9; use ./gradlew (or gradle) directly
./gradlew gatlingRun # runs all simulations
./gradlew gatlingRun-perf.MySimulation # specific class (plugin ≥ 3.14 only)
# TypeScript / JavaScript (use simulation name, not file path)
BASE_URL=https://staging.example.com USERS=50 \
npx gatling run --simulation MySimulation
报告位于:target/gatling/<simulation>-<timestamp>/index.html