mcpField
K
k6-best-practices

k6-best-practices

github.com/rcampos09テスト・品質1182 回インストール32 週間前に更新
リポジトリ

k6-best-practices について

JavaScript または TypeScript で k6 の負荷テストスクリプトを作成・修正するスキルです。5 ブロック構成に沿って、負荷モデル、しきい値、データ処理、実行時の問題を扱います。

主な機能

  1. 015 ブロックのスクリプト構成
  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 プロトコルを対象に、一貫性のある本番環境対応のパターンを徹底します。

出力形式

スクリプトを作成または修正する場合は、必ず次の3点を提示してください。

  1. 完全で実行可能なスクリプトファイル — 断片的なスニペットは提示しないでください。
  2. 必要な環境変数を含む正確な実行コマンド。
  3. 選択したエグゼキューターと、それが負荷目標に適している理由の1行の説明。

OOM エラーまたは open() の誤用を修正する場合は、ユーザーが一方しか言及していなくても、両方の失敗モードを説明してください。

  • init コンテキスト内の通常の変数 → OOM(VU ごとにデータがコピーされる)。修正方法:SharedArray。
  • default() 内の open() → 実行時に即座にエラー(can't call open() in the VU context)。修正方法:init コンテキストに移動する。

ステップ 1 — コンテキストの確認

不明な点だけを質問してください。

  • 言語: JavaScript(デフォルト) · TypeScript
  • プロトコル: HTTP/REST · WebSocket · gRPC
  • 目的: 新しいスクリプト · 既存のスクリプトの修正 · DSL に関する具体的な質問
  • 負荷モデル: 固定同時ユーザー数(クローズド)または固定到着率(RPS、オープン)?

ユーザーがエグゼキューターの種類、負荷モデル、シナリオ設定、複数シナリオのオーケストレーション、または startTime/exec/gracefulStop パラメーターについて質問した場合に限り、references/EXECUTORS.md を読み込んでください。

ユーザーが WebSocket または gRPC に言及した場合に限り、references/PROTOCOLS.md を読み込んでください。 接続の設定、イベント処理、ストリーミング DSL、注意事項が含まれています。

ユーザーがフォルダー構成、プロジェクトアーキテクチャ、VU 間でのデータ共有、TypeScript の設定、単一ファイルを超える拡張、モジュール構成、再利用可能なヘルパー、複数ステップのユーザーフロー(e コマース、チェックアウト、ログイン+閲覧)、または共通ロジックを共有する複数シナリオについて質問した場合に限り、references/DESIGN-PATTERNS.md を読み込んでください。


ステップ 2 — 5 ブロックパターンを適用する

すべての k6 スクリプトには、次の5つのブロックをこの順序で含める必要があります。まずスケルトン全体を生成し、その後で詳細を埋めてください。

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

3つの失敗メカニズム:

  • 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 ごとに複製される

init コンテキストスコープで宣言された変数は、すべてVU ごとに1回コピーされます。50 MB の JSON ファイルを VU 200 個で使う場合、RAM は 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() のエラーは2種類あります。

  • init コンテキスト内の通常の変数 → OOM(VU ごとのコピー)。修正方法:SharedArray。
  • default() 内の open() → 実行時エラー(can't call open() in the VU context)。修正方法:init コンテキストに移動する。
5. ファイルの先頭にインポートがない — 慣例と可読性に反する

すべての import 文は、ファイルの最初の行に置く必要があります。export const options、SharedArray、その他のコードより前に記述してください。ES モジュールでは、技術的には配置に関係なくインポートが巻き上げられますが、それ以外の場所に置くとスクリプトが読みにくくなり、init コンテキストに関する考え方にも反します。

// 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. 固定 RPS の目標に対する ramping-vus — 不適切な負荷モデル

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 では、VUs が不足するとイテレーションがドロップされます。出力の 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 でトークンを共有 — すべてのユーザーが同じ ID を共有

すべての VU の開始前に setup() が 1 回実行されます。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' },
});

5 ブロックパターン — リファレンス

ブロック 1:Options
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:Data(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:Default Function(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}` },
  });
}

Executor の選択ガイド

クローズドモデル — 同時ユーザー数を制御します。接続数やセッション状態が重要な場合に使用してください。

Executor使用する場面必須パラメーター
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 に使用してください。

Executor使用する場面必須パラメーター
constant-arrival-rate固定 RPS の目標rate、timeUnit、preAllocatedVUs、maxVUs、duration
ramping-arrival-rateRPS の段階的な引き上げ(ストレステスト)stages、timeUnit、preAllocatedVUs、maxVUs
externally-controlledREST API によるリアルタイムの VU 調整vus、maxVUs、duration

デフォルトの選択肢: ramping-vus。SLA が RPS で表される場合は、constant-arrival-rate に切り替えてください。


Thresholds リファレンス

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

参考資料

コメント