☰
Gatling 3.0.0 压测脚本工程化实战指南
2026/10/14 12:09:58 网站建设 项目流程

简介:Gatling 3.0.0 是一套面向开发者、测试工程师及 DevOps 团队的现代性能测试工具,可用于模拟成千上万的并发用户,验证 Web 应用在高负载下的响应时间、吞吐量与资源利用率。压缩包共 114 个文件,大小约 57.17MB,内部以 97 个 jar 依赖包为运行核心,搭配 6 个 Scala 脚本源文件、3 个 conf 配置文件,以及 2 个 bat 和 2 个 sh 启动脚本,另有 license、csv 数据模板等辅助资源,解压即可在 Windows/Linux 环境快速搭建测试环境。资源内置了录制器与脚本配置模板,可简化测试场景创建过程;基于 Scala DSL 的脚本语言使场景逻辑清晰、易维护,配合配置模板可灵活调整负载参数,适合初学者快速上手,也便于团队统一维护。目前已获得 233 人浏览学习。借助内置的 Highcharts 图表报告,可直观查看 RPS、平均响应时间及百分位分布,同时提供详细的请求与响应日志,帮助快速定位瓶颈;若进一步接入 Jenkins 等 CI 工具,还能实现提交后自动回归压测。

1. Gatling 3.0.0 是什么:压测脚本工程化的首选落点

线上接口用浏览器点都正常,一上并发,95 分位响应时间直接翻倍——这种“单个请求没事、并发就翻车”的场景,最适合用 Gatling 3.0.0 来定位。Gatling 3.0.0 是当前把性能测试工程化最顺手的开源方案之一:脚本写 Scala 描述虚拟用户行为,底层用 Netty 异步 IO 制造压力,跑完自动生成带趋势图的 HTML 报告。3.0.0 这版的重点不是多几个函数,而是 DSL 全面类型安全、断言更严谨、并补上 closed 并发注入模型,让“恒定在线人数”的容量验证能在脚本里直接表达。它适合后端开发、测试和 DevOps 把压测脚本当产品代码评审入库;会写一点 HTTP 请求,十分钟就能跑通第一个压测。

2. Gatling 3.0.0 的最小工程与第一个 DSL 脚本:从 Maven 到报告落地

2.1 先搞懂 3.0.0 的 DSL 变了什么

Gatling 3.0.0 不是 2.x 换了个版本号。从旧资料里抄来的脚本,直接贴到 3.0.0 大概率编译不过。最明显的变化是 Session 属性读取:过去很多教程允许宽松读属性,3.0.0 的 API 强制你写成session("token").as[String],变量不存在或者类型不匹配,编译期就会暴露问题,而不是等压测跑到一半才崩。这个收紧动作让脚本更像产品代码,变量名写错不会等到请求失败才被察觉。

第二处关键变化是断言和压力注入模型的补全。lt、gt、between这类类型安全断言成为主推写法;注入阶段从以 open 模型为主,补上了constantConcurrentUsers、rampConcurrentUsers等 closed 模型,正好覆盖“系统在线人数恒定”的容量验证场景。还有一个工程化细节:脚本组织更强调setUp(...).protocols(...)的配置与场景分离模式,多个仿真类共用协议时,可以把公共配置抽成方法复用,这也是 3.0.0 里值得养成的习惯。

2.2 用 Maven 把 Gatling 3.0.0 装进项目

我一般不用独立发行包,而是把 Gatling 3.0.0 直接接进 Maven 工程。原因只有一个:脚本要能进 Git、能被同事 diff、能挂到 CI。独立发行包适合临时做验证,不适合当项目资产。最小 pom 配置如下:

<dependencies> <dependency> <groupId>io.gatling.highcharts</groupId> <artifactId>gatling-charts-highcharts</artifactId> <version>3.0.0</version> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>io.gatling</groupId> <artifactId>gatling-maven-plugin</artifactId> <version>3.0.0</version> </plugin> </plugins> </build>

依赖坐标用gatling-charts-highcharts而不是gatling-core,是因为 HTML 图表报告就在这个 artifact 里;只引 core 的话,跑完只有无图的数据文件。scope指定为test,它就不会被打进生产包。版本号 3.0.0 与依赖保持一致,避免插件和核心库版本错位导致的 DSL 编译失败。

