性能压测工具横评:JMeter vs wrk vs Gatling vs k6——该用哪个

引言

去年大促前,我们给订单服务做了一轮压测。报告很漂亮:

8,000 QPS,平均响应时间 38ms,错误率 0%。

老板批了扩容预算,团队安心等大促。结果开场第 7 分钟,订单服务在 1,200 QPS 就开始超时,网关 504 一片。

复盘会上有人问了一句:"那 8,000 QPS,到底是订单服务扛住的,还是压测机扛住的?"

会议室安静了。

把脚本和监控翻出来,四个问题摆上桌:

  1. 压测是同事在 JMeter GUI 里点的,本机 16 核开了 2,000 个线程,压测机 CPU 先到 100%,GUI 还在实时刷新结果树;
  2. 2,000 线程从第一秒压到最后一秒,没有阶梯、没有预热,JIT 和连接池冷启动阶段的数据被平均值抹平了;
  3. 只看平均响应时间 38ms,没人提 P99——事后翻原始数据,当时 P99 已经 2.3 秒;
  4. 所有请求都用同一个商品 ID,缓存命中率接近 100%,测出来的是"缓存有多快",不是"系统能扛多少"。

那次之后我才真正理解:压测工具决定你能不能看清瓶颈,压测方法决定你看到的是不是真相。

这篇文章把四款主流工具放在同一张桌子上:JMeter、wrk、Gatling、k6。不吹"谁最强"——它们压根不是同一类东西。

工具一句话定位最适合最不适合上手成本
JMeter图形化、插件最全的"压测 IDE"复杂业务链路、多协议、非程序员参与极限吞吐、CI 门禁低(GUI)/ 中(CLI)
wrk几十 KB 的 HTTP 压力发生器接口基线、网关调优、快速验证业务链路、断言、HTTP/2极低
Gatling用代码描述用户行为的 DSL复杂场景、漂亮报告、JVM 团队临时压测、轻量级验证中高(Scala)
k6为 CI 而生的 JS 压测工具门禁、云原生、Grafana 生态极限吞吐、无代码团队低中(JS)

一句话破题:JMeter 是瑞士军刀,wrk 是手术刀,Gatling 是可编程的流水线,k6 是写进 CI 的压测代码——先想清楚你要回答的是"极限数字""复杂链路"还是"每次合入都不许变慢",再选工具。选错了,压出来的数字连自己都不敢信。


一、四个工具,四种出身

1.1 JMeter:功能最全,也最"重"

出身:Apache 基金会顶级项目,Java 编写,历史可以追溯到 1998 年。

定位:把压测做成"可视化编程"——线程组、取样器、断言、定时器、监听器,拖拖拽拽就能拼出一条业务链路。

特点:

  • 协议覆盖最广:HTTP、JDBC、JMS、FTP、LDAP、SMTP、TCP,WebSocket 和 gRPC 靠插件补齐
  • 插件生态最成熟:Custom Thread Groups、PerfMon、Backend Listener、各类取样器
  • 支持分布式压测(controller / worker)
  • 内置 HTML 报告,含 APDEX、分位数、吞吐曲线

代价:

  • 一个虚拟用户就是一个 Java 线程,并发一高,压测机自己先吃不消
  • .jmx 是 XML,Git diff 几乎不可读,Code Review 无从谈起
  • GUI 只能用来编脚本,真压测必须走 CLI——这条坑每年都有人踩
# 正确的压测姿势:非 GUI 模式 + 结束后生成 HTML 报告
jmeter -n -t order.jmx -l result.jtl -e -o report/

1.2 wrk:一把只做一件事的手术刀

出身:Will Glozer 2012 年开源,C 语言 + epoll/kqueue + LuaJIT,代码量很小。

定位:只压 HTTP/1.1,只做"用固定连接数打满目标"这一件事。

特点:

  • 极轻:单机几万到几十万并发连接,内存占用仍然很低
  • 极快:没有 GC、没有虚拟机,压测端自身开销接近理论下限
  • 可用 Lua 脚本做参数化和简单逻辑
  • 一条命令出结果,适合随手验证

硬伤:

  • 只支持 HTTP/1.1,HTTP/2 完全不支持
  • 没有阶梯加压、没有到达率控制、没有断言、没有 HTML 报告
  • 没有分布式能力
  • 项目基本停更(4.2.0 之后多年没有新版本)
wrk -t8 -c256 -d30s --latency http://127.0.0.1:8080/api/orders/1

1.3 Gatling:把压测写成代码

出身:2011 年开源,Scala 编写,基于 Netty 的异步非阻塞引擎,开源版 Apache 2.0。

定位:用 DSL 描述用户行为,把压测脚本当成正式代码来维护。

特点:

  • 虚拟用户是"消息"而不是线程,少量线程就能驱动大量并发
  • 场景编排是一等公民:inject、pause、feed、check、assert
  • HTML 报告是四家里最漂亮的,分位数、时序图、错误分布一应俱全
  • 断言失败会让构建失败,天生适合 CI
  • 3.7 之后提供 Java DSL,不想写 Scala 的团队也能用

