跨境电商系统JMeter 和 Gatling 分工

一、为什么单工具压测在跨境场景跑不通

跨境电商的调用链比一般电商更长。一个订单从用户点下"提交"到最终落库,中间要穿过网关鉴权、商品服务、价格服务(要算完税价和实时汇率)、库存服务、优惠券、订单服务、支付网关、清关申报、物流下单,任何一环慢 300ms,P99 就会翻倍。

链路上的流量性质还不一样:

  • 商品详情和搜索是读多写少,能被 Redis 和 CDN 吃掉大部分,瓶颈通常在 Elasticsearch 的分片和深分页。
  • 下单和支付回调是写路径,压力直冲数据库行锁、库存扣减、事务提交。
  • 支付回调和物流推送由第三方驱动,带有重试语义,天然存在重复请求。

只用一种工具覆盖这三类流量,基本做不到。笔者的实践结论是:用 JMeter 做业务流编排 ,用 Gatling 做核心接口的极限施压和精细统计,两者跑同一套数据模型、同一套流量标记,结果才可比。

维度 JMeter Gatling
执行模型 一个用户一个线程(Thread per user) Akka Actor + 异步 IO,单机可撑数千虚拟用户
适合的活儿 多接口串联、参数化、条件逻辑、JDBC 取样、断言组合 单接口或短链路的高并发、长稳压测、脉冲注入
报告能力 需配 jpgc 插件或 Grafana Backend Listener 自带 HTML 报告,含 P50/P75/P95/P99.9 分位图、连接耗时拆解
资源开销 2000 虚拟用户常需 2~4 核、2G 以上堆 同量级虚拟用户堆内存压力明显低
上手成本 低,GUI 拖拽 需会 Scala DSL,但语法有限,半天够用

分工建议:JMeter 承载"像真人"的混合流量,线程数不必拉太高;Gatling 负责把下单、支付、库存扣减这几个接口压到拐点。两边的并发不要简单相加------JMeter 场景里已经包含下单行为,Gatling 的叠加量应按真实峰值里"纯下单请求"的占比折算。


二、先定目标,再谈工具

压测目标没定清,跑完只会得到一堆好看的曲线。跨境场景建议锁定四类目标:

  1. 容量水位:黑五峰值模型下,下单接口 TPS 上限、P99 响应时间、系统拐点出现在第几个并发档位。
  2. 保护策略是否真生效:限流阈值触发后是返回 429 还是静默丢弃、熔断后降级页是否可用、库存分布式锁在 5000 并发下会不会出现超卖。
  3. 长稳表现:8 小时持续压测下堆内存是否单调上升、连接池是否耗尽、MQ 是否积压。
  4. 可扩容性:应用节点从 4 台扩到 8 台,TPS 是接近翻倍还是卡在数据库这一层------这条直接决定大促前扩容该加在哪。

指标阈值要在压测前书面约定,例如:核心接口 P99 ≤ 800ms、错误率 ≤ 0.1%、下单 TPS ≥ 目标峰值的 1.2 倍。事后改阈值等于没测。


三、环境与数据隔离:这部分做不好,压测会变成事故

这是全文最容易出事的地方。跨境业务涉及订单、支付流水、清关申报数据,一旦压测脏数据进了生产库,清理成本远高于压测本身的价值。

环境镜像:压测环境要与生产同构------同样的节点数、同样的中间件版本、同样的网关规则和超时配置。降配环境测出来的 TPS 没有参考价值,只能用来做相对比较。

四层隔离,缺一层都不算安全:

层级 做法 注意点
账号隔离 批量生成前缀统一的测试会员(如 pt_ 开头),与真实账号段分开 测试账号不绑定真实手机号,避免触发短信通道
数据隔离 订单、支付流水、库存流水走影子表(同结构、不同表名或不同库) 影子表要同步真实表的索引,索引不同性能差异巨大
流量标记 请求头写 X-Pressure-Test: true,网关注入 traceId 前缀 服务层收到标记后关闭真实短信、邮件、推送,并写入影子库分支
第三方沙箱 支付、物流、海关全部指向沙箱地址 沙箱的响应时间通常比生产快,测出的 P99 会偏乐观,需在报告里注明

流量标记的落地方式是关键。推荐在网关做一次统一拦截:读到标记后,往 ThreadLocal 写上下文,MyBatis 拦截器根据上下文改写目标表名,MQ 生产者改写 topic 后缀为 _shadow。这样业务代码零改动,压测和生产共用一套镜像。

事后清理写成脚本:按标记前缀清影子表、重置测试库存、回收测试账号,压测报告的附录里附上清理执行的记录。


四、压测流程:七个阶段,每阶段有明确的退出条件

阶段一 准备。产出三样东西:全链路接口清单(含调用顺序、超时阈值、依赖关系)、流量模型、数据准备脚本。流量模型不要只写"QPS 5000",要写成用户行为比例------比如 100 个虚拟用户里 60 个在浏览详情、15 个在搜索、20 个在加购、5 个走到下单,这才是能直接翻译成 JMeter 吞吐量控制器的东西。