脚本目录结构固定如下,仿真类需要放在带包名的目录下,不能放在默认包:

src/test/scala/ com/example/gatling/ BookSearchSimulation.scala src/test/resources/ data/ users.csv gatling.conf

gatling.conf没有特殊需求时可以不写,Gatling 3.0.0 会使用内置默认配置。这个目录结构是 Maven 插件扫描的约定:仿真类放src/test/scala,数据文件放src/test/resources。

2.3 第一个脚本:一次 GET 压测写透

现在写一个最小但完整的仿真类:

import io.gatling.core.Predef._ import io.gatling.http.Predef._ import scala.concurrent.duration._ class BookSearchSimulation extends Simulation { val httpProtocol = http .baseUrl("http://localhost:8080") .acceptHeader("application/json") val scn = scenario("图书搜索") .exec( http("搜索图书") .get("/api/books?keyword=gatling") .check(status.is(200)) ) setUp( scn.inject( atOnceUsers(1), rampUsers(20).during(10.seconds) ) ).protocols(httpProtocol) }

这段脚本要拆成三层看。httpProtocol是公共协议配置,baseUrl定了被测地址,之后的请求只要写路径就行;scenario定义虚拟用户行为,exec里的http("搜索图书")是这个请求的统计名称,报告里会用它聚合数据;setUp决定压力怎么给,atOnceUsers(1)先放 1 个用户立即跑一次,目的不是压测,而是验证脚本到服务的链路通不通。

rampUsers(20).during(10.seconds)是 10 秒内从 0 线性增长到 20 个虚拟用户。atOnceUsers(1)和rampUsers组合是从冒烟到施压的标准写法:先一个请求验证环境,再按阶段加压力。.check(status.is(200))是检查器,响应码不等于 200 就把这次请求记为失败。

运行命令很简单:

mvn gatling:test

如果工程里有多个仿真类,指定要跑哪个:

mvn gatling:test -Dgatling.simulationClass=com.example.gatling.BookSearchSimulation

第一次跑通的目标不是把并发调高,而是确认错误率为 0、请求真的打到了被测服务、报告能生成。这三点全绿,再开始调压力参数。

2.4 第一份报告:控制台和 HTML 里该看什么

Gatling 3.0.0 跑完会在控制台输出报告目录,路径类似target/gatling/booksearchsimulation-<时间戳>,打开其中的index.html就是完整图表报告。第一次看报告,别被一堆曲线淹没,先看这张表里的三个数:

指标含义关注点
响应时间分位数50 / 75 / 95 / 99 分位95 分位代表长尾体验,最容易暴露问题
吞吐量Requests/sec与业务预期 QPS 对比,判断是否达标
错误率KO 请求占比应该为 0,或至少处于下降趋势

如果报告目录没生成,先回到控制台看有没有报OutOfMemoryError,再看配置里的gatling.core.directory.reportsOnly这类输出路径有没有被改写。报告是 Gatling 压测的最终产出,跑完先确认它在,再关闭终端。

提示:Gatling 3.0.0 的报告按“仿真类名 + 时间戳”生成多份,别只看最新目录,按控制台输出的完整路径去开,避免误看旧报告。

3. 把单请求升级成真实业务链路:检查点、CSV 数据源与 Token 关联

3.1 用 check 把“响应对没对”变成硬性条件

单请求脚本能跑,只代表网络通了,不代表业务对了。很多后端的查询接口在参数错误时也会返回 200,但code字段不是 0。Gatling 3.0.0 里检查响应内容的常用写法是这样:

.exec( http("搜索图书") .get("/api/books?keyword=gatling") .check( status.is(200), jsonPath("$.code").is("0"), jsonPath("$.data.token").saveAs("token") ) )

status.is(200)只验证 HTTP 状态;jsonPath("$.code").is("0")验证业务码,把“假成功”过滤掉;jsonPath("$.data.token").saveAs("token")则把响应里的 token 存进当前虚拟用户的 Session,后面请求可以取用。

saveAs是把响应数据变成变量的关键动作,变量属于当前虚拟用户,不会跨用户串数据。检查器写得好,压测结果才有意义;如果只查状态码,业务链路大概率测了个寂寞。

3.2 用 CSV Feeder 让每个虚拟用户变“真人”

