mcpField
K
k6-best-practices

k6-best-practices

github.com/rcampos09测试与质量1182 次安装32 周前更新
代码仓库

关于 k6-best-practices

用于编写和修复 JavaScript 或 TypeScript k6 负载测试脚本的技能,采用五块结构,并涵盖负载模型、阈值、数据处理和常见运行时问题。

主要功能

  1. 01五块式脚本结构
  2. 02HTTP/REST、WebSocket 和 gRPC 指引
  3. 03执行器与负载模型选择
  4. 04阈值和自定义指标
  5. 05OOM 与 open() 错误指引

适用场景

  1. 01编写完整的 k6 负载测试脚本
  2. 02为目标负载选择执行器
  3. 03解决数据加载和 VU 内存问题
  4. 04检查阈值与请求断言

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/rcampos09/performance-testing-skills --skill k6-best-practices

安装后在 Claude Code 中输入/k6-best-practices确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

evals/
evals.json
references/
DESIGN-PATTERNS.md
EXECUTORS.md
PROTOCOLS.md

详细介绍

k6 场景构建器

为使用 JavaScript 或 TypeScript 编写的 k6 负载测试脚本制定一致、可用于生产环境的模式,涵盖 HTTP/REST、WebSocket 和 gRPC 协议。

输出格式

生成或修复脚本时,始终提供以下三项:

  1. 完整、可运行的脚本文件——绝不提供不完整的代码片段。
  2. 准确的运行命令及所需的环境变量。
  3. 一行说明,解释所选执行器及其为何适合该负载目标。

修复任何 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

参考资料

评论

支持 Markdown