代价:

  • 要建工程、要编译,临时压一个接口显得笨重
  • Scala DSL 有学习曲线,报错信息对新手不友好
  • 分布式压测要上商业版 Gatling Enterprise
  • 插件生态比 JMeter 小一个量级
mvn gatling:test
# 或
gatling.sh -s com.example.OrderSimulation

1.4 k6:开发者体验优先

出身:LoadImpact 2017 年开源,后被 Grafana Labs 收购,Go 编写,脚本用 JavaScript。

定位:让压测脚本像单元测试一样进入研发流程。

特点:

  • 单二进制,装完就能跑,没有 JVM、没有依赖
  • 脚本是 JS,前端和后端都能改
  • 原生支持场景编排、到达率模型、阈值门禁
  • 原生支持 HTTP/1.1、HTTP/2、WebSocket、gRPC,浏览器级测试有 k6/browser
  • 输出可以直接打到 Prometheus、InfluxDB、Grafana Cloud
  • Kubernetes 上有官方 k6 Operator

代价:

  • 每个 VU 跑一个 JS 运行时,压测端吞吐明显低于 wrk 和 Gatling
  • 内置 HTML 报告要自己写 handleSummary
  • 特殊协议要靠 xk6 自己编译扩展
  • AGPL-3.0 许可证,商用嵌入前要让法务看一眼
k6 run script.js

二、评测方法:先把"公平"定义清楚

压测工具横评最容易犯的错,是拿 A 的极限去比 B 的默认配置。所以先把口径钉死。

2.1 测试环境

项目配置
压测机8 vCPU / 16GB,Linux,万兆网卡
目标机8 vCPU / 16GB,同可用区
目标服务Nginx 返回 1KB 静态 JSON,开启 keep-alive
并发256(四个工具统一)
时长60 秒(前 10 秒预热,取后 50 秒)
指标压测端吞吐、压测机 CPU、客户端观测 P99、错误率

目标服务选 Nginx 而不是业务服务,是为了让瓶颈尽量落在压测端——这样比较的才是"工具自身开销",而不是"谁的 Java 服务写得更快"。

2.2 统一场景

登录拿 token → 查订单 → 停顿 1 秒 → 循环。并发从 0 阶梯到 200,保持 5 分钟。

这个场景能同时考察四件事:多步骤会话、动态参数传递、think time、响应断言。

2.3 同一个场景,四种写法

JMeter:元件树就是脚本

Test Plan
├── Thread Group(线程数 200,Ramp-up 60s,循环 10)
│   ├── HTTP Cookie Manager
│   ├── HTTP Header Manager
│   ├── CSV Data Set Config(users.csv)
│   ├── Transaction Controller「登录」
│   │   ├── HTTP Request:POST /api/login
│   │   └── JSON Extractor:$.token → ${token}
│   ├── Transaction Controller「查订单」
│   │   ├── HTTP Request:GET /api/orders/${orderId}
│   │   ├── Response Assertion:200
│   │   └── JSON Assertion:$.status == "PAID"
│   └── Constant Timer:1000ms
└── Backend Listener(InfluxDB / Prometheus)

真正的脚本是一个几千行的 .jmx XML。改一个参数要在 GUI 里点半天,Review 时只能靠肉眼比对。

wrk:Lua 能做的事比想象中多,但也就到这里

-- tokens.txt:提前用登录接口批量换好的 token,每行一个
local tokens = {}
local idx = 0

