API性能测试:JMeter脚本化与Gatling代码化双方案
前面咱们搞定了功能测试,但接口能跑通不代表能扛住流量。今天聊性能测试——JMeter和Gatling两个主流工具,什么时候用哪个?怎么设计压测场景?
一、性能测试不是"把并发调大就行"
很多新手做性能测试,上来就设置"1000并发跑10分钟",然后看TPS高不高。这其实是个误区。
性能测试的三种类型
| 类型 | 目的 | 怎么测 |
|---|---|---|
| 负载测试 | 系统在正常负载下表现如何 | 逐步加压到预期峰值,观察指标 |
| 压力测试 | 系统的极限在哪 | 持续加压直到系统崩溃,找瓶颈 |
| 稳定性测试 | 长时间运行会不会出问题 | 保持中等负载跑几小时/几天,看内存泄漏 |
关键指标解读
┌─────────────────────────────────────────────────────────┐ │ 用户请求 ──> 响应时间(Latency) <── 服务端处理 │ │ ├── 网络传输时间 │ │ ├── 服务端处理时间 │ │ └── 数据库/缓存查询时间 │ │ │ │ TPS(每秒事务数)= 完成的请求数 / 时间 │ │ 并发数 = 同时在线的用户/连接数 │ │ 吞吐量 = 单位时间处理的数据量(MB/s) │ │ │ │ 错误率 = 失败请求 / 总请求 │ │ P50/P95/P99 = 50%/95%/99%的请求响应时间 │ └─────────────────────────────────────────────────────────┘重点关注P99而不是平均响应时间。平均数容易被少数快请求拉低,P99告诉你"最慢的那1%用户"体验如何。
二、JMeter:老牌但够用的GUI工具
快速上手
下载解压后,直接运行bin/jmeter.bat(Windows)或bin/jmeter.sh(Mac/Linux)。
一个简单的压测脚本
Step 1:添加线程组(模拟用户)
右键测试计划 → 添加 → 线程(用户)→ 线程组 线程数:100 ← 并发用户数 Ramp-Up:10秒 ← 10秒内启动100个用户 循环次数:10 ← 每个用户执行10次Step 2:添加HTTP请求
右键线程组 → 添加 → 取样器 → HTTP请求 协议:https 服务器名称:api.example.com 端口:443 方法:GET 路径:/api/productsStep 3:添加监听器(看结果)
右键线程组 → 添加 → 监听器 → 聚合报告 右键线程组 → 添加 → 监听器 → 查看结果树Step 4:运行
点绿色三角按钮,等跑完看聚合报告:
Label #Samples Average Median 90%Line 95%Line 99%Line Min Max Error% Throughput HTTP请求 1000 156 120 280 350 500 50 800 0.00% 450.2/secJMeter的进阶配置
参数化:不同用户不同数据
右键线程组 → 添加 → 配置元件 → CSV数据文件设置 文件名:users.csv 变量名:username,passwordusers.csv内容:
user001,pass001 user002,pass002 user003,pass003HTTP请求中引用:
路径:/api/login 参数: username: ${username} password: ${password}断言:验证响应
右键HTTP请求 → 添加 → 断言 → 响应断言 测试字段:响应文本 模式匹配规则:包含 测试模式:"success":true思考时间:模拟真实用户停顿
右键线程组 → 添加 → 定时器 → 高斯随机定时器 偏差:1000ms ← 平均停顿1秒 固定延迟偏移:500msJMeter的痛点
| 痛点 | 说明 |
|---|---|
| GUI模式资源消耗大 | 压测时别用GUI,用命令行模式 |
| 脚本难版本控制 | .jmx是XML,diff看不懂 |
| 分布式配置麻烦 | 主从节点要配RMI,防火墙端口要开 |
| 复杂逻辑难实现 | 想做个条件分支?写BeanShell吧,痛苦 |
命令行模式(生产环境必须用):
# 非GUI模式运行jmeter-n-tapi-test.jmx-lresult.jtl-e-oreport/# -n: 非GUI模式# -t: 测试脚本# -l: 结果日志# -e: 生成报告# -o: 报告输出目录三、Gatling:代码化压测的新选择
Gatling用Scala DSL写压测脚本,虽然要学点Scala语法,但回报巨大。
引入依赖
<dependency><groupId>io.gatling.highcharts</groupId><artifactId>gatling-charts-highcharts</artifactId><version>3.9.5</version><scope>test</scope></dependency><plugin><groupId>io.gatling</groupId><artifactId>gatling-maven-plugin</artifactId><version>4.6.0</version></plugin>第一个Gatling脚本
// src/test/scala/com/example/ApiSimulation.scalapackagecom.exampleimportio.gatling.core.Predef._importio.gatling.http.Predef._importscala.concurrent.duration._classApiSimulationextendsSimulation{// HTTP协议配置valhttpProtocol=http.baseUrl("https://api.example.com").acceptHeader("application/json").contentTypeHeader("application/json")// 场景定义valscn=scenario("查询商品场景").exec(http("查询商品列表").get("/api/products").queryParam("page","1").queryParam("size","20").check(status.is(200)).check(jsonPath("$.total").exists)).pause(1,3)// 思考时间:1-3秒随机停顿.exec(http("查询商品详情").get("/api/products/${productId}")// 从session中提取变量.check(status.is(200)))// 注入负载配置setUp(scn.inject(rampUsers(100).during(10.seconds),// 10秒内启动100用户constantUsersPerSec(50).during(60.seconds)// 然后保持50/s的速率跑60秒)).protocols(httpProtocol)}运行:
mvn gatling:test-Dgatling.simulationClass=com.example.ApiSimulationGatling的DSL有多爽
复杂场景编排
valbrowse=scenario("浏览购买流程")// 1. 登录.exec(http("登录").post("/api/auth/login").body(StringBody("""{"username":"${username}","password":"${password}"}""")).check(jsonPath("$.token").saveAs("authToken"))// 提取token存到session).pause(2)// 2. 浏览商品(从CSV feeder读取商品ID).feed(csv("products.csv").random).exec(http("查看商品").get("/api/products/${productId}").header("Authorization","Bearer ${authToken}").check(jsonPath("$.stock").saveAs("stock"))).pause(1,5)// 3. 条件判断:有库存才下单.doIf(session=>session("stock").as[Int]>0){exec(http("创建订单").post("/api/orders").header("Authorization","Bearer ${authToken}").body(StringBody("""{"productId":"${productId}","quantity":1}""")).check(status.is(201)))}多种负载模型
setUp(// 模型1:逐步加压(找拐点)scn.inject(nothingFor(5.seconds),atOnceUsers(10),rampUsers(100).during(30.seconds),rampUsers(500).during(60.seconds),rampUsers(1000).during(120.seconds)),// 模型2:脉冲负载(模拟秒杀)spikeScenario.inject(rampUsers(10000).during(10.seconds),nothingFor(30.seconds),rampUsers(10000).during(10.seconds)),// 模型3:稳定性测试stabilityScenario.inject(constantUsersPerSec(100).during(2.hours))).protocols(httpProtocol)丰富的断言
// 全局断言setUp(scn.inject(...)).protocols(httpProtocol).assertions(global.responseTime.max.lt(1000),// 最大响应时间 < 1sglobal.successfulRequests.percent.gt(99.0),// 成功率 > 99%global.requestsPerSec.gt(100),// TPS > 100details("创建订单").responseTime.percentile(95).lt(500)// 下单P95 < 500ms)Gatling的报告
运行完后自动生成HTML报告,长这样:
target/gatling/apisimulation-20240101120000/ ├── index.html # 总览 ├── js/ # 图表JS └── req_*.html # 每个请求的详细统计报告包含:
- 响应时间分布图(直方图 + 百分位曲线)
- 每秒请求数/响应数趋势
- 活跃用户数变化
- 错误统计和堆栈
四、JMeter vs Gatling:怎么选?
| 维度 | JMeter | Gatling |
|---|---|---|
| 学习曲线 | 低(GUI点点点) | 中(要学Scala DSL) |
| 脚本维护 | 难(XML难diff) | 易(代码可版本控制) |
| 复杂场景 | 难(BeanShell痛苦) | 易(代码灵活) |
| 资源消耗 | 较高 | 较低(Netty异步IO) |
| 报告美观 | 一般 | 优秀(自带炫酷图表) |
| 团队协作 | 测试人员为主 | 开发人员顺手 |
| 生态插件 | 丰富(社区插件多) | 较少但够用 |
我的建议:
- 快速验证/测试团队用→ JMeter
- 持续集成/开发团队用→ Gatling
- 复杂业务场景→ Gatling(代码比GUI灵活太多)
五、性能测试的"坑"与最佳实践
坑1:在本地跑压测
你的笔记本能模拟1000并发?别闹了。网络带宽、CPU、内存都是瓶颈。
正确做法:用专门的压测机器,或者云厂商的压测服务(阿里云PTS、AWS Load Testing等)。
坑2:不预热直接压
JVM要预热(JIT编译),连接池要初始化,缓存要填充。一上来就猛压,结果不准。
正确做法:
// Gatling中加个预热阶段scn.inject(rampUsers(100).during(60.seconds),// 先慢慢预热1分钟constantUsersPerSec(1000).during(300.seconds)// 再正式压测5分钟)坑3:只压一个接口
真实场景是多个接口混合调用,比例不同。
正确做法:
// 模拟真实用户行为比例setUp(// 80%用户只浏览browseOnly.inject(rampUsers(800).during(60.seconds)),// 15%用户浏览+加购物车browseAndCart.inject(rampUsers(150).during(60.seconds)),// 5%用户完整购买流程fullPurchase.inject(rampUsers(50).during(60.seconds))).protocols(httpProtocol)坑4:不看服务端指标
客户端TPS高,但服务端CPU 100%、内存OOM、GC频繁,这不算"通过"。
正确做法:压测时同时监控:
- 服务端CPU/内存/磁盘IO
- 数据库连接池使用率、慢查询
- Redis命中率、连接数
- JVM GC频率和耗时
- 网络带宽
坑5:没有基线和对比
“这次压测TPS是500”——然后呢?上次是多少?优化后提升了多少?
正确做法:每次压测结果存档,建立性能基线,优化前后对比。
六、Spring Boot + Gatling 集成示例
// 在Spring Boot测试里启动Gatling压测@SpringBootTest(webEnvironment=SpringBootTest.WebEnvironment.RANDOM_PORT)classPerformanceTest{@LocalServerPortprivateintport;@TestvoidrunGatlingSimulation()throwsException{// 设置系统属性,让Gatling连上随机端口System.setProperty("gatling.http.baseUrl","http://localhost:"+port);// 运行GatlingGatlingPropertiesBuilderprops=newGatlingPropertiesBuilder().simulationClass("com.example.ApiSimulation").resultsDirectory("target/gatling-results");Gatling.fromMap(props.build(),List.of());}}七、小结
今天咱们聊了性能测试的方方面面:
| 主题 | 要点 |
|---|---|
| 性能测试类型 | 负载测试、压力测试、稳定性测试,目的不同 |
| 关键指标 | 关注P99、错误率、吞吐量,别只看平均数 |
| JMeter | GUI易上手,适合快速验证和测试团队 |
| Gatling | 代码化DSL,适合复杂场景和CI集成 |
| 常见坑 | 本地压测、不预热、单接口压测、不看服务端指标、无基线 |
一句话总结:JMeter是"瑞士军刀",Gatling是"精密仪器"。日常快速验证用JMeter,持续性能测试和复杂场景用Gatling。
你们性能测试用JMeter还是Gatling?遇到过什么坑?欢迎聊聊。