压测最怕每个用户请求同样的参数,容易被缓存干扰,也测不出真实业务波动。Gatling 3.0.0 的 Feeder 机制专门解决数据源问题。准备一份 CSV:

userId,keyword u1001,gatling u1002,performance u1003,jmeter

脚本里这样接:

val feeder = csv("data/users.csv").circular val scn = scenario("带数据的图书搜索") .feed(feeder) .exec( http("搜索图书") .get("/api/books") .queryParam("userId", "${userId}") .queryParam("keyword", "${keyword}") )

csv("data/users.csv")默认从 classpath 加载,也就是src/test/resources/data/users.csv;.feed(feeder)在虚拟用户启动时从数据源取一行写入 Session;后续请求里的${userId}和${keyword}是 EL 表达式,运行时替换成当前用户的数据。

.circular是循环取数,数据取完会从头再来;默认的queue策略在数据耗尽时会直接报错中断。如果 CSV 只有 100 行而并发 500,queue 跑到第 101 个用户就崩了,circular 则能跑完整轮。数据量大时不要用.random,内存占用会明显上升;普通业务压测用 circular 最稳。

3.3 登录拿 Token、后续请求带上:一套完整脚本

真实业务几乎没有单请求场景,基本都是登录、拿凭证、再查数据。下面的脚本把一家模拟书店系统的登录和查订单串起来,它代表最常见的业务链路压测写法:

import io.gatling.core.Predef._ import io.gatling.http.Predef._ import scala.concurrent.duration._ class OrderFlowSimulation extends Simulation { val httpProtocol = http .baseUrl("http://localhost:8080") .contentTypeHeader("application/json") .userAgentHeader("gatling-3.0.0-simulation") val userFeeder = csv("data/users.csv").circular val loginReq = http("登录") .post("/api/login") .body(StringBody("""{"username":"${username}","password":"${password}"}""")).asJson .check( status.is(200), jsonPath("$.token").saveAs("token") ) val orderReq = http("查询订单") .get("/api/orders") .header("Authorization", "Bearer ${token}") .check(status.is(200)) val scn = scenario("下单链路") .feed(userFeeder) .exec(loginReq) .pause(1.seconds, 3.seconds) .exec(orderReq) setUp( scn.inject( rampUsers(50).during(30.seconds) ) ).protocols(httpProtocol) }

contentTypeHeader在整个协议层统一设置 JSON,后续 POST 不用重复写头;body(StringBody(...)).asJson里的${username}来自 Feeder,登录响应里的 token 通过saveAs("token")存进 Session;orderReq的Authorization头用${token}取出来。

pause(1.seconds, 3.seconds)是模拟用户思考时间的标准写法,1 到 3 秒随机。很多压测结果过于“好看”,就是没加 pause,脚本把被测系统压成了纯粹的吞吐测试,而不是用户行为模拟。真实业务链路里,这个随机思考时间必须留在场景中。

4. 压力参数与注入模型:open、closed 和阶梯加压怎么选

4.1 按到达率注入:open 模型的四类函数

open 模型的意思是每秒有多少新用户进入系统由脚本控制,系统并发数是被测服务的背压决定的。Gatling 3.0.0 最常用的 open 注入函数有四个:

注入函数行为常见用途
atOnceUsers(n)n 个用户同时启动冒烟验证、峰值瞬间冲击
rampUsers(n).during(d)d 时长内线性爬到 n 个用户常规容量测试
constantUsersPerSec(r).during(d)每秒稳定进入 r 个用户验证固定 QPS 场景
rampUsersPerSec(r1).to(r2).during(d)每秒到达率从 r1 线性涨到 r2阶梯式摸底测试

多阶段可以组合在同一个inject里:

scn.inject( atOnceUsers(2), rampUsers(50).during(30.seconds), constantUsersPerSec(20).during(60.seconds) )

阶段按顺序执行:先 2 个用户验证,再 30 秒爬到 50 个用户,最后以每秒 20 个新用户的速率持续施压 60 秒。这里要注意,constantUsersPerSec(20)的“20”是每秒到达率,不是在线人数;如果每个用户平均处理 2 秒,在线并发大约是 40。

4.2 按并发数注入:closed 模型的适用场景

