K6性能测试实战:从零构建开发者友好的压测工作流
1. 为什么是 K6,而不是 JMeter 或 Locust?
我第一次在客户现场看到性能测试需求时,团队还在用 JMeter。当时一台 16 核 32G 的压测机,跑 500 并发就卡得连监听器都刷新不动,堆栈日志里全是OutOfMemoryError: Java heap space。运维同事一边调-Xmx8g一边叹气:“这玩意儿不是在测接口,是在测我们 JVM 参数调得够不够熟。”后来换上 Locust,Python 写起来确实顺手,但一到 3000+ 并发,GIL 锁住的不只是线程,还有我们排查 CPU 瓶颈的耐心——top里 Python 进程 CPU 占满却 QPS 上不去,最后发现是单进程模型扛不住高并发调度开销。
直到去年接手一个实时风控 API 的压测任务,要求模拟 10 万用户每秒发起 2 万次决策请求,且必须支持动态 token 刷新、灰度路由 header 注入、以及按用户画像分组施压(比如 30% 新用户走 A 链路,70% 老用户走 B 链路)。我们试了三天,最终落地的是 K6。不是因为它“新”,而是它把三件关键事做对了:用 Go 编译成单二进制,内存常驻开销低于 15MB;用 JavaScript(ES6+)写脚本,但运行时由 V8 引擎 JIT 编译,无解释器瓶颈;原生支持分布式执行,但控制面极轻——你不需要部署一套独立的协调集群,只要k6 run --vus 10000 --duration 5m script.js,它自己就能把压力均匀分发到本地所有 CPU 核心上。
K6 不是另一个“又一个压测工具”,它是把性能测试从“运维配合型任务”拉回“研发可自主驱动型实践”的关键拐点。它不强制你学新语言(JS 是前端/后端/脚本工程师的通用母语),不绑架你进复杂的 UI 配置流程(没有“添加线程组→添加 HTTP 请求→添加断言→添加监听器”这种 Wizard 式操作),更不让你为压测环境单独维护一套 Java/Python 运行时。你写的.js文件,就是可版本化、可 Code Review、可 CI/CD 自动触发的测试资产。我见过最典型的场景是:前端同学改完登录页的 JWT 解析逻辑,顺手在 PR 里加了一行k6 run --quiet login_stress.js到 GitHub Actions,CI 流水线跑完自动输出 P95 延迟报表——这件事在 JMeter 时代需要测试工程师手动导出 jmx、上传到压测平台、等审批、再排队执行,平均耗时 4 小时。
所以当你看到“K6 性能测试教程”这个标题,请先放下“又一个工具入门”的预设。这不是教你点几下鼠标,而是带你重建一套以代码为中心、以开发者为第一用户、以生产环境真实流量模型为标尺的性能验证工作流。接下来所有内容,都围绕一个目标展开:让你在 2 小时内,从零写出第一个能真实反映业务 SLA 达标情况的 K6 脚本,并理解每一行代码背后的资源消耗逻辑和可观测性设计意图。
2. 环境搭建:为什么只装一个二进制,却要理解三个底层机制?
K6 官方文档说“下载二进制,chmod +x,直接运行”,这句话没错,但如果你真这么干,在后续调试中一定会栽跟头。我见过太多人卡在第一步:k6 run script.js报错failed to start the VU: context deadline exceeded,查了一上午以为是网络问题,最后发现是本地 DNS 解析超时——而这个超时值,恰恰由 K6 启动时隐式加载的三个核心机制共同决定。下面我把环境搭建拆成“物理安装”和“逻辑就绪”两层,后者才是真正决定你能否顺利跑通第一个脚本的关键。
2.1 物理安装:跨平台二进制的正确打开方式
K6 提供 macOS / Linux / Windows 三端预编译二进制,绝对不要用包管理器(如 brew、apt、choco)安装。原因很现实:包管理器安装的版本往往滞后 2~3 个 minor release,而 K6 的 breaking change 主要集中在http.batch()的返回结构、check()函数的 error handling 行为、以及--out输出插件的参数命名上。我们曾因brew install k6装了 v0.43.0,而文档示例基于 v0.45.0,导致http.batch([...]).map(...)报TypeError: Cannot read property 'map' of undefined,排查了 3 小时才发现是版本错配。
正确做法是:
- 访问 https://github.com/grafana/k6/releases (注意:只认官方 GitHub Release 页面,不认任何镜像站或第三方打包源)
- 找到最新 stable 版本(如
v0.46.0),下载对应平台的k6-v0.46.0-xxx.tar.gz - 解压后得到单文件
k6,执行chmod +x k6,然后sudo mv k6 /usr/local/bin/ - 验证:
k6 version应输出k6 v0.46.0 (go1.21.6, linux/amd64)(末尾的go1.21.6, linux/amd64是关键,它告诉你 K6 运行时依赖的 Go 版本和系统架构)
提示:Windows 用户请下载
k6-v0.46.0-windows-amd64.zip,解压后将k6.exe放入PATH。不要尝试用 WSL 运行 Linux 版本——K6 的--vus参数会按宿主机 CPU 核心数分配 VU(Virtual User),WSL 下读取的是 WSL 虚拟机的 CPU 数,而非 Windows 物理核数,极易导致压测强度严重失真。
2.2 逻辑就绪:必须掌握的三个隐式机制
K6 启动时,会静默初始化三个影响脚本行为的核心模块,它们不写在文档首页,却是你写脚本前必须心里有数的底层契约:
第一,VU(Virtual User)生命周期与内存模型
每个 VU 是一个独立的 JavaScript 执行上下文,拥有自己的全局作用域、堆内存和事件循环。K6 不共享 VU 间的变量(let counter = 0在每个 VU 里都是独立副本),但共享init context(即脚本顶层代码)。这意味着:你在脚本顶部const token = getAuthToken(),这个 token 会被所有 VU 复用;但let reqCount = 0放在default function() {}里,每个 VU 都有自己的计数器。这个设计直接决定了你如何管理状态——比如 JWT token 刷新,你不能在default函数里每次请求都重新获取,而应该用setup()函数预生成 token 池,再通过__ENV或sharedArray分发给各 VU。
第二,HTTP Client 的连接复用策略
K6 默认启用 HTTP/1.1 Keep-Alive 和 HTTP/2 多路复用,但它的连接池大小是硬编码的:每个 VU 最多维持 100 个空闲连接。这个值无法通过 CLI 参数调整,只能在脚本里显式配置:
import http from 'k6/http'; export const options = { vus: 100, duration: '30s', // 关键:覆盖默认连接池行为 thresholds: { 'http_req_duration': ['p(95)<200'] }, }; // 在 default 函数里,必须显式设置 keepalive const params = { headers: { 'Content-Type': 'application/json' }, // 这行决定连接是否复用 tags: { name: 'login_api' } }; // 注意:http.get(url, params) 默认复用连接 // 但 http.post(url, body, params) 如果没设 params,会新建连接如果你忽略这点,在高并发下会看到大量connection refused或too many open files错误——因为每个 VU 尝试建立超过 100 个新连接,而系统文件描述符(ulimit -n)默认只有 1024。
第三,时钟精度与时间戳对齐机制
K6 的Date.now()返回的是纳秒级单调时钟(monotonic clock),而非系统 wall-clock。这意味着:即使你手动修改系统时间,K6 脚本里的new Date().getTime()依然线性增长。这个设计保证了压测过程中sleep()、check()超时判断、以及rate指标计算的绝对稳定性。但副作用是:你无法用new Date()获取真实世界时间来做日志标记(比如console.log('Request at', new Date())打印的其实是相对启动时间的毫秒偏移)。解决方案是使用__ENV.K6_INSTANCE_ID结合Date.now()做逻辑时间戳,或直接用k6自带的execution对象:
export default function () { // 正确获取当前 VU 的执行阶段信息 console.log(`VU ${__VU} in iteration ${__ITER}, time elapsed: ${__ENV.K6_INSTANCE_ID}`); }这三个机制,构成了 K6 区别于其他工具的底层骨架。你不必记住所有细节,但必须建立直觉:K6 的“轻量”不是功能少,而是把复杂性收敛到可预测、可推演的几个关键点上。接下来写脚本时,每一个import、每一个export、每一个sleep()调用,背后都在和这三者发生交互。
3. 编写第一个脚本:从“Hello World”到真实业务流量建模
很多教程教的第一个脚本是http.get('https://test.k6.io'),这就像教人开车先让学员在空停车场绕圈——安全,但离真实路况差了十万八千里。真正的“第一个 K6 脚本”,必须包含四个不可省略的要素:身份认证、请求参数化、结果校验、性能指标埋点。下面我带你写一个真实的电商登录接口压测脚本,它能跑通,也能暴露你环境中所有潜在瓶颈。
3.1 脚本骨架:为什么setup()和teardown()不是可选项?
先看完整脚本(已脱敏,保留所有关键结构):
import http from 'k6/http'; import { check, sleep, group } from 'k6'; import { Rate } from 'k6/metrics'; // 1. 初始化阶段:只执行一次,在所有 VU 启动前 export function setup() { // 模拟登录获取 token(这里用静态 token 替代真实调用,实际应调用 auth API) const res = http.post('https://api.example.com/auth/login', JSON.stringify({ username: 'test_user', password: 'secure_pass_123' }), { headers: { 'Content-Type': 'application/json' } }); // 断言登录成功,失败则整个压测中止 check(res, { 'login status is 200': (r) => r.status === 200, 'token exists in response': (r) => r.json().token !== undefined }); return { token: res.json().token }; } // 2. 主压测逻辑:每个 VU 独立执行 export default function (data) { // data 是 setup() 返回的对象,所有 VU 共享同一份 token const token = data.token; // 分组标记,便于后续 Grafana 查看不同接口的指标 group('Login Flow', function() { // 第一步:获取用户基本信息(GET) const userInfoRes = http.get('https://api.example.com/user/profile', { headers: { 'Authorization': `Bearer ${token}`, 'X-Client-ID': 'web_app_v2' } }); // 第二步:提交登录后行为日志(POST) const logRes = http.post('https://api.example.com/log/event', JSON.stringify({ event: 'login_success', timestamp: Date.now(), user_id: 'test_user' }), { headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' } }); // 第三步:检查两个请求是否都成功 const success = check(userInfoRes, { 'user info status is 200': (r) => r.status === 200, 'user info has name field': (r) => r.json().name !== undefined }) && check(logRes, { 'log event status is 200': (r) => r.status === 200, 'log event returns id': (r) => r.json().event_id !== undefined }); // 仅当全部检查通过,才计入成功率指标 if (success) { loginSuccessRate.add(1); } else { loginSuccessRate.add(0); } }); // 每次迭代后休眠 1~3 秒,模拟真实用户思考时间 sleep(Math.random() * 2 + 1); } // 3. 清理阶段:所有 VU 执行完毕后执行一次 export function teardown(data) { console.log('Teardown completed. Total VUs:', __ENV.K6_VUS); } // 4. 自定义指标:必须在脚本顶层声明 const loginSuccessRate = new Rate('login_success_rate'); // 5. 压测配置:决定如何运行这个脚本 export const options = { stages: [ { duration: '30s', target: 10 }, // ramp-up 10 VUs in 30s { duration: '1m', target: 10 }, // stay at 10 VUs for 1m { duration: '30s', target: 50 }, // ramp-up to 50 VUs { duration: '2m', target: 50 }, // hold at 50 VUs ], thresholds: { // 关键 SLA 指标:P95 延迟 < 500ms,成功率 > 99.5% 'http_req_duration': ['p(95)<500'], 'login_success_rate': ['rate>0.995'], // 额外监控:错误率不能超过 0.1% 'http_req_failed': ['rate<0.001'] } };现在逐段解析为什么这样写:
setup()函数:不是“可选”,而是“必填”
它解决的是“状态前置准备”问题。真实业务中,90% 的接口都需要认证 token。如果把这个逻辑放在default函数里,每个 VU 每次迭代都去调一次/auth/login,那你的压测就变成了“测鉴权服务”,而不是“测目标接口”。setup()确保 token 只获取一次,然后通过return传递给所有 VU,既节省资源,又符合真实用户行为(用户登录一次,后续请求复用 token)。
group()的真实价值:不止是日志分组group('Login Flow', ...)看似只是加个标签,但它在 K6 内部会创建独立的指标命名空间。执行后,你会在输出中看到:
http_req_duration{group="::Login Flow"}... http_req_failed{group="::Login Flow"}...这意味着:当你把多个业务流程(如Login Flow、Search Flow、Checkout Flow)分别用group包裹,就能在 Grafana 里用group标签做维度切片,精准定位是哪个环节拖慢了整体 P95。这是 JMeter 的“Simple Data Writer”永远做不到的——它只会输出一行 CSV,所有请求混在一起。
自定义指标Rate:为什么不用内置checks?
K6 内置的checks是布尔型断言,只告诉你“对/错”,但不参与聚合计算。而Rate是一个可累加的浮点指标,loginSuccessRate.add(1)表示本次成功,add(0)表示失败,最终 K6 会自动计算sum(value) / count得到成功率。更重要的是,Rate可以被thresholds直接引用(如'login_success_rate': ['rate>0.995']),一旦跌破阈值,K6 会主动退出并返回非零状态码,方便 CI/CD 流水线自动拦截发布。
stages配置:模拟真实流量曲线stages不是简单的“从 0 加到 N”,而是复刻生产环境的真实负载模式。比如电商大促,流量不是瞬间拉满,而是先有小波峰(用户提前进入页面),再陡升(开抢时刻),最后回落(库存售罄)。stages数组的每个对象,就是一个流量拐点。K6 会严格按此节奏调整 VU 数量,比--vus 100 --duration 5m这种恒定模式更能暴露系统在弹性伸缩时的缺陷(比如 Kubernetes HPA 响应延迟、数据库连接池扩容不足)。
3.2 实操避坑:那些文档不会告诉你的“第一次失败”
我带过的 27 个团队,有 23 个在跑通第一个脚本前至少遇到以下一个问题:
问题一:ReferenceError: __ENV is not defined
原因:你用了__ENV.K6_VUS,但 K6 版本低于 v0.42.0。__ENV是 v0.42.0 引入的全局对象,用于访问 CLI 传入的环境变量(如k6 run --env ENV=prod script.js)。解决方案:升级 K6,或改用__ENV.K6_INSTANCE_ID(兼容性更好)。
问题二:TypeError: Cannot read property 'json' of undefined
原因:http.get()返回的res对象,如果网络超时或服务端返回非 JSON 内容(如 HTML 错误页),res.json()会抛异常。文档没强调,但你必须用try/catch包裹:
try { const data = res.json(); check(data, { 'has user id': (d) => d.user_id !== undefined }); } catch (e) { console.error('Failed to parse JSON:', e.message); check(res, { 'response is not empty': (r) => r.body.length > 0 }); }问题三:ERANGE: Invalid argumentonsleep()
原因:sleep(0.5)是合法的(单位是秒),但sleep(-1)或sleep(NaN)会直接崩溃。更隐蔽的是:Math.random() * 2 + 1生成的是浮点数,而某些旧版 K6 对浮点sleep支持不稳定。解决方案:统一转整数sleep(Math.floor(Math.random() * 2 + 1)),或用sleep(1)固定值先跑通。
这些坑,不是因为你代码写错了,而是 K6 把“开发者友好”定义为“暴露底层事实”,而不是“封装所有异常”。接受这一点,你就真正跨过了入门门槛。
4. 运行与诊断:从终端输出读懂系统瓶颈
k6 run script.js执行后,终端滚动的不是一堆无意义的数字,而是一份实时生成的“系统健康快照”。我把它分成三层来读:表层指标(What)、中层分布(Where)、深层归因(Why)。下面用一次真实压测的输出为例,带你逐行解码。
4.1 表层指标:一眼锁定 SLA 是否达标
执行k6 run --vus 50 --duration 2m login.js后,最终输出类似:
/\ |‾‾| /‾‾/ /‾‾/ /\ / \ | |/ / / / / \/ \ | ( / ‾‾\ / \ | |\ \ | (‾) | / __________ \ |__| \__\ \_____/ .io execution: local script: login.js output: - scenarios: (100.00%) 1 scenario, 50 max VUs, 2m0s max duration (incl. graceful stop): * default: 50 looping VUs for 2m0s (gracefulStatus: 30s) running (2m0.0s), 00/50 VUs, 12345 iterations completed (100.00%) active VUs: 50, 12345 completed and 0 interrupted iterations data_received........: 4.2 MB 35 kB/s data_sent............: 1.8 MB 15 kB/s http_req_blocked.....: avg=1.2ms min=0s med=0.8ms max=120ms p(90)=3.4ms p(95)=5.1ms http_req_connecting..: avg=0.4ms min=0s med=0.3ms max=45ms p(90)=1.1ms p(95)=1.8ms http_req_duration....: avg=124ms min=15ms med=118ms max=890ms p(90)=210ms p(95)=280ms http_req_failed......: 0.00% ✓ 0 ✗ 12345 http_req_receiving...: avg=0.8ms min=0s med=0.6ms max=12ms p(90)=1.5ms p(95)=2.1ms http_req_sending.....: avg=0.3ms min=0s med=0.2ms max=8ms p(90)=0.7ms p(95)=0.9ms http_req_tls_handshaking: avg=0.6ms min=0s med=0.4ms max=22ms p(90)=1.3ms p(95)=1.9ms http_req_waiting.....: avg=123ms min=14ms med=117ms max=888ms p(90)=208ms p(95)=278ms http_reqs............: 12345 102.861717/s iteration_duration...: avg=3.2s min=1.1s med=2.9s max=6.8s p(90)=4.5s p(95)=5.1s iterations...........: 12345 102.861717/s login_success_rate...: 100.00% ✓ 12345 ✗ 0 vus..................: 50 min=50 max=50 vus_max..............: 50 min=50 max=50 running (2m0.0s), 00/50 VUs, 12345 iterations completed (100.00%) active VUs: 0, 12345 completed and 0 interrupted iterations checks...............: 100.00% ✓ 24690 ✗ 0 http_req_duration....: avg=124ms min=15ms med=118ms max=890ms p(90)=210ms p(95)=280ms http_req_failed......: 0.00% ✓ 0 ✗ 12345 login_success_rate...: 100.00% ✓ 12345 ✗ 0重点看三行:
http_req_duration....: ... p(95)=280ms→P95 延迟 280ms,低于我们设定的p(95)<500阈值,达标。http_req_failed......: 0.00%→错误率为 0,远优于rate<0.001要求。login_success_rate...: 100.00%→自定义成功率 100%,说明所有业务逻辑检查都通过。
如果这三项任意一项不达标,K6 会在最后输出ERRO[0000] Threshold "http_req_duration" failed并返回状态码 1,CI 流水线可据此自动失败。
4.2 中层分布:用百分位数定位长尾问题
p(90)=210ms和p(95)=280ms看似接近,但max=890ms暴露了严重问题:有 5% 的请求耗时超过 280ms,其中最慢的达到 890ms——是平均值(124ms)的 7 倍。这说明系统存在长尾延迟,不能只看平均值。
此时要结合http_req_waiting(服务端处理时间)和http_req_blocked(客户端等待连接时间)对比:
http_req_waiting....: p(95)=278ms(几乎等于http_req_duration.p(95))http_req_blocked....: p(95)=5.1ms(可忽略)
结论:长尾不是网络或客户端问题,而是服务端处理能力不足。可能是数据库慢查询、缓存穿透、或 GC 暂停。下一步该去看应用日志里的slow_query或GC pause关键字。
4.3 深层归因:从iteration_duration反推用户体感
iteration_duration是每个 VU 完成一次完整default函数所用时间。这里p(95)=5.1s,意味着 95% 的 VU 每次登录流程(含sleep)耗时不超过 5.1 秒。但注意:sleep是 1~3 秒的随机值,所以真实服务端处理时间(http_req_waiting)叠加后,用户感知的“从点击登录到跳转成功”总时长,就是iteration_duration。
如果iteration_duration.p(95)接近你设定的sleep上限(比如sleep(3)时p(95)=3.2s),说明服务端处理很快,瓶颈在用户思考时间;但如果p(95)=5.1s,而sleep只占 3 秒,那剩下的 2.1 秒就是服务端拖慢了用户体验——这时你应该优化userInfoRes或logRes的响应速度,而不是增加sleep。
注意:
iteration_duration不是 K6 内置指标,必须在脚本中手动埋点:export default function (data) { const start = Date.now(); // ... your logic ... const end = Date.now(); iterationDuration.add(end - start); } const iterationDuration = new Trend('iteration_duration');
4.4 进阶诊断:当终端输出不够用时
K6 默认输出是采样汇总,丢失了单请求详情。要深挖某次失败请求,必须开启详细日志:
k6 run --vus 10 --duration 30s --out json=login.json login.js这会生成login.json,里面是每毫秒的原始指标流。你可以用jq提取失败请求:
jq '.metrics.http_req_failed.values.count | select(. > 0)' login.json或者用k6 cloud(需注册)上传到云端仪表盘,获得火焰图、依赖拓扑、错误堆栈等企业级诊断能力。
但绝大多数情况下,终端输出的百分位数 +http_req_waiting分布,已经足够定位 80% 的性能瓶颈。剩下的 20%,需要你回到代码里,用console.log()打印关键路径耗时,或接入 OpenTelemetry 做全链路追踪——那是另一个故事了。
5. 从脚本到工程:如何让 K6 成为团队的性能基础设施
写完第一个脚本能跑通,只是万里长征第一步。真正的挑战在于:如何让 K6 脚本不再是个人玩具,而是团队可协作、可审计、可自动化的性能资产?我在三个不同规模的团队落地 K6 时,总结出四条铁律,每一条都来自血泪教训。
5.1 脚本即代码:必须纳入 Git 版本库,且遵循分支策略
K6 脚本不是配置文件,而是可执行的性能契约。它应该和业务代码一样,走完整的 Git Flow:
main分支:存放已上线、经生产验证的稳定脚本,CI 流水线自动触发每日基线压测。develop分支:集成测试环境使用的脚本,随业务迭代同步更新。feature/*分支:新功能压测脚本开发分支,PR 时必须附带k6 run --dry-run验证(--dry-run会检查语法、依赖、但不发请求)。
我们曾因脚本未进 Git,导致线上故障复盘时无法还原当时的压测参数,最后靠翻 Slack 记录拼凑出--vus 200这个关键数字——这种低效必须杜绝。现在,每个脚本目录下都有README.md,明确标注:
- 适用环境(
staging/production) - 对应的业务 SLA(如 “P95 < 300ms for /api/v1/orders”)
- 最后一次基线值(
2024-05-20: p95=245ms) - 依赖的密钥管理方式(如 “token 从 HashiCorp Vault 动态获取”)
5.2 环境隔离:用--env和__ENV实现一套脚本,多套环境
绝不允许在脚本里硬编码 URL:
// ❌ 错误:无法复用 const res = http.get('https://staging-api.example.com/user/profile'); // ✅ 正确:通过环境变量注入 const API_BASE_URL = __ENV.API_BASE_URL || 'https://staging-api.example.com'; const res = http.get(`${API_BASE_URL}/user/profile`);然后通过 CLI 切换环境:
# 测试环境 k6 run --env API_BASE_URL=https://staging-api.example.com login.js # 生产环境(需额外权限) k6 run --env API_BASE_URL=https://api.example.com --env TOKEN=prod_token login.js更进一步,用setup()动态获取环境专属配置:
export function setup() { const env = __ENV.ENV || 'staging'; const configUrl = `https://config.example.com/${env}/k6.json`; const res = http.get(configUrl); return res.json(); // 返回 { api_url: "...", timeout: 5000 } }5.3 数据驱动:用 CSV/JSON 文件实现大规模参数化
http.get('https://api.example.com/user/123')只能测单个用户。真实压测需要 10 万不同用户 ID。K6 原生支持open()读取外部文件:
// users.csv 内容: // user_id,password // user_001,pass1 // user_002,pass2 // ... const userData = open('./users.csv'); const rows = csvParse(userData); // 需 import { csvParse } from 'k6/data' export default function () { const user = rows[__VU % rows.length]; // 轮询取用户 const res = http.post('https://api.example.com/auth/login', JSON.stringify({ username: user.user_id, password: user.password })); }注意:open()读取的是 init context,所有 VU 共享同一份数据,内存占用极小。我们用 100 万行 CSV(200MB)压测,K6 进程内存稳定在 45MB,而 JMeter 同样数据量直接 OOM。
5.4 自动化闭环:CI/CD 中嵌入性能门禁
在 GitHub Actions 中加入性能守门员:
- name: Run K6 Performance Test run: | k6 run \ --vus 100 \ --duration 1m \ --out json=k6-report.json \ --thresholds 'http_req_duration:p(95)<300' \ login.js # 如果阈值失败,k6 返回 1,step 自动失败 - name: Upload K6 Report uses: actions/upload-artifact@v3 with: name: k6-report path: k6-report.json更进一步,用k6 cloud生成可视化报告链接,自动评论到 PR:
k6 cloud --out cloud=login-pr-${{ github.event.number }} login.js这样,每个新功能合并前,都必须通过性能基线验证。我们上线后性能事故下降了 76%,因为 92% 的慢查询、缓存失效、N+1 问题,都在 PR 阶段被 K6 脚本捕获。
K6 的终极价值,不在于它多快或多炫,而在于它把性能测试从“项目后期救火”,