阶段二 单接口基线。逐个接口压到单点极限,记录最大 TPS 与拐点。这一步的意义是提前暴露单点缺陷------如果搜索接口单独压就已经 500ms,串进链路后整条链路的 P99 必然崩,没必要浪费一轮全链路资源。基线不达标的接口先优化,再进链路。

阶段三 链路串联。用 JMeter 按真实比例混合流量,做梯度加压(每档稳定运行 10 分钟)和 4~8 小时长稳。长稳主要看三件事:Full GC 频率是否随运行时间上升、连接池活跃数是否逼近上限、MQ 堆积是否持续增长。

阶段四 极限与脉冲。Gatling 负责这一阶段。脉冲压测要模仿真实秒杀:不是平滑爬坡,而是 10 秒内从 200 并发跳到 5000,维持 30 秒再回落。这样才能验证限流器的令牌桶容量和预热逻辑是否合理。

阶段五 故障注入 。用 tc 注入 200ms 网络延迟、随机丢包 1%,用 kill -9 干掉一个应用节点,主库执行主从切换。观察熔断是否在约定时间内打开、重试是否引发雪崩(重点看重试风暴:当下游超时,上游重试 3 次等于把流量放大 3 倍,必须有重试预算和抖动退避)。

阶段六 分析。见第六节。

阶段七 回归。相同脚本、相同模型复测,指标对比表要放在报告第一页。


五、脚本关键实现

JMeter 侧:把业务流写对

结构自上而下:Test Plan → Thread Group(Stepping) → HTTP Request Defaults → Header Manager → Cookie Manager → CSV Data Set → Transaction Controller(整条链路) → 各接口 Sampler → 断言 → Backend Listener。

几个容易踩的点:

  • CSV 数据量必须大于虚拟用户数 × 循环次数 ,否则文件读完后 JMeter 会从头复用,参数重复直接命中同一份缓存,压力全被缓存吸收,测出来的 TPS 虚高。可按 Recycle on EOF = false,让数据耗尽时报错暴露问题。
  • 登录态:跨境电商常用多级 Token(网关 token + 业务 token),用 JSON Extractor 提取后放进 HTTP Header Manager,别只靠 Cookie Manager。
  • 业务断言别只看 HTTP 200 。跨境电商的失败常常是 200 里带 code: 40001(库存不足)或 code: 50010(汇率过期),必须加 JSON 断言校验业务码,否则错误率统计会失真。
  • 分布式压测 :master 只做调度,-Xms2g -Xmx2g 起堆,slave 数量按目标并发 ÷ 单机安全并发(一般 800~1000 虚拟用户)计算。注意 slave 与 master 的 JMeter 版本、插件版本、JDK 版本必须完全一致,否则 CSV 分发和结果聚合会出错。JMeter 5.x 起默认 RMI 走 SSL,跨机房部署要配好 keystore,否则连接不上。

Gatling 侧:给一段能直接跑的下单压测

原文那段 Scala 在 Gatling 3.7+ 已经编译不过(StringBody 是 Gatling 2 的写法),而且缺少参数化和思考时间,压出来的是纯机器流量。给一版可直接用的(Gatling 3.9 / Scala 2.13):

复制代码
package simulations

import io.gatling.core.Predef._
import io.gatling.http.Predef._
import scala.concurrent.duration._
import scala.util.Random

class CrossBorderOrderSimulation extends Simulation {

  private val baseUrl = sys.env.getOrElse("TARGET_HOST", "https://test-api.example.com")

  private val httpProtocol = http
    .baseUrl(baseUrl)
    .acceptHeader("application/json")
    .contentTypeHeader("application/json")
    .header("X-Pressure-Test", "true")          // 流量标记,必须带
    .shareConnections                            // 复用连接,更接近真实客户端

  // 商品 ID 与地址 ID 随机化,避免全部命中同一份缓存
  private val orderFeeder = Iterator.continually(Map(
    "goodsId"   -> (10000 + Random.nextInt(2000)).toString,
    "skuNum"    -> (1 + Random.nextInt(3)).toString,
    "addressId" -> f"addr_${Random.nextInt(5000)}%05d",
    "requestId" -> java.util.UUID.randomUUID().toString   // 幂等键
  ))

  private val orderScenario = scenario("Cross-border order peak")
    .feed(orderFeeder)
    .exec(
      http("POST /api/order/create")
        .post("/api/order/create")
        .body(StringBody(
          """{"goodsId":"${goodsId}","num":${skuNum},"addressId":"${addressId}","requestId":"${requestId}"}"""
        )).asJson
        .check(status.in(200, 409))              // 409 用于幂等冲突,不计为失败
        .check(jsonPath("$.code").saveAs("bizCode"))
    )
    .pause(200.millis, 800.millis)               // 思考时间,避免机械流量