function init(args)
    for line in io.lines("tokens.txt") do
        tokens[#tokens + 1] = line
    end
end

request = function()
    idx = idx + 1
    local token = tokens[(idx % #tokens) + 1]
    return wrk.format("GET", "/api/orders/1", {
        ["Authorization"] = "Bearer " .. token
    })
end

注意两个细节:init 在每个 wrk 线程里各执行一次,token 表是线程私有的;wrk 没有会话概念,登录链路只能提前在脚本外准备好。多步骤业务链路,wrk 天生不擅长。

Gatling:场景、检查、注入、断言,都是一等公民

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

class OrderSimulation extends Simulation {

  val httpConf = http
    .baseUrl("http://localhost:8080")
    .acceptHeader("application/json")

  val users = csv("users.csv").circular

  val scn = scenario("登录并查询订单")
    .feed(users)
    .exec(http("登录")
      .post("/api/login")
      .body(StringBody("""{"user":"${user}","pass":"${pass}"}""")).asJson
      .check(status.is(200))
      .check(jsonPath("$.token").saveAs("token")))
    .pause(1)
    .exec(http("查询订单")
      .get("/api/orders/${orderId}")
      .header("Authorization", "Bearer ${token}")
      .check(status.is(200))
      .check(jsonPath("$.status").is("PAID")))
    .pause(2, 5)

  setUp(
    scn.inject(
      rampUsersPerSec(1).to(200).during(60.seconds),
      constantUsersPerSec(200).during(300.seconds)
    )
  ).protocols(httpConf)
    .assertions(
      global.responseTime.percentile3.lt(500),
      global.successfulRequests.percent.gt(99.5)
    )
}

k6:用 JS 把同一件事写一遍

import http from 'k6/http';
import { check, sleep } from 'k6';

const BASE = 'http://localhost:8080';

export const options = {
  scenarios: {
    login_and_query: {
      executor: 'ramping-vus',
      startVUs: 0,
      stages: [
        { duration: '60s', target: 200 },
        { duration: '5m', target: 200 },
      ],
    },
  },
  thresholds: {
    http_req_duration: ['p(99)<500'],
    http_req_failed: ['rate<0.01'],
    checks: ['rate>0.995'],
  },
};

export function setup() {
  const res = http.post(
    `${BASE}/api/login`,
    JSON.stringify({ user: 'u1', pass: 'p1' }),
    { headers: { 'Content-Type': 'application/json' } }
  );
  return { token: res.json('token') };
}

export default function (data) {
  const res = http.get(`${BASE}/api/orders/1`, {
    headers: { Authorization: `Bearer ${data.token}` },
  });
  check(res, {
    'status is 200': (r) => r.status === 200,
    'status is PAID': (r) => r.json('status') === 'PAID',
  });
  sleep(1);
}

setup() 只执行一次,返回的数据在所有 VU 之间只读共享——这是 k6 处理"登录一次、全员复用"的标准姿势。

2.4 度量口径

指标怎么取为什么重要
吞吐成功响应数 / 有效时长别把错误响应也算进吞吐
P50 / P95 / P99 / P999原始样本分位数平均值会掩盖长尾
错误率非 2xx + 超时 + 断言失败高并发下的错误率比吞吐更早恶化
压测机 CPU / 软中断mpstat -P ALL 1、sar -n DEV 1判断压测端有没有先饱和
目标机饱和度CPU、GC、连接池、线程池队列找到真正的瓶颈

先记住一句:这组数字只用来比较"工具自身开销",不用来给目标服务下容量结论。 换一台机器、换一个响应体大小,绝对值会差几倍,但四个工具之间的相对关系比较稳定。


三、维度一:上手成本与表达力

3.1 第一条压测命令

工具最短路径到出结果需要什么
JMeter打开 GUI,建线程组 + HTTP 请求 + 监听器,点启动5–10 分钟JDK
wrkwrk -t8 -c256 -d30s URL10 秒一个二进制
Gatling建 Maven 工程、写 Simulation、mvn gatling:test30–60 分钟JDK + 构建工具
k6装二进制、写 20 行 JS、k6 run10–20 分钟一个二进制

如果只是想知道"这个接口大概能扛多少",wrk 的投入产出比没有对手。JMeter 赢在"不写代码也能拼出复杂链路",但它的最短路径恰恰是最容易出事的那条(GUI 压测)。

3.2 复杂场景的表达力

能力JMeterwrkGatlingk6
多步骤会话✅ 元件组合❌ 需脚本外预处理✅ DSL✅ JS
动态参数提取✅ JSON/正则/XPath⚠ Lua 手写✅ check + saveAs✅ res.json()
数据驱动✅ CSV Data Set⚠ Lua 读文件✅ Feeder✅ SharedArray
条件分支/循环✅ Controller❌✅ doIf / repeat✅ JS
自定义指标⚠ 需插件/脚本❌✅✅ 自定义 Metric
断言失败即失败⚠ 需额外解析❌✅✅

一个真实感受:用 JMeter 拼"登录 → 取 token → 下单 → 断言库存"这条链路,大概 20 个元件;用 Gatling 或 k6,是 30 行代码。前者上手快,后者改起来快——一次性脚本选 JMeter,长期维护的脚本选代码。

3.3 可维护性

维度JMeterwrkGatlingk6
版本化⚠ XML,diff 不可读✅ Lua 文本✅ 代码✅ 代码
Code Review❌ 基本没有⚠ 脚本短,能看✅✅
复用/封装⚠ Test Fragment❌✅ 函数/类✅ 模块
调试体验⚠ 只能看结果树⚠ print⚠ 编译期报错✅ console + --http-debug
与业务代码同仓❌⚠✅✅

这一维度决定了工具能不能长期活在团队里。压测脚本一旦变成"某人电脑上的一个 .jmx",下次大促前大概率要重写。

3.4 结论

上手速度:wrk > JMeter > k6 > Gatling
长期可维护性:Gatling ≈ k6 > wrk > JMeter


四、维度二:协议覆盖与场景编排

4.1 协议矩阵

协议JMeterwrkGatlingk6
HTTP/1.1✅✅✅✅
HTTPS✅✅ 需编译 OpenSSL✅✅
HTTP/2⚠ 第三方插件❌⚠ 以当前版本为准✅ 原生
WebSocket✅ 插件❌✅✅
gRPC⚠ 社区插件❌✅ 3.9+✅ 原生模块
TCP/UDP✅ TCP❌❌⚠ xk6 扩展
JDBC✅❌❌⚠ xk6 扩展
JMS / MQ✅ JMS❌✅ JMS⚠ xk6 扩展
MQTT✅ 插件❌⚠ 社区⚠ xk6 扩展
浏览器⚠ WebDriver Sampler❌❌✅ k6/browser

这张表基本决定了选型的下限:

  • 要压 JDBC、JMS、老协议 → 只有 JMeter 是稳妥选择
  • 要压 HTTP/2 或 gRPC → k6 最省事
  • 只想压 HTTP/1.1 接口 → 四个都能干,wrk 最轻

4.2 场景编排矩阵

能力JMeterwrkGatlingk6
固定并发✅✅✅✅
阶梯加压✅ 插件❌✅ 原生✅ 原生
恒定到达率✅ 插件/定时器❌✅ 原生✅ 原生
think time✅ Timer⚠ Lua delay()✅ pause✅ sleep
参数化✅⚠✅✅
断言✅❌✅✅
阈值门禁⚠ 自己解析❌✅✅
分布式✅ 自带❌⚠ 商业版✅ Operator/Cloud
录制/转换✅ 代理录制❌✅ HAR⚠ HAR 工具

wrk 的"没有阶梯、没有到达率"不是实现偷懒,是设计取舍:它只做固定连接数的闭模型压测。你可以用多个 wrk 进程叠加出阶梯,但那是手工活,不是工具能力。

4.3 一个容易被忽略的差异:开模型 vs 闭模型

  • 闭模型:固定 VU / 连接数,上一个请求完成后才发下一个。服务变慢 → 发压变慢 → 慢请求被"藏起来"
  • 开模型:固定到达率,不管服务多慢都按节奏发。服务变慢 → 请求排队 → 延迟真实暴露
工具默认模型怎么切到开模型
JMeter闭(线程)Arrivals Thread Group、Precise Throughput Timer
wrk闭(固定连接)不支持;用 wrk2 的 -R 近似
Gatling两种都支持constantUsersPerSec / rampUsersPerSec
k6两种都支持constant-arrival-rate / ramping-arrival-rate

这个差异比"谁快"重要得多,第六章详细讲。

4.4 结论

协议广度:JMeter > k6 > Gatling > wrk
场景编排:Gatling ≈ k6 > JMeter > wrk


五、维度三:压测端性能与资源开销

5.1 先搞清楚:你测的可能是压测机

压测系统的吞吐上限是:

实测吞吐 ≈ min(目标服务能力, 压测端发压能力, 网络带宽)

多数人只盯着第一项,忽略了后两项。当压测端先饱和时,你看到的"目标服务 P99 飙升"其实是压测机自己的排队。

四个工具的并发模型完全不同:

JMeter   1 VU = 1 个 Java 线程(阻塞 IO)
         → 线程栈 + 上下文切换 + GC,并发越高越重

wrk      1 线程 = 1 个 epoll 事件循环(固定线程数)
         → 无 GC、无虚拟用户对象,开销接近理论下限

Gatling  1 VU = 1 条消息(Netty 事件循环驱动)
         → 少量线程撑大量 VU,JVM 但非线程模型

k6       1 VU = 1 个 goroutine + 1 个 JS 运行时
         → 协程轻,但每条脚本都要过 JS 引擎

5.2 压测端吞吐实测

同一台 8 vCPU 压测机、同一个 Nginx 目标、256 并发、60 秒、1KB 响应:

工具压测端吞吐(req/s)压测机 CPU客户端观测 P99瓶颈在哪
wrk 4.240–55 万90–100%1.5–2.5ms接近目标上限
Gatling 3.x18–30 万80–95%2–4ms压测端
k6 0.5x/1.x8–15 万80–95%3–6msJS 运行时
JMeter 5.62.5–5 万90–100%8–15ms线程模型

三个读数:

  1. wrk 的吞吐就是"这台机器能发多快"的基准线。Gatling 大约是它的 50–60%,k6 约 20–30%,JMeter 约 5–10%。
  2. 同样 256 并发,JMeter 产生的实际 RPS 最低——不是它"测得不准",是它的线程在客户端侧就排队了,延迟读数被自身放大。
  3. 这些数字不是排行榜。换个响应体大小、换个 TLS、换个脚本复杂度,比例会变;但"wrk 最轻、JMeter 最重"这个排序很稳定。

5.3 资源占用

工具运行形态1,000 并发大致内存10,000 并发备注
JMeterJVM + 每 VU 一线程2–4GB需多机或大堆调优线程栈 + GC 是主要成本
wrk固定线程 + epoll几十 MB通常 <1GB内存随连接数增长,增长很慢
GatlingJVM + 消息驱动1–2GB数 GB,单机可撑吞吐受 CPU 与网络限制
k6Go + 每 VU 一个 JS VM1–3GB内存偏高,建议多实例脚本越复杂,单 VU 越贵

以上是量级,不是精确值——脚本里有没有 JSON 解析、响应体多大、TLS 开不开,都会让数字翻倍。

5.4 压测机自检:别让工具背锅

正式压测前,先压一个"几乎不耗 CPU"的本地接口,确认压测端的天花板:

# 1. 压测端极限(目标换成本机一个空接口)
wrk -t8 -c256 -d30s --latency http://127.0.0.1:9999/ping

# 2. 压测机 CPU / 软中断
mpstat -P ALL 1
sar -n DEV 1

# 3. 连接数与文件描述符
ss -s
ulimit -n

# 4. 端口耗尽与 TIME_WAIT
sysctl net.ipv4.ip_local_port_range
ss -tan state time-wait | wc -l

经验值:目标 QPS 不要超过压测端极限的 50–60%。超过这条线,你测到的延迟里有一半是压测机自己的排队。

5.5 结论

压测端性能:wrk > Gatling > k6 > JMeter
但请注意:压测端性能只在"你要压的 QPS 接近工具上限"时才有意义。 压一个 5,000 QPS 的业务服务,四个工具都不会成为瓶颈,这时该看的是表达力和可维护性。


六、维度四:结果可信度与报告

6.1 报告能力

能力JMeterwrkGatlingk6
实时进度✅✅✅✅
HTML 报告✅ 内置(APDEX/分位数/时序)❌✅ 内置,最完整⚠ 需 handleSummary
原始样本✅ .jtl❌ 只有汇总✅✅ JSON/CSV
时序数据落库✅ Backend Listener❌⚠ 需插件✅ Prometheus/InfluxDB
分位数✅✅ --latency 直方图✅✅ 可配置
错误归因✅ 断言结果⚠ 只统计非 2xx✅ 错误分类✅ 错误码 + check
断言即门禁⚠❌✅✅

报告漂亮程度:Gatling > JMeter > k6 > wrk。
但报告漂亮不等于数字可信,接下来这个坑比报告重要十倍。

6.2 协调遗漏:一个把 P99 从 2 秒变成 200 毫秒的陷阱

闭模型压测里,VU 必须等上一个响应回来才能发下一个。服务端一慢,发压节奏自动变慢,最慢的那批请求反而采样最少——这就是 coordinated omission。

真实情况(假设每 100ms 该发一个请求):
t=0     发请求 A → 2000ms 才返回
t=100   本该发 B,但线程在等 A,没发
t=200   没发
...
t=2000  A 返回,记一笔 2000ms,然后发 B
        B 很快返回 10ms

统计结果:
  样本 = [2000, 10, 10, 10, ...]
  大量本该在等待期间发出的请求根本没被记录
  P99 被"稀释"成 200ms,真实用户体验是 2 秒

四个工具的处理方式:

工具是否处理怎么做
wrk❌ 不处理固定连接数的闭模型;用 wrk2 的 -R 按固定速率发压
JMeter⚠ 默认不处理换 Arrivals Thread Group / Precise Throughput Timer
Gatling✅ 可处理用 constantUsersPerSec / rampUsersPerSec 的开模型
k6✅ 可处理用 constant-arrival-rate / ramping-arrival-rate

wrk2 的修正用法:

# 按固定 20000 req/s 发压,延迟按"本该发出的时间"计算
wrk2 -t8 -c256 -R20000 -d60s --latency http://127.0.0.1:8080/api/orders/1

k6 的开模型写法:

export const options = {
  scenarios: {
    peak: {
      executor: 'ramping-arrival-rate',
      startRate: 100,
      timeUnit: '1s',
      preAllocatedVUs: 200,
      maxVUs: 2000,
      stages: [
        { target: 1000, duration: '2m' },
        { target: 3000, duration: '3m' },
        { target: 0, duration: '1m' },
      ],
    },
  },
};

Gatling 的开模型写法:

setUp(
  scn.inject(
    rampUsersPerSec(100).to(3000).during(5.minutes)
  )
)

6.3 四个工具的延迟口径

工具默认延迟指标包含什么注意
JMeterelapsed / latency / connectelapsed 是整请求;latency 是首字节报告里默认给平均值,必须手动看分位数
wrkLatency单请求往返--latency 才有分位数
Gatlingresponse time请求开始到响应结束,不含 pause报告默认给分位数,比较良心
k6http_req_durationsending + waiting + receivinghttp_req_waiting 才是 TTFB

6.4 结论

报告完整度:Gatling > JMeter > k6 > wrk
结果可信度:取决于你有没有用开模型,而不是取决于工具。 用闭模型压一个会变慢的系统,四个工具都会给你一个偏乐观的 P99。


七、维度五:CI/CD 与云原生

7.1 门禁能力

工具失败退出码官方 CI 集成典型做法
JMeter⚠ 默认不反映断言失败jmeter-maven-plugin解析 .jtl 或插件 ignoreResultFailures=false
wrk❌ 没有失败概念无自己 grep 输出,脆弱
Gatling✅ 断言失败构建失败Maven / Gradle / sbt 插件mvn gatling:test
k6✅ 阈值失败退出码 99CLI 即可,无需插件k6 run script.js

k6 的门禁写法最直接:

export const options = {
  thresholds: {
    http_req_duration: ['p(95)<300', 'p(99)<800'],
    http_req_failed: ['rate<0.005'],
  },
};

任何一条不满足,进程退出码 99,流水线直接红。

Gatling 的断言同理:

.assertions(
  global.responseTime.percentile3.lt(500),
  global.successfulRequests.percent.gt(99.5)
)

7.2 容器化

工具镜像形态大致体积启动速度
JMeterJVM + JMeter400–600MB数秒
wrkAlpine + 二进制<20MB毫秒级
GatlingJVM + Gatling500MB+数秒
k6单二进制~80MB毫秒级

在 Kubernetes 里跑一次性压测 Job,镜像大小直接影响调度和冷启动。wrk 和 k6 在这一项上优势明显。

7.3 分布式压测

工具方案成本
JMeter自带 controller / worker免费,但要做 RMI 网络与时间同步
wrk无自己起多个进程/机器再汇总
GatlingGatling Enterprise商业授权
k6k6 Operator(K8s)/ Grafana Cloud k6Operator 免费,Cloud 按量

JMeter 的分布式是四家里唯一"开源且原生"的,但配置并不轻松:worker 要开 jmeter-server,controller 要能访问 worker 的 RMI 端口,结果汇总还有时钟偏差问题。

# jmeter.properties(controller 侧)
remote_hosts=10.0.0.11:1099,10.0.0.12:1099
# worker 侧
jmeter-server -Djava.rmi.server.hostname=10.0.0.11

# controller 侧,-r 表示分发到 remote_hosts
jmeter -n -t plan.jmx -r -l result.jtl -e -o report/

7.4 与可观测性栈集成

工具PrometheusGrafanaInfluxDB备注
JMeter⚠ 插件⚠ 通过 Backend Listener✅配置繁琐
wrk❌❌❌只能解析文本
Gatling⚠ 插件⚠⚠开源版偏弱
k6✅ remote-write✅ 原生生态✅一条参数搞定
k6 run --out experimental-prometheus-rw script.js

7.5 许可证

工具许可证商用注意
JMeterApache-2.0无限制
wrkApache-2.0无限制
GatlingApache-2.0(开源核心)分布式/企业功能需商业授权
k6AGPL-3.0内部使用没问题;嵌入对外产品需法务评估

k6 的 AGPL 是最容易被忽略的一条。只是内部压测、CI 跑脚本,不构成分发;但如果把 k6 打包进对外销售的软件,就要认真看一遍许可证。

7.6 结论

CI 友好度:k6 > Gatling > JMeter > wrk
云原生程度:k6 > Gatling > JMeter > wrk


八、维度六:生态与长期维护

8.1 插件与扩展

工具扩展方式生态规模典型扩展
JMeter插件包 + JSR223最大Custom Thread Groups、PerfMon、WebSocket、gRPC
wrkLua 脚本很小自定义请求、简单断言
GatlingScala/Java 库 + 官方模块中等JMS、gRPC、JDBC feeder
k6xk6 扩展 + JS 库中等且增长快SQL、Redis、Kafka、MQTT、浏览器

JMeter 的插件生态是它最深的护城河。很多"JMeter 不支持"的协议,最后都能找到一个社区插件——代价是插件质量参差不齐,版本兼容性要自己扛。

8.2 版本活跃度

工具维护状态风险
JMeterApache 基金会,5.6.x 持续维护迭代慢,但不会消失
wrk4.2.0 后基本冻结新特性别指望,能用多久看社区
Gatling商业公司维护,3.x 活跃开源版功能有边界,高级能力导向商业版
k6Grafana 主导,版本活跃与 Grafana 生态绑定加深

wrk 的"停更"不一定是问题——它足够小、足够稳定,HTTP/1.1 的压测需求也不会变。但如果你的目标是 HTTP/2、gRPC,它连门槛都进不去。

8.3 团队适配

团队画像最顺手
测试团队为主,开发参与少JMeter
后端团队,习惯写代码k6 / Gatling
有 Scala/JVM 背景Gatling
前端也能帮忙改脚本k6
只做接口冒烟wrk

8.4 结论

生态广度:JMeter > k6 > Gatling > wrk
长期活跃度:k6 ≈ Gatling > JMeter > wrk


九、综合对比

9.1 六维评分

维度JMeterwrkGatlingk6
上手成本⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
脚本表达力⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
协议/场景覆盖⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
压测端性能⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
报告与指标⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
CI / 云原生⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
综合⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐

9.2 关键数据汇总

指标JMeterwrkGatlingk6
压测端吞吐(256 并发)2.5–5 万40–55 万18–30 万8–15 万
HTTP/2插件❌视版本✅
开模型压测插件wrk2✅✅
断言门禁需解析❌✅✅
分布式自带❌商业版Operator
单二进制❌✅❌✅
许可证Apache-2.0Apache-2.0Apache-2.0AGPL-3.0

9.3 适用场景

场景推荐原因
接口快速验证wrk一条命令,10 秒出结果
网关/Nginx 调优wrk压测端开销最低,不干扰判断
复杂业务链路JMeter元件组合 + 插件,非程序员也能维护
JDBC/JMS/老协议JMeter四家里唯一覆盖
漂亮报告 + 复杂场景GatlingDSL + 报告 + 断言
CI 门禁k6阈值退出码 + 单二进制
K8s 分布式压测k6Operator 原生
HTTP/2、gRPCk6原生支持
极限吞吐基准wrk压测端天花板最高

十、选型建议:按场景给答案

10.1 决策树

你要压什么?
├── 只想快速知道"这个接口能扛多少"(HTTP/1.1)
│   └── wrk(要看真实延迟再加 wrk2 -R)
├── 复杂业务链路:登录、token、多接口关联、多协议
│   └── JMeter
├── 每次合入都要跑、结果要卡门禁、要接 Grafana
│   └── k6
├── 团队有 JVM/Scala 背景、要漂亮报告、要长期维护
│   └── Gatling
└── 要压 HTTP/2 / gRPC / WebSocket
    └── k6(或 JMeter + 插件)

10.2 如果只能选一个

选 k6。

理由不是它每一项都最强,而是它在"够用"和"能进流程"之间平衡得最好:

  • 单机 8–15 万 RPS,覆盖绝大多数业务的压测需求
  • 脚本是代码,能进 Git、能 Review、能复用
  • 阈值即门禁,CI 里一行命令就能卡住性能回退
  • HTTP/2、gRPC、WebSocket 都是原生能力
  • 输出能直接接 Prometheus + Grafana,和现有可观测性栈打通

wrk 留着做接口基线——一个二进制,不占地方,排查网关问题时比什么都快。

JMeter 只在两种情况下保留:需要压 JDBC/JMS 等非 HTTP 协议,或者测试团队需要不写代码维护脚本。

Gatling 适合已经深度使用 JVM 技术栈、且对报告和场景编排有强要求的团队。

10.3 三种典型组合

团队阶段组合分工
起步期wrk + k6wrk 做接口基线,k6 做 CI 冒烟
成长期k6 + JMeterk6 做门禁和日常压测,JMeter 补复杂协议
成熟期k6 + Gatling + wrkk6 门禁、Gatling 全链路、wrk 极限基准

工具不是婚姻,没必要从一而终。关键是统一指标口径,否则三个工具测出三套数字,复盘时又要吵架。

10.4 反模式清单

反模式后果正确做法
JMeter GUI 模式跑正式压测GUI 和监听器吃掉大量资源,数字失真jmeter -n,关闭结果树
只看平均响应时间长尾被平均掉,线上照样超时看 P99/P999
恒定并发一把压到底看不到拐点,也无法定位容量阶梯加压 + 预热
压测机和目标机同机互相抢 CPU,测的是自己分机部署,先做压测端自检
所有请求同一个 ID缓存全命中,数字虚高参数化 + 真实数据分布
用 wrk 做 CI 门禁没有断言、没有退出码用 k6 或 Gatling
用 k6 压工具极限先被 JS 运行时拖住,误判目标容量极限吞吐用 wrk
拿一次压测结果当永久容量版本、数据量、依赖都在变每次发布前回归

10.5 从 JMeter 迁到 k6 的映射表

JMeterk6 对应
Thread Groupscenarios
Ramp-upramping-vus
Loop Countiterations
CSV Data Set ConfigSharedArray + open()
JSON Extractorres.json('$.field')
Response Assertioncheck()
Constant Throughput Timerconstant-arrival-rate
Backend Listener--out experimental-prometheus-rw
Aggregate ReporthandleSummary()

迁移的难点不在语法,在思维:JMeter 是"配置元件",k6 是"写代码"。前者靠点,后者靠想。


十一、比工具更重要的事:一次可信压测的检查清单

11.1 压测机不能是瓶颈

# 压测端极限自检
wrk -t8 -c256 -d30s --latency http://127.0.0.1:9999/ping

# 观察
mpstat -P ALL 1        # CPU 是否单核打满
sar -n DEV 1           # 网卡是否打满
ss -s                  # 连接数
ulimit -n              # 文件描述符

压测端 CPU 超过 80%、网卡接近带宽上限、出现大量 TIME_WAIT,任何一条成立,这轮数据就只能参考,不能作为容量结论。

11.2 阶梯加压,不要一把梭

并发
 3000 ┤                    ╭────╮
 2000 ┤              ╭─────╯    ╰───
 1000 ┤        ╭─────╯
    0 ┼────╮───╯
      └────┴────┴────┴────┴────┴────▶ 时间
       预热  爬坡   稳态   峰值   回落

爬坡段用来找拐点,稳态段用来测容量,峰值段用来验证过载保护。只有稳态段的数据,说明不了系统在压力变化时的行为。

11.3 看分位数、错误率和饱和度

指标看什么危险信号
P99长尾用户体验超过 SLA 的 1.5 倍
P999极端慢请求出现秒级尖刺
错误率非 2xx / 超时 / 断言失败拐点前就开始上升
CPU目标机使用率持续 >80%
GCFull GC 频率与停顿吞吐上升时 GC 时间同步上升
连接池等待队列长度请求排队,QPS 不再增长
线程池活跃线程与队列队列持续增长

11.4 数据和缓存要"像生产"

压测数字虚高,最常见的原因是数据不真实:

  • 所有请求同一个用户、同一个商品 → 缓存命中率接近 100%
  • 数据库只有 1 万行 → 索引全部在内存
  • 没有走真实的下游依赖 → 跳过了最慢的那一跳

至少准备三组数据:热数据(缓存命中)、冷数据(穿透)、混合分布(接近生产)。

11.5 上线前检查清单

检查项标准
压测端自检目标 QPS < 压测端极限的 60%
压测模式非 GUI(JMeter)/ 独立进程(wrk)
负载模型阶梯加压,含预热与稳态
指标口径P99 + 错误率 + 饱和度
数据分布热/冷/混合三组
依赖下游不 mock,或明确标注 mock 范围
结果留存原始样本 + 报告 + 脚本版本号
回归对比与上一版本同口径对比

十二、常见问题

12.1 wrk 支持 HTTPS 和 HTTP/2 吗?

HTTPS 支持,但编译时要带 OpenSSL:

make WITH_OPENSSL=1

HTTP/2 不支持,wrk2 也不支持。要压 HTTP/2,用 k6、Gatling(确认版本)或 h2load。

12.2 JMeter 的 GUI 到底能不能压测?

能跑,但不该跑。GUI 里的监听器会把每个请求的完整结果留在内存里,几千并发就能把堆打爆;GUI 自身的渲染也抢 CPU。正确姿势是 GUI 只用来编脚本和调试,正式压测一律 jmeter -n,并且关闭"查看结果树"。

12.3 k6 比 wrk 慢,是不是不适合大压力?

看你的"大压力"是多少。k6 单机 8–15 万 RPS,对绝大多数业务服务来说远超其承载能力。真正需要压到几十万、上百万 RPS 的场景(网关、静态资源、极限基准),才应该用 wrk。k6 的短板是压测端开销,不是场景能力。

12.4 Gatling 必须会 Scala 吗?

3.7 之后有 Java DSL,Java 团队可以不写 Scala。但复杂场景、自定义 feeder、动态断言,最后还是绕不开一点 Scala 知识。如果团队完全没有 JVM 函数式编程经验,k6 的上手成本更低。

12.5 为什么压测结果和线上差那么多?

通常是四件事没对齐:流量模型(并发 vs 到达率)、数据分布(缓存命中率)、依赖范围(mock 掉了最慢的下游)、环境差异(机器规格、网络延迟、GC 参数)。压测不是"跑个数字",是"复现生产流量模型"。

12.6 四个工具能混用吗?

能,而且推荐。常见组合是:wrk 做接口基线和网关调优,k6 做 CI 门禁,JMeter 补复杂协议,Gatling 做全链路回归。前提是统一指标口径——同一套 SLA、同一种延迟定义、同一套数据分布,否则数字没有可比性。

12.7 k6 的 AGPL 许可证有风险吗?

内部使用、CI 跑脚本、自建 Prometheus 输出,都不涉及分发,没有额外义务。只有把 k6 嵌入对外交付的产品时才需要评估。拿不准就让法务看一眼,不要靠猜。

12.8 压测机要几台?

先测单台极限,再按"目标 QPS ÷ 单台极限 × 1.7"估算。系数 1.7 是留给网络抖动、脚本复杂度和安全余量的。能一台搞定就别上分布式——分布式压测的时间同步和结果汇总本身就会引入误差。


十三、总结

最终结论

压测工具选型:
├── 快速验证 / 接口基线 / 网关调优 → wrk
├── 复杂业务链路 / 多协议 / 非程序员维护 → JMeter
├── 复杂场景 + 漂亮报告 + JVM 团队 → Gatling
├── CI 门禁 / 云原生 / HTTP/2 / gRPC → k6
└── 只能选一个 → k6(wrk 作为补充)

六维速查表

维度JMeterwrkGatlingk6
上手成本中最低最高低
表达力中低最高高
协议覆盖最广仅 HTTP/1.1较广较广
压测端性能最低最高高中
报告完整无最漂亮够用
CI/云原生弱弱强最强

一句话

wrk 回答"这台机器能发多快",JMeter 回答"这条业务链路能不能跑通",Gatling 回答"复杂场景下的性能画像是什么",k6 回答"这次合入有没有让系统变慢"——工具本身没有高下,问题没有问对,才是真的白压。

关键数据

  • 同样 256 并发,压测端吞吐:wrk 40–55 万 > Gatling 18–30 万 > k6 8–15 万 > JMeter 2.5–5 万 RPS
  • wrk 只支持 HTTP/1.1;k6 原生支持 HTTP/2、gRPC、WebSocket
  • JMeter 默认退出码不反映断言失败;k6 阈值失败退出码 99
  • 闭模型压测会低估 P99,wrk2、Gatling 开模型、k6 arrival-rate 都能修正
  • 目标 QPS 不应超过压测端极限的 50–60%

给团队的建议

项目状态建议
还没有压测体系先上 k6,配 CI 冒烟,再补 wrk 基线
已有大量 JMeter 脚本保留复杂链路,简单接口逐步迁到 k6
测试团队主导JMeter 继续用,但强制 CLI 模式
追求极限吞吐wrk + wrk2,别用业务工具压网关
要接 Grafanak6 + Prometheus remote-write
要压 HTTP/2 / gRPCk6,别在 JMeter 插件上耗时间
准备上生产按 11.5 的检查表逐项过,先做压测端自检

互动话题:你们团队现在用哪个压测工具?有没有遇到过"压测报告很漂亮、上线就崩"的情况?是工具选错了,还是压测方法本身有问题?评论区聊聊,点赞最高的场景我拆成下一篇的完整复盘。


参考资料


标题:性能压测工具横评:JMeter vs wrk vs Gatling vs k6——该用哪个
作者:jiangyi
地址:http://jiangyi.space/articles/2026/10/26/1792923134743.html
公众号:服务端技术精选
    评论
    0 评论
avatar

取消