简介:Gatling 3.0.0 是一款面向现代 Web 应用的性能测试工具,适合开发者与测试工程师用来模拟高并发用户负载,验证系统在压力下的响应时间、吞吐量与资源利用率。资源包共 114 个文件,以 97 个 jar 依赖库为主体,辅以 6 个 Scala 脚本、3 个 conf 配置文件及 bat、sh 启动脚本,压缩包约 57.17MB,解压后可直接运行录制器与压测入口。其测试脚本基于 Scala DSL 编写,可读性与可维护性较强,配合 Highcharts 图表报告清晰呈现每秒请求数、平均响应时间与百分位分布,便于定位性能瓶颈。已有 237 人学习下载,适合希望快速搭建压测环境、理解 Gatling 脚本结构与报告解读的读者参考实践。
1. Gatling 3.0.0 拆包:一份能直接跑起来的压测工具包
压测这件事,最怕的不是压不出问题,而是压出来的数字自己都不信。前阵子帮一个团队排查接口超时,他们用某款图形化工具跑出来的 TPS 曲线漂亮得像教科书,结果一上生产就崩,回头一查,压测脚本里连思考时间都没设,请求全砸在一个静态参数上。后来我换成 Gatling 3.0.0 重跑,同样的接口,报告里响应时间分布、并发用户曲线、错误率一目了然,问题当场现形。这份 gatling3.0.0 资源包,就是围绕这个版本整理的一套可落地压测环境,包含核心运行库、依赖和基础脚本模板,适合需要做接口级性能测试、又不想被图形界面遮住细节的从业者。它解决的不是“能不能压”,而是“压得准不准、看得清不清”。下面按我实际拆包和跑通的顺序,把选型理由、脚本写法、参数调法和踩过的坑一次讲透。
2. Gatling 3.0.0 的选型逻辑与运行环境搭建
2.1 为什么是 Gatling 3.0.0,而不是图形化压测工具
性能测试工具大致分两类:一类是图形界面驱动、录制回放为主的,上手快但脚本可编程性弱;另一类是以代码描述压测场景的,Gatling 属于后者。3.0.0 这个版本在 DSL 表达力和报告结构上已经比较成熟,脚本用 Scala 写,压测场景本身就是一段可版本管理的代码,改参数、加断言、做参数化都落在文件里,而不是散落在某个界面的输入框里。这一点对团队协作很关键——脚本能进代码仓库,能 code review,能跟着接口一起演进。
从执行模型看,Gatling 基于 Akka 的异步消息机制,单机就能撑起较高并发,不像线程模型那样一个用户一个线程,几千并发就把机器拖垮。它的报告是静态 HTML,跑完直接打开看,响应时间分位数、每秒请求数、活跃用户数、错误分布都在一张页面上,不需要额外接监控。对于接口级压测,这套组合足够用,而且轻。
选 3.0.0 而不是更新的版本,实际考虑是资源包里的依赖和脚本模板都是按这个版本对齐的,Scala 版本、Akka 版本、插件配置互相咬合,贸然换版本容易出现 DSL 语法不兼容或者报告插件加载失败。压测工具这种东西,版本稳定比版本新更重要,跑出来的基线数据要能横向对比,中途换版本等于把历史数据作废。
2.2 环境准备:JDK、目录结构与启动方式
Gatling 3.0.0 运行依赖 JDK 8,这是硬门槛,JDK 11 及以上在这个版本上会出现反射相关的兼容问题。先确认本机 Java 版本:
java -version # 期望输出类似:java version "1.8.0_xxx" # 如果显示 11 或 17,需要切换 JAVA_HOME 到 JDK 8解包后的目录结构大致是这样,认识这几个位置能省很多找文件的时间:
| 目录/文件 | 作用 |
|---|---|
| bin/gatling.sh | Linux/macOS 启动脚本 |
| bin/gatling.bat | Windows 启动脚本 |
| user-files/simulations | 压测脚本存放位置,Scala 文件放这里 |
| user-files/resources | 参数化数据文件,如 CSV、feed 数据 |
| user-files/bodies | 请求体模板文件 |
| conf/gatling.conf | 全局配置,编码、报告输出等 |
| lib | 核心 jar 与依赖 |
| results | 每次运行的报告输出目录 |
启动方式很直接,进入 bin 目录执行脚本,它会扫描 simulations 目录下的脚本并列出可选编号:
cd gatling/bin ./gatling.sh # 交互式列出可用 simulation,输入编号回车即可运行 # 也可以直接指定:./gatling.sh -s 包名.类名这里有个容易忽略的点:脚本的包名和文件路径要对应。放在 user-files/simulations 下的 Scala 文件,第一行的 package 声明要和相对路径一致,否则启动时扫描不到,列表里是空的。我一般把脚本统一放在simulations/根下,package 写simulations,省得路径和包名对不上。
2.3 全局配置里值得先改的三个参数
conf/gatling.conf 是全局行为的总开关,刚拿到包不用全看,先改这三处:
第一处是编码。默认配置里gatling.core.encoding如果是空或者非 UTF-8,中文参数化数据读进来会乱码,压测请求体里带中文的场景直接翻车。改成UTF-8。
第二处是报告输出目录。默认写到 results 下按时间戳建子目录,如果要做多轮对比,建议把gatling.core.directory.results指到一个固定父目录,方便脚本批量收集。
第三处是gatling.core.runDescription,这是报告标题,跑之前填上本次压测的目标,比如“订单查询接口基线”,报告打开第一眼就知道这轮在测什么,比事后翻记录靠谱。
gatling { core { encoding = "UTF-8" runDescription = "订单查询接口基线" directory { results = "results" } } }配置改完不需要重启什么服务,下次启动脚本自动读取。参数含义上,encoding 影响所有文件读写,runDescription 只影响报告展示,results 决定产物落点,三者互不干扰,可以单独调。
3. 用 Scala DSL 写第一个可复现的压测脚本
3.1 脚本骨架:HttpProtocol、Scenario 与 Injection 三段式
Gatling 脚本的结构是固定的三段式:先定义 HTTP 协议配置(baseUrl、公共头),再定义场景(一串请求和停顿),最后用注入模型把场景挂到协议上跑。这个结构不是形式主义,它把“压什么”“怎么压”“压多少”拆成三层,改其中一层不影响另外两层。
一个最小可跑的脚本长这样,我拿一个模拟的订单查询接口举例:
package simulations import io.gatling.core.Predef._ import io.gatling.http.Predef._ import scala.concurrent.duration._ class OrderQuerySimulation extends Simulation { // 第一段:协议配置,baseUrl 和公共请求头 val httpProtocol = http .baseUrl("http://127.0.0.1:8080") .acceptHeader("application/json") .contentTypeHeader("application/json") // 第二段:场景定义,一个请求加一次思考时间 val scn = scenario("订单查询场景") .exec( http("query_order") .get("/api/order/query?id=1001") .check(status.is(200)) ) .pause(1.second, 3.seconds) // 第三段:注入模型,10 个用户 10 秒内均匀启动 setUp( scn.inject(rampUsers(10) over (10.seconds)) ).protocols(httpProtocol) }逻辑上,httpProtocol里的 baseUrl 是后续所有相对路径的拼接前缀,公共头在这里设一次,场景里就不用重复写。scenario里的exec是动作单元,check是断言,状态码不是 200 就记为失败,报告里会单独统计。pause是思考时间,模拟真实用户两次操作之间的间隔,不设的话请求会背靠背打出去,压出来的数字虚高。setUp里的rampUsers(10) over (10.seconds)表示 10 个虚拟用户在 10 秒内线性启动,避免瞬间冲击造成启动阶段的假尖峰。
参数上,rampUsers的两个值要一起看:用户数和爬坡时长决定启动速率。10 用户 10 秒是每秒 1 个,如果改成 100 用户 10 秒就是每秒 10 个,后者对被测服务的瞬时压力大得多。第一次跑建议用小值摸基线,确认脚本逻辑没问题再放大。
3.2 参数化:用 feeder 把静态请求变成真实流量
上面那个脚本所有请求都打同一个 id,压测意义有限,因为服务端可能对同一参数做了缓存,压出来的是缓存命中率而不是真实处理能力。参数化是让压测可信的第一步,Gatling 用 feeder 实现。
准备一个 CSV 放在 user-files/resources 下,比如 order_ids.csv:
orderId 1001 1002 1003 1004 1005脚本里用csvfeeder 读进来,在 exec 里用${orderId}引用:
val orderFeeder = csv("order_ids.csv").circular val scn = scenario("订单查询场景-参数化") .feed(orderFeeder) .exec( http("query_order") .get("/api/order/query?id=${orderId}") .check(status.is(200)) ) .pause(1.second, 3.seconds)circular是循环策略,数据读完从头再来;另外还有random(随机取)和queue(读完即止,数据耗尽后用户退出)。选哪个看场景:要持续稳定压力用 circular,要模拟真实用户不重复请求用 random,要做一次性批量校验用 queue。这里有个血泪经验,CSV 第一行是表头,feeder 默认会跳过,但如果你的文件没有表头,要在csv()里显式传Seq("orderId")指定列名,否则第一行数据会被当成表头吃掉,压测少一条数据还查不出原因。
3.3 断言与报告:让压测结果自己说话
脚本跑完不等于压测结束,得让工具替你判断“这轮算不算通过”。Gatling 的 assertions 挂在 setUp 后面,常见的是对响应时间和成功率设阈值:
setUp( scn.inject(rampUsers(50) over (30.seconds)) ).protocols(httpProtocol) .assertions( global.responseTime.percentile3.lt(500), // 95% 请求响应时间小于 500ms global.successfulRequests.percent.gt(99) // 成功率高于 99% )percentile3对应 95 分位,percentile2是 75 分位,percentile4是 99 分位,这个编号容易记混,我一般直接查报告里的分位表对照。断言不通过时,启动脚本的退出码非零,可以接进流水线做门禁。报告本身是 results 下的 index.html,打开后重点看三块:响应时间分布图看长尾,活跃用户曲线看注入是否符合预期,错误表看失败集中在哪个请求。长尾比均值更能暴露问题,均值 200ms 但 99 分位 3 秒的接口,用户体验是灾难性的。
4. 注入模型与并发参数的调法:从 10 用户到千级并发
4.1 四种注入策略的适用边界
Gatling 3.0.0 的注入模型不止 rampUsers 一种,选错模型会让压测曲线失真。常见四种:
| 注入方式 | 写法 | 适用场景 |
|---|---|---|
| 线性爬坡 | rampUsers(n) over (t) | 摸容量上限,看随并发增长的性能拐点 |
| 恒定并发 | constantUsersPerSec(n) during (t) | 稳态压测,验证长时间运行的稳定性 |
| 阶梯加压 | stressPeakUsers(n) over (t) | 找崩溃点,逐级加码直到错误率飙升 |
| 瞬时冲击 | atOnceUsers(n) | 秒杀类场景,验证突发流量下的表现 |
稳态压测我一般用constantUsersPerSec,它保证每秒新增用户数恒定,配合场景里的 pause,整体并发会稳定在一个水平。找拐点用rampUsers,从低到高线性加,报告里响应时间开始抬头的那个并发值就是容量参考。atOnceUsers慎用,它会在同一时刻拉起全部用户,对压测机本身也是冲击,机器扛不住时你分不清是被测服务慢还是压测机卡。
4.2 并发上不去时先查这三处
很多人第一次把用户数调到几百就发现 TPS 不涨反降,报告里错误率飙升,第一反应是被测服务扛不住,其实常见原因在压测端。按顺序查:
第一,压测机文件句柄数。Linux 默认单进程打开文件数上限 1024,每个 TCP 连接占一个句柄,几百并发就顶到上限,表现为连接超时或Too many open files。临时调高:
ulimit -n 65535 # 只对当前 shell 生效,压测脚本要在同一 shell 里启动第二,JVM 堆内存。Gatling 默认堆不大,高并发下 GC 频繁会拖慢请求发出速率。改 bin/gatling.sh 里的JAVA_OPTS,把-Xmx调到 2G 以上,具体看压测机内存。
第三,思考时间设得太短。pause 如果只有几十毫秒,虚拟用户几乎不停发请求,并发数虚高但每个用户的行为不像真人,压出来的数字没有参考价值。真实用户操作间隔通常在秒级,按业务实际填。
提示:压测机和被测服务不要部署在同一台机器上,否则 CPU 和网络互相争抢,两边数据都不可信。
4.3 分布式压测的取舍
单机压到一定量级后,压测机自身会成为瓶颈,这时要考虑多机分担。Gatling 3.0.0 本身不带开箱即用的分布式编排,常见做法是用多台机器各自跑脚本,通过统一的时间窗口和相同的注入配置,最后把多份报告的关键指标汇总对比。这种做法的代价是时间同步和结果合并要自己做,好处是不引入额外组件,环境干净。
如果并发需求在单机可承受范围内(一般几千用户以内,配合足够的堆和句柄),不建议上分布式,多机带来的时钟偏差和结果合并误差,有时比单机瓶颈更影响判断。我一般先用单机压到拐点,确认瓶颈在被测服务而不是压测机,再决定要不要扩。
5. 避坑清单:Gatling 3.0.0 实操中的五类翻车
5.1 脚本扫描不到,启动列表为空
现象:执行 gatling.sh 后,可选 simulation 列表是空的,或者只列出部分脚本。
原因:package 声明与文件相对路径不一致,或者文件扩展名不是 .scala,或者文件放在了 simulations 目录之外。Gatling 按目录扫描并校验包名,两者对不上就跳过。
解决:确认文件在 user-files/simulations 下,第一行 package 与相对路径一致(根目录下写package simulations),文件名与类名一致。改完重新启动,列表即刷新。
5.2 参数化数据读进来是乱码
现象:请求体或 URL 里带中文,服务端收到的是问号或乱码,断言失败。
原因:CSV 文件编码与 gatling.conf 里的 encoding 不一致,常见是文件存成了 GBK 而配置是 UTF-8,或者反过来。
解决:统一成 UTF-8。用编辑器确认 CSV 实际编码,再把 conf 里gatling.core.encoding设为UTF-8。改完清掉 results 下旧报告重跑,避免拿旧数据误判。
5.3 报告里响应时间正常,但错误率异常高
现象:响应时间分位数看着很漂亮,错误表里却有一大片 4xx 或 5xx。
原因:断言只写了 status.is(200),但接口在某些参数下返回业务错误码(HTTP 200 但 body 里 code 非 0),或者参数化数据里有无效 id 导致 404。前者是断言没覆盖业务层,后者是数据质量问题。
解决:对业务错误码加 jsonPath 断言,比如.check(jsonPath("$.code").is("0"));对参数化数据先做一轮有效性校验,把无效 id 剔掉。错误率高的报告不能只看响应时间就下结论。
5.4 高并发下压测机先崩
现象:用户数调到几百后,报告里大量连接超时,但被测服务监控显示负载并不高。
原因:压测机句柄数或堆内存到顶,请求发不出去,超时是压测端产生的,不是服务端。
解决:按 4.2 的顺序查 ulimit、JAVA_OPTS 堆大小,同时用top看压测机 CPU 是否打满。确认是压测端瓶颈后,要么加机器,要么降低单机并发、延长爬坡时间。
5.5 换版本后脚本编译不过
现象:把资源包里的脚本拿到另一个 Gatling 版本上跑,报 DSL 方法找不到或类型不匹配。
原因:Gatling 各版本间 DSL 有调整,3.0.0 的某些写法在后续版本被弃用或改名,Scala 版本差异也会导致编译失败。
解决:压测脚本和 Gatling 版本绑定管理,脚本目录里放一个版本说明文件,记录对应的 Gatling 和 JDK 版本。跨版本迁移时先在小流量脚本上验证 DSL 兼容性,再批量改。基线数据要标注版本,不同版本的数据不直接对比。
6. 把压测接进日常:报告对比与基线管理的一个习惯
压测跑通只是起点,真正有价值的是能持续对比。我现在的做法是每轮压测结束后,把 results 下生成的报告目录按“接口名_日期_并发数”重命名归档,同时从报告里抽三个数记到一张表里:95 分位响应时间、TPS 峰值、错误率。这三个数构成一条基线,下次改动后重跑,直接和基线比,涨了还是跌了一眼可见。
抽取这三个数不用手工翻报告,报告目录下的stats.json里有结构化数据,写个小脚本读出来汇总:
import json, glob, os rows = [] for path in glob.glob("results/*/js/stats.json"): with open(path, encoding="utf-8") as f: data = json.load(f) # 取全局统计段,字段名以实际报告为准 rows.append({ "run": os.path.basename(os.path.dirname(os.path.dirname(path))), "p95_ms": data.get("meanResponseTime", {}).get("percentiles", {}).get("95"), "errors": data.get("numberOfRequests", {}).get("ko"), }) for r in rows: print(r)这段脚本的逻辑是按目录遍历每份报告的 stats.json,抽出 95 分位和失败请求数。字段名在不同小版本间可能有差异,第一次用先打印整个 json 的键看一眼,别硬套。参数上,glob 的路径按你的 results 实际结构改,如果报告目录层级不同,调整dirname的层数即可。
基线管理有个容易忽略的细节:压测环境要固定。同一份脚本在开发机和测试环境跑出来的数没有可比性,机器配置、网络延迟、数据库数据量都会影响结果。我一般固定一台压测机、固定被测环境、固定数据准备脚本,三样不变,基线才有意义。数据准备这块,压测前把测试库重置到同一状态,避免上一轮写入的数据影响下一轮查询性能。
从那以后我每次开新一轮压测,都强制先跑一遍基线脚本确认环境和上一轮一致,再动被测服务。这个习惯帮我挡掉过好几次“性能退化”的误报——其实是压测机被人占了资源。希望这套拆包和调参的路子,能帮你把 Gatling 3.0.0 用出它该有的精度。
本文还有配套的精品资源,点击获取