关于 k6-best-practices
用于编写和修复 JavaScript 或 TypeScript k6 负载测试脚本的技能,采用五块结构,并涵盖负载模型、阈值、数据处理和常见运行时问题。
主要功能
- 01五块式脚本结构
- 02HTTP/REST、WebSocket 和 gRPC 指引
- 03执行器与负载模型选择
- 04阈值和自定义指标
- 05OOM 与 open() 错误指引
适用场景
- 01编写完整的 k6 负载测试脚本
- 02为目标负载选择执行器
- 03解决数据加载和 VU 内存问题
- 04检查阈值与请求断言
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ rcampos09/ performance-testing-skills --skill k6-best-practices安装后在 Claude Code 中输入/k6-best-practices确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
evals/evals.jsonreferences/DESIGN-PATTERNS.mdEXECUTORS.mdPROTOCOLS.md详细介绍
k6 场景构建器
为使用 JavaScript 或 TypeScript 编写的 k6 负载测试脚本制定一致、可用于生产环境的模式,涵盖 HTTP/REST、WebSocket 和 gRPC 协议。
输出格式
生成或修复脚本时,始终提供以下三项:
- 完整、可运行的脚本文件——绝不提供不完整的代码片段。
- 准确的运行命令及所需的环境变量。
- 一行说明,解释所选执行器及其为何适合该负载目标。
修复任何 OOM 错误或 open() 误用时,必须涵盖两种失败情形——即使用户只提到其中一种:
- 初始化上下文中的普通变量 → OOM(数据会为每个 VU 复制一份)。修复方法:使用
SharedArray。 - 在
default()中调用open()→ 立即发生运行时错误(can't call open() in the VU context)。修复方法:移至初始化上下文。
第 1 步 — 收集上下文
只询问尚不明确的信息:
- 语言: JavaScript(默认)· TypeScript
- 协议: HTTP/REST · WebSocket · gRPC
- 目标: 新建脚本 · 修复现有脚本 · 特定 DSL 问题
- 负载模型: 固定并发用户数(闭合模型)还是固定 RPS 到达率(开放模型)?
仅当用户询问执行器类型、负载模型、场景配置、多场景编排或 startTime/exec/gracefulStop 参数时,才加载 references/EXECUTORS.md。
仅当用户提及 WebSocket 或 gRPC 时,才加载 references/PROTOCOLS.md。 其中包含连接设置、事件处理、流式 DSL 和注意事项。
仅当用户询问文件夹结构、项目架构、在 VU 之间共享数据、TypeScript 设置、扩展到单个文件之外、模块化结构、可复用辅助函数、多步骤用户流程(电商、结账、登录+浏览)或共享通用逻辑的多个场景时,才加载 references/DESIGN-PATTERNS.md。
第 2 步 — 应用 5 块模式
每个 k6 脚本都必须按顺序包含以下五个块。先生成完整骨架,再填入细节。
Block 1 → Options scenarios + thresholds (executor, VUs, duration, SLA gates)
Block 2 → Data SharedArray for parameterized inputs — loaded once, shared across VUs
Block 3 → Setup one-time auth or preparation — runs once before VUs start
Block 4 → Default fn the VU workload: requests, checks, groups, sleep
Block 5 → Teardown cleanup (optional — required for gRPC/WS connection close)
常见错误 — 检查每个脚本
1. 请求之间没有 sleep() — 会产生不真实的负载
没有思考时间时,VU 会以最快速度冲击服务器。务必在逻辑步骤之间添加 sleep()。
// Wrong — zero think time, unrealistic throughput
export default function() {
http.get(`${BASE_URL}/api/products`);
http.get(`${BASE_URL}/api/cart`);
}
// Correct — realistic think time between steps
import { sleep } from 'k6';
export default function() {
http.get(`${BASE_URL}/api/products`);
sleep(Math.random() * 2 + 1); // 1–3s think time
http.get(`${BASE_URL}/api/cart`);
sleep(1);
}
2. 将 check() 用作测试门控 — 它不会让测试失败
check() 会记录通过/失败统计,但绝不会停止测试或使测试失败。这是最常见的 k6 误解。要实际让测试失败,请使用 thresholds。
// Wrong — test always exits 0 even if every request returns 500
check(res, { 'status is 200': (r) => r.status === 200 });
// Correct — thresholds fail the test; check() provides per-request diagnostics
export const options = {
thresholds: {
http_req_failed: ['rate<0.01'], // < 1% errors — fails test if breached
http_req_duration: ['p(95)<500'], // p95 < 500ms — fails test if breached
checks: ['rate>0.99'], // > 99% checks pass
},
};
check(res, { 'status is 200': (r) => r.status === 200 }); // keep for diagnostics
三种失败机制:
check()— 每次迭代的断言,会记录统计数据,但不会停止测试threshold— 聚合 SLA 门控,超出阈值时会使测试失败(退出代码 99)fail(msg)— 立即停止当前迭代,用于无法恢复的错误
3. 将基础 URL 硬编码 — 会导致无法在多个环境中使用
// Wrong
const res = http.get('https://hardcoded-prod.example.com/api/users');
// Correct — parameterize all environment-specific values
const BASE_URL = __ENV.BASE_URL || 'https://staging.example.com';
运行:k6 run --env BASE_URL=https://prod.example.com script.js
4. 将测试数据存储在普通变量中 — 数据会按 VU 数量倍增
在初始化上下文中声明的任何变量都会为每个 VU 复制一份。对于一个 50 MB 的 JSON 文件和 200 个 VU,这需要 10 GB 内存。请使用 SharedArray——数据只加载一次,并以只读方式供所有 VU 共享。
// Wrong — 50 MB × 200 VUs = 10 GB RAM
import { open } from 'k6'; // open() is init-context only
const users = JSON.parse(open('./data/users.json')); // per-VU copy
// Correct — 50 MB total regardless of VU count
import { SharedArray } from 'k6/data';
const users = new SharedArray('users', () => JSON.parse(open('./data/users.json')));
始终一并说明 open() 的两种错误:
- 初始化上下文中的普通变量 → OOM(每个 VU 都会复制一份)。修复方法:使用
SharedArray。 - 在
default()中调用open()→ 运行时错误(can't call open() in the VU context)。修复方法:移至初始化上下文。
5. 未将导入语句放在文件顶部 — 违反惯例且影响可读性
所有 import 语句都必须放在文件的最前几行——位于 export const options、SharedArray 和其他任何代码之前。虽然 ES 模块在技术上无论导入语句位于何处都会将其提升,但将导入语句放在其他位置会使脚本难以阅读,也会破坏对初始化上下文的理解。
// Wrong — imports scattered between blocks
export const options = { ... }; // ❌ options before imports
import { SharedArray } from 'k6/data'; // ❌ import mid-file
const users = new SharedArray(...);
import http from 'k6/http'; // ❌ another import even later
import { check, sleep } from 'k6';
// Correct — all imports first, then options, then data, then functions
import http from 'k6/http';
import { check, group, sleep, fail } from 'k6';
import { SharedArray } from 'k6/data';
export const options = { ... };
const users = new SharedArray('users', () => JSON.parse(open('./data/users.json')));
export default function() { ... }
6. 用 ramping-vus 实现固定 RPS 目标——工作负载模型不正确
ramping-vus 控制并发用户数(封闭模型)。吞吐量会随响应时间变化。要实现固定 RPS 目标,请使用 constant-arrival-rate。
// Wrong for "maintain 100 RPS" — actual RPS drops when server is slow
executor: 'ramping-vus', stages: [{ duration: '5m', target: 100 }]
// Correct — arrival rate is constant regardless of response time
executor: 'constant-arrival-rate',
rate: 100,
timeUnit: '1s',
duration: '5m',
preAllocatedVUs: 50, // rule: ceil(rate × p95_seconds × 1.2)
maxVUs: 200,
7. preAllocatedVUs 配置过小——迭代会静默丢弃
constant-arrival-rate 在 VU 不足时会丢弃迭代。请监控输出中的 dropped_iterations。
preAllocatedVUs = ceil(rate × p95_response_time_in_seconds × 1.2)
Example: 100 RPS, p95 = 400ms → ceil(100 × 0.4 × 1.2) = 48 → use 50
8. gracefulStop 短于 p99 响应时间——错误率虚高
测试结束后,k6 会等待 gracefulStop 指定的时长,让正在执行的迭代完成。如果 p99 为 2s,且 gracefulStop 使用默认值 30s,则没有问题。但如果 p99 为 45s(批处理作业),请显式设置 gracefulStop: '60s'。
scenarios: {
load: {
executor: 'constant-vus',
vus: 100,
duration: '5m',
gracefulStop: '60s', // set to at least 2× p99 response time
}
}
9. 所有 VU 共用一个令牌——所有用户共用同一身份
所有 VU 启动前,setup() 只会运行一次。setup() 返回的令牌会被所有 VU 共用——它们都以同一用户身份进行身份验证,导致服务端行为失真(单一会话,且没有按用户隔离数据)。
// Wrong — 100 VUs, all acting as the same user
export function setup() {
const res = http.post(`${BASE_URL}/auth/login`, JSON.stringify({ username: 'admin', password: 'secret' }), ...);
return { token: res.json('access_token') }; // shared by all VUs
}
// Correct — each VU logs in with its own credentials from SharedArray
import { SharedArray } from 'k6/data';
const credentials = new SharedArray('creds', () => JSON.parse(open('./data/users.json')));
export default function() {
const cred = credentials[(__VU - 1) % credentials.length]; // deterministic per VU
const res = http.post(`${BASE_URL}/auth/login`,
JSON.stringify({ username: cred.username, password: cred.password }),
{ headers: { 'Content-Type': 'application/json' } }
);
const token = res.json('access_token');
// use token for subsequent requests in this iteration
}
setup() 仅用于共享的无状态初始化(例如,通过管理员 API 调用填充测试数据集)。
10. POST 缺少 Content-Type——服务器返回 415
// Wrong — 415 Unsupported Media Type
http.post(`${BASE_URL}/api/users`, JSON.stringify({ name: 'Alice' }));
// Correct
http.post(`${BASE_URL}/api/users`, JSON.stringify({ name: 'Alice' }), {
headers: { 'Content-Type': 'application/json' },
});
五段式模式——参考
第 1 段:选项
export const options = {
scenarios: {
load: {
executor: 'ramping-vus',
startVUs: 0,
stages: [
{ duration: '2m', target: 50 }, // ramp up
{ duration: '5m', target: 50 }, // hold
{ duration: '2m', target: 0 }, // ramp down
],
gracefulRampDown: '30s',
gracefulStop: '30s',
},
},
thresholds: {
http_req_duration: ['p(95)<500', 'p(99)<1000'],
http_req_failed: ['rate<0.01'],
checks: ['rate>0.99'],
},
};
第 2 段:数据(SharedArray)
import { SharedArray } from 'k6/data';
// JSON file
const users = new SharedArray('users', () => JSON.parse(open('./data/users.json')));
// CSV file (manual parse — k6 has no built-in CSV parser)
const csvUsers = new SharedArray('csv-users', () =>
open('./data/users.csv')
.split('\n')
.slice(1) // skip header row
.filter(line => line.trim().length > 0)
.map(line => {
const [username, password] = line.split(',');
return { username: username.trim(), password: password.trim() };
})
);
// Access pattern — deterministic: each VU always uses the same record
const user = users[(__VU - 1) % users.length];
// Access pattern — random: any VU may pick any record
const user = users[Math.floor(Math.random() * users.length)];
第 3 段:Setup
const BASE_URL = __ENV.BASE_URL || 'https://staging.example.com';
export function setup() {
// Use only for stateless, shared initialization — NOT per-VU auth
const res = http.post(`${BASE_URL}/api/seed`, null, {
headers: { Authorization: `Bearer ${__ENV.ADMIN_TOKEN}` },
});
if (res.status !== 200) {
fail(`Seed failed: ${res.status} — aborting test`);
}
return { seedId: res.json('id') };
}
第 4 段:默认函数(VU 工作负载)
import http from 'k6/http';
import { group, check, sleep } from 'k6';
export default function(data) {
const cred = users[(__VU - 1) % users.length];
const token = login(cred); // per-VU auth
group('Browse', () => {
const res = http.get(`${BASE_URL}/api/products`, {
headers: { Authorization: `Bearer ${token}` },
tags: { endpoint: 'products' },
});
check(res, {
'products 200': (r) => r.status === 200,
'products not empty': (r) => r.json('#') > 0,
'products < 500ms': (r) => r.timings.duration < 500,
});
sleep(Math.random() * 2 + 1);
});
group('Checkout', () => {
const res = http.post(`${BASE_URL}/api/orders`,
JSON.stringify({ productId: data.seedId }),
{ headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
tags: { endpoint: 'checkout' } }
);
check(res, {
'order created 201': (r) => r.status === 201,
'order has id': (r) => r.json('id') !== undefined,
});
sleep(1);
});
}
第 5 段:Teardown
export function teardown(data) {
// Clean up seed data
http.del(`${BASE_URL}/api/seed/${data.seedId}`, null, {
headers: { Authorization: `Bearer ${__ENV.ADMIN_TOKEN}` },
});
}
执行器选择指南
封闭模型——控制并发用户数。适用于连接数或会话状态至关重要的场景。
| 执行器 | 使用场景 | 必需参数 |
|---|---|---|
constant-vus | 稳态、固定并发数 | vus、duration |
ramping-vus | 逐步增加/减少负载、标准负载测试 | stages、startVUs |
per-vu-iterations | 每个 VU 精确运行 N 次迭代 | vus、iterations、maxDuration |
shared-iterations | 所有 VU 合计精确运行 N 次迭代 | vus、iterations、maxDuration |
开放模型——控制到达速率。适用于以 RPS 为目标的公共 API。
| 执行器 | 使用场景 | 必需参数 |
|---|---|---|
constant-arrival-rate | 固定 RPS 目标 | rate、timeUnit、preAllocatedVUs、maxVUs、duration |
ramping-arrival-rate | 逐步提高 RPS(压力测试) | stages、timeUnit、preAllocatedVUs、maxVUs |
externally-controlled | 通过 REST API 实时调整 VU 数量 | vus、maxVUs、duration |
默认选择: ramping-vus。当 SLA 以 RPS 表示时,切换到 constant-arrival-rate。
阈值参考
export const options = {
thresholds: {
// Global response time
http_req_duration: ['p(95)<500', 'p(99)<1000'],
// Per-tagged endpoint (tag set on the request)
'http_req_duration{endpoint:checkout}': ['p(99)<2000'],
// Error rate
http_req_failed: ['rate<0.01'],
// Check pass rate
checks: ['rate>0.99'],
// Throughput floor
http_reqs: ['rate>50'],
// Custom metric
'checkout_duration_ms': ['p(95)<300'],
},
};
// Abort test early if a threshold is breached:
export const options = {
thresholds: {
http_req_duration: [
{ threshold: 'p(95)<500', abortOnFail: true, delayAbortEval: '1m' },
// delayAbortEval: wait 1m before evaluating — avoids aborting during ramp-up
],
},
};
自定义指标
import { Counter, Gauge, Trend, Rate } from 'k6/metrics';
const checkoutMs = new Trend('checkout_duration_ms');
const authErrors = new Counter('auth_errors_total');
const cacheHits = new Rate('cache_hit_rate');
export default function(data) {
const start = Date.now();
const body = JSON.stringify({ productId: '123' });
const res = http.post(`${BASE_URL}/api/orders`, body, {
headers: { 'Content-Type': 'application/json',
Authorization: `Bearer ${data.token}` },
tags: { endpoint: 'checkout' },
});
checkoutMs.add(Date.now() - start, { step: 'payment' });
if (res.status === 401) authErrors.add(1);
cacheHits.add(res.headers['X-Cache'] === 'HIT');
}
运行命令
# Basic run
k6 run script.js
# Override VUs and duration from CLI (useful for quick smoke test)
k6 run --vus 2 --duration 30s script.js
# Pass environment variables
k6 run --env BASE_URL=https://staging.example.com \
--env USERNAME=perf_user \
--env PASSWORD=secret \
script.js
# Output to JSON for post-analysis
k6 run --out json=results.json script.js
# Output to InfluxDB + Grafana
k6 run --out influxdb=http://localhost:8086/k6 script.js
# TypeScript — compile first, then run the output
npx esbuild src/load.ts --bundle --outfile=dist/load.js --target=es2015
k6 run dist/load.js
# Grafana Cloud execution
k6 cloud run script.js