一、为什么单工具压测在跨境场景跑不通
跨境电商的调用链比一般电商更长。一个订单从用户点下"提交"到最终落库,中间要穿过网关鉴权、商品服务、价格服务(要算完税价和实时汇率)、库存服务、优惠券、订单服务、支付网关、清关申报、物流下单,任何一环慢 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 的叠加量应按真实峰值里"纯下单请求"的占比折算。
二、先定目标,再谈工具
压测目标没定清,跑完只会得到一堆好看的曲线。跨境场景建议锁定四类目标:
- 容量水位:黑五峰值模型下,下单接口 TPS 上限、P99 响应时间、系统拐点出现在第几个并发档位。
- 保护策略是否真生效:限流阈值触发后是返回 429 还是静默丢弃、熔断后降级页是否可用、库存分布式锁在 5000 并发下会不会出现超卖。
- 长稳表现:8 小时持续压测下堆内存是否单调上升、连接池是否耗尽、MQ 是否积压。
- 可扩容性:应用节点从 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 最大连接、网关并发阈值。这些问题在压测环境里复现成本低,在生产里就是事故。所以压测报告里除了"哪个接口慢",更应该写清"哪个配置项限制了它"。