简介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 groupIdio.gatling.highcharts/groupId artifactIdgatling-charts-highcharts/artifactId version3.0.0/version scopetest/scope /dependency /dependencies build plugins plugin groupIdio.gatling/groupId artifactIdgatling-maven-plugin/artifactId version3.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.confgatling.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?keywordgatling) .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.simulationClasscom.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?keywordgatling) .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 机制专门解决数据源问题。准备一份 CSVuserId,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 行而并发 500queue 跑到第 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)存进 SessionorderReq的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 并发下跑通链路确认没有数据脏写、登录态过期、死锁这类脚本问题再按阶段爬升否则分不清是脚本问题还是系统瓶颈。连接配置里最容易影响结果的是shareConnectionshttp .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_range1024 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.simulationClasscom.example.gatling.OrderFlowSimulation \ -Dusers200 \ -DrampSeconds60本地开发把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 后剩下的事就是随时盯着报告面板有问题第一时间看到。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?