  setUp(
    // 平滑爬坡到 8000 用户,保持 5 分钟
    orderScenario.inject(
      rampUsers(8000).during(60.seconds),
      constantUsersPerSec(8000 / 300.0).during(300.seconds)
    )
  ).protocols(httpProtocol)
    .assertions(
      global.failedRequests.percent.lte(0.1),
      global.responseTime.percentile3.lte(800),  // P95
      global.responseTime.percentile4.lte(1500)  // P99
    )
    .maxDuration(10.minutes)
}

几点说明:rampUsers + repeat 的组合会让请求总数等于用户数 × 循环次数,实际 TPS 曲线无法从报告里读出,改用 rampUsers + constantUsersPerSec 更容易和 TPS 目标对齐。.assertions 里的阈值就是第二步约定的 SLO,跑完自动判定通过与否,避免人工翻报告时的扯皮。requestId 幂等键一定要带,否则重复下单的统计分不清是系统缺陷还是脚本没做幂等。


六、监控与数据分析

监控分层,四层都要有基线才有定位能力:

  • 施压端:确认施压机自身 CPU 不超 70%、网络不跑满。很多"系统瓶颈"其实是施压机打不动。
  • 应用层:QPS、P95/P99、线程池活跃数与队列长度、GC 次数与暂停时长、SkyWalking 里的慢 Span。
  • 中间件:Redis 命中率与慢命令、MQ 生产速率与消费堆积量、网关限流触发计数。
  • 数据库:活跃连接数、慢查询条数(long_query_time=0.1 抓)、行锁等待时长、主从延迟。

看指标的顺序:先看错误率,再看 P99,最后看 TPS。错误率超过阈值时 TPS 再高也没有意义。CPU 长期高于 80% 通常是瓶颈信号,但也要排除是 GC 线程抢占------这时候看 CPU 在 GC 线程上的占比更准确。

典型瓶颈与对策(跨境场景):

现象 常见根因 优化方向
下单 TPS 上不去,DB CPU 不高 应用层同步锁或串行化逻辑 检查库存扣减是否全表加锁,改为按 SKU 分段的分布式锁 + Redis Lua 原子扣减
库存出现超卖 扣减非原子、锁粒度太粗 预扣库存落 Redis,异步落库,订单侧做幂等(唯一键 + requestId)
搜索接口 P99 抖动大 深分页、聚合查询、分片不均 限制 from+size 上限,改 search_after,按商品类目做路由分片
支付回调重复入账 第三方重试 + 本地无幂等 回调表加唯一索引,用业务单号做去重,处理失败进补偿队列
大促流量下雪崩 无限流、无降级、重试无预算 网关令牌桶限流、非核心接口(推荐、评论)先降级、重试加指数退避和抖动
长稳后响应逐渐变慢 连接池泄漏、缓存穿透 检查 HTTP 客户端连接未释放,热点 key 加空值缓存

七、常态化:把压测做成回归,而非一次性项目

一个可执行的节奏:

  • 每月跑一次单接口基线,形成趋势曲线。基线的价值在于"变化"------同一接口本月 TPS 比上月低 15%,即使仍达标,也该查一下是不是新加的拦截器拖慢了。
  • 每次版本上线前跑链路压测,重点压改动链路。
  • 大促前 3 周跑全模型压测(含脉冲和故障注入),留出优化窗口。
  • 压测报告存档,至少保留最近 6 次,对比表是容量规划最硬的依据。

最后一点经验:压测发现的问题里,约七成是配置问题而非代码问题------连接池上限、超时时间、线程池大小、Redis 最大连接、网关并发阈值。这些问题在压测环境里复现成本低,在生产里就是事故。所以压测报告里除了"哪个接口慢",更应该写清"哪个配置项限制了它"。

相关推荐
汪汪8888889 小时前
jmeter压测下载功能影响本地内存的方案
jmeter
han68892 天前
jmeter操作数据库
数据库·jmeter
川石课堂软件测试4 天前
涨薪技术|0到1学会性能测试第8课-Analysis分析图
服务器·数据库·python·sql·jmeter·mysql·容器
川石课堂软件测试4 天前
JMeter异步接口测试实战
数据库·python·jmeter·mysql·kubernetes·测试用例·prometheus
zuozewei7 天前
第02篇-k6-脚本结构与生命周期
软件测试·jmeter·性能测试·k6·ai 编程
fen_fen7 天前
Jmeter5.1.1连接oracle数据库的错误复盘
数据库·jmeter·oracle
川石课堂软件测试10 天前
自动化测试|持续集成Git使用详解
git·python·jmeter·mysql·ci/cd·容器·kubernetes
wuhuhuan12 天前
JMeter 连接数据库并取值(JDBC 实战)
数据库·jmeter
大貔貅喝啤酒13 天前
Windows 安装 使用JMeter 教程
windows·jmeter