closed 模型和 open 模型正好相反,它保证系统里的并发用户数恒定,脚本不关心每秒进来多少新用户。Gatling 3.0.0 补上这套模型,是这版最实用的更新之一:

scn.inject( constantConcurrentUsers(30).during(60.seconds), rampConcurrentUsers(30).to(80).during(60.seconds) )

constantConcurrentUsers(30)让系统始终有 30 个活跃用户,前一个用户跑完,后一个立刻补位;rampConcurrentUsers(30).to(80)在 60 秒内把并发数从 30 抬到 80。

做容量评估时我通常先用 open 模型摸底,找到系统拐点,再用 closed 模型验证“恒定在线人数”下的稳定性。比如内部管理系统在线人数基本固定 500,用constantConcurrentUsers(500)比用“每秒到达率”更贴近真实。注意 closed 模型下虚拟用户跑得很快,如果没有pause,吞吐量会被脚本自身速度抬高,得出虚高结论。

4.3 参数搭配:预热、峰值、总时长与连接配置

一个较完整的压力阶段设计可以这样写:

setUp( scn.inject( rampUsers(20).during(20.seconds), constantUsersPerSec(20).during(60.seconds), stressPeak(50).during(30.seconds) ) ).protocols(httpProtocol) .maxDuration(3.minutes)

stressPeak的语义是在指定时间内模拟一次压力峰值,适合验证系统面对突增流量时的表现;.maxDuration是总时长上限,防止脚本因异常卡住无限跑下去。三个阶段合在一起的思路是:先预热、再稳定施压、最后打一次峰值。我不建议第一个版本就把并发拉满,先在 10 到 20 并发下跑通链路,确认没有数据脏写、登录态过期、死锁这类脚本问题,再按阶段爬升,否则分不清是脚本问题还是系统瓶颈。

连接配置里最容易影响结果的是shareConnections:

http .baseUrl("http://localhost:8080") .shareConnections .disableCaching .userAgentHeader("Gatling/3.0.0")

shareConnections让所有虚拟用户共享底层连接,能大幅降低压测机端口占用和连接建立开销;但代价是连接数不再等于真实用户数。如果压测目标是应用层接口性能,可以开;如果就是想验证网关的连接上限,千万别开。disableCaching关闭 HTTP 缓存,让每次请求都真正打到后端,避免压测机本地缓存拉高指标。

5. Gatling 3.0.0 避坑:Session 丢失、端口耗尽与结果失真

5.1 Session 键丢失导致 NoSuchElementException

现象:脚本跑到某个请求时直接失败,控制台报java.util.NoSuchElementException: key not found: token,或者从日志看Token相关请求全部 401。

原因:上一个请求的saveAs("token")没有执行,或者字段名拼写不一致。也可能是分支逻辑导致部分虚拟用户根本没走登录请求,后者在exec带条件判断时最容易被忽略。

解决:在依赖 Session 变量的请求前加防御性检查:

.exec(loginReq) .exec { session => if (session.contains("token")) session else session.markAsFailed } .exec(orderReq)

session.contains判断键是否存在,不存在就调用markAsFailed把当前用户标记为失败,这样报告里能清楚看到是因为变量缺失失败,而不是请求本身超时。排查时先核对saveAs的键名和${...}引用名,逐字一致是基本功。

5.2 压测机先倒下:端口耗尽与句柄不够

现象:压测机在高并发下报java.net.SocketException: Too many open files,或者BindException: Address already in use,响应时间在压测机上快速恶化,而被测机资源还远没打满。

原因:压测机端口和文件句柄被压测流量耗尽。这是典型的资源问题,属于血泪经验:压力还没到被测系统,压测机自己先成了瓶颈。

解决:提高系统限制并压低连接配额。

ulimit -n 65535 sudo sysctl -w net.ipv4.ip_local_port_range="1024 65535"

配合脚本层面的shareConnections,能让虚拟用户复用连接而不是每请求新建一条连接。如果压测机是多核,还要注意 Gatling 默认按 CPU 核数分配执行线程,虚拟机核数过少会低估最大并发能力。

5.3 指标全绿但上线翻车:压测拓扑有问题

现象:压测报告错误率 0、响应时间非常漂亮,可功能一上线并发稍高就故障,结果和压测完全对不上。

原因:最常见的是压测机和被测机部署在同一台主机,资源互相竞争,得出了一个“假低延迟”;或者压测只打到内网网关没打到真实存储与业务集群,压了个寂寞。

解决:检查三点:一是压测机与被测服务物理隔离;二是被测机 CPU、内存、磁盘曲线在你压测期间必须同步抬升,否则流量没真正打到它;三是在被测服务访问日志里按 User-Agent 过滤,确认 Gatling 流量确实到达。指标全绿不一定是好消息,先确认流量真的走到链路末端,再信报告。

5.4 长压测报告生成时 OOM

现象:压测跑完了,控制台没有输出报告路径,直接报OutOfMemoryError: Java heap space,或者报告目录只有零碎文件。

原因:Gatling 3.0.0 会在内存里记录大量请求明细,长时间高并发压测时堆内存不够用。这个版本对内存的需求比 2.x 明显更高,长跑场景尤甚。

解决:在启动前增大堆内存:

export JAVA_OPTS="-Xmx4g" mvn gatling:test

如果 4G 还不够,先检查是不是压测时长拉得过长、请求采样太密;很多场景可以通过缩短单阶段时长、拆成多次仿真来规避。千万别让长压测在内存崩溃后白跑,跑之前先确认压测机空闲内存够不够。

5.5 脚本能跑但请求全部 4xx/415

现象:用浏览器或命令行工具能正常请求,Gatling 一跑全是 400、403、415,错误率接近 100%。

原因:缺少必要的请求头。浏览器自动带了Content-Type、Accept、Cookie 等头,Gatling 脚本里没配就裸奔,服务端大概率拒绝或拦截。

解决:在协议层补齐真实请求头:

http .baseUrl("http://localhost:8080") .contentTypeHeader("application/json") .acceptHeader("application/json") .userAgentHeader("Mozilla/5.0 (compatible; Gatling)")

先从浏览器网络面板里把关键请求头复制到协议配置,跑一次确认基础通了,再做业务校验。很多 403 也和 User-Agent 被服务端反爬策略识别有关,Gatling 3.0.0 默认 UA 不一定被服务端放行,显式设置一个明确的浏览器 UA 是低成本的解决办法。

6. 把回归压测接进 CI:用参数化和断言让流水线自己判断

6.1 用系统属性把并发数变成可配置参数

脚本里写死并发数会让 CI 场景很尴尬:本地用小并发验证,流水线里跑大并发,总不能维护两份脚本。Gatling 仿真类本身就是 JVM 进程,直接读System.getProperty就是标准传参方式:

val totalUsers = System.getProperty("users", "50").toInt val rampSeconds = System.getProperty("rampSeconds", "30").toInt setUp( scn.inject( rampUsers(totalUsers).during(rampSeconds.seconds) ) ).protocols(httpProtocol)

流水线里就能这样传参:

mvn gatling:test \ -Dgatling.simulationClass=com.example.gatling.OrderFlowSimulation \ -Dusers=200 \ -DrampSeconds=60

本地开发把users调成 5 验证脚本通不通,CI 里传真实压测规模做回归。参数全部走命令行,脚本本身不用改。

6.2 用断言决定 CI 的绿与红

压测跑完不检查结果等于没跑。Gatling 3.0.0 的断言失败会让mvn gatling:test进程返回非零退出码,常见 CI 平台会自动把该次任务标红。回到第 3 章那个订单链路,可以这样挂断言:

setUp(scn.inject(...)) .protocols(httpProtocol) .assertions( global.responseTime.max.lt(1000), global.successfulRequests.percent.gt(99.5), details("查询订单").responseTime.percentile3.lt(800) )

global.responseTime.max.lt(1000)是所有请求最大响应时间不超过 1 秒;global.successfulRequests.percent.gt(99.5)是成功率不低于 99.5%;details("查询订单")单独盯住链路里的关键请求,percentile3是 95 分位响应时间。把断言阈值和容量基线对齐,流水线就自动成了性能回归的守门员。

我最初把阈值设得很激进,第一个晚上就被最大响应时间打红;后来改成用 95 分位和成功率组合判断,而不是死盯单次峰值,流水线才真正稳定下来。性能压测不怕报警,怕的是没有基线、跑完没人看。把脚本纳入 CI 后,剩下的事就是随时盯着报告面板,有问题第一时间看到。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询