k6を使い始めた人、たまに書くけど毎回調べ直してしまう人向けのチートシートをまとめました。
CLIコマンド・options・負荷パターン・メトリクスについて、実務で使う範囲のものを記載しています。
「負荷テスト」を体系的に学びたい方はこちらの連載(全5回)もどうぞ。
「アプリエンジニア目線での負荷テストの考え方」からハンズオン形式で学べるものを用意しています。
基本構造とライフサイクル
import http from 'k6/http';
import { check, sleep } from 'k6';
// 1. init: オプション定義・ファイル読み込み(VUごとに1回)
export const options = { vus: 1, iterations: 10 };
// 2. setup: テスト開始前に1回だけ実行(認証トークン取得など)
export function setup() {
const res = http.post(`${BASE_URL}/api/auth/login`, JSON.stringify({
username: 'testuser', password: 'testpass',
}), { headers: { 'Content-Type': 'application/json' } });
return { token: JSON.parse(res.body).token };
}
// 3. default: 各VUが繰り返し実行するテスト本体
export default function (data) {
const res = http.get(`${BASE_URL}/api/users`, {
headers: { Authorization: `Bearer ${data.token}` },
});
check(res, { 'status 200': (r) => r.status === 200 });
sleep(1);
}
// 4. teardown: テスト終了後に1回だけ実行(データクリーンアップなど)
export function teardown(data) {
http.post(`${BASE_URL}/api/auth/logout`, null, {
headers: { Authorization: `Bearer ${data.token}` },
});
}
// 5. handleSummary: テスト終了後のサマリーデータをフック
export function handleSummary(data) {
return { 'summary.html': htmlReport(data) };
}
| ライフサイクル | 実行タイミング | 用途 |
|---|---|---|
init |
VUごとに1回(テスト開始前) |
options 定義、ファイル読み込み |
setup() |
全体で1回(テスト開始前) | 認証トークン取得、テストデータ準備 |
default() |
VUごとに繰り返し | テスト本体 |
teardown() |
全体で1回(テスト終了後) | クリーンアップ |
handleSummary() |
全体で1回(テスト終了後) | レポート出力 |
注意: setup() の戻り値は JSON シリアライズされて default() と teardown() に渡される。関数やHTTPコネクションは渡せない。
CLIコマンド
# 基本実行
k6 run script.js
# 環境変数を渡す
BASE_URL=https://staging.example.com k6 run script.js
# VU数・時間をCLIで上書き(スクリプトの options より優先)
k6 run --vus 10 --duration 30s script.js
# JSON結果を出力
k6 run --out json=results.json script.js
# CSV結果を出力
k6 run --out csv=results.csv script.js
# 特定タグのメトリクスのみ出力
k6 run --tag testid=run01 script.js
# HTTP デバッグ(リクエスト/レスポンスの詳細を出力)
k6 run --http-debug="full" script.js
# ドライラン(スクリプトの構文チェックのみ)
k6 inspect script.js
# Docker で実行(k6未インストールの環境向け)
docker run --rm -i --network=host \
-e BASE_URL=http://localhost:3000 \
grafana/k6 run - < script.js
options リファレンス
export const options = {
// === 実行制御 ===
vus: 10, // 固定VU数
duration: '1m', // 実行時間
iterations: 100, // 総実行回数(全VU合計)
// === 段階的負荷(stagesを使う場合はvus/durationは不要)===
stages: [
{ duration: '1m', target: 10 },
{ duration: '3m', target: 10 },
{ duration: '1m', target: 0 },
],
// === 閾値 ===
thresholds: {
http_req_failed: ['rate<0.05'],
http_req_duration: ['p(95)<500', 'p(99)<1000'],
checks: ['rate>0.95'],
// カスタムメトリクス
'my_custom_metric': ['avg<200'],
// タグ付きメトリクス
'http_req_duration{endpoint:auth}': ['p(95)<300'],
},
// === シナリオ(複数の負荷パターンを同時実行)===
scenarios: {
browse: {
executor: 'constant-vus',
vus: 10,
duration: '5m',
exec: 'browseProducts', // 実行する関数名
},
purchase: {
executor: 'ramping-vus',
startVUs: 0,
stages: [{ duration: '5m', target: 5 }],
exec: 'purchaseItem',
},
},
// === その他 ===
noConnectionReuse: false, // コネクション再利用(デフォルト: false)
userAgent: 'k6/custom', // User-Agent
insecureSkipTLSVerify: true, // SSL証明書検証スキップ
throw: true, // HTTPエラーで例外を投げる
};
executor 一覧
| executor | 負荷の制御方法 | 用途 |
|---|---|---|
constant-vus |
固定VU数 × 固定時間 | 最もシンプル |
ramping-vus |
VU数を段階的に変化 | Load/Stress/Spike Test |
per-vu-iterations |
各VUが指定回数実行 | データ投入 |
shared-iterations |
全VUで合計N回実行 | 固定回数のバッチ処理 |
constant-arrival-rate |
秒間リクエスト数を固定 | SLA検証(VU数は自動調整) |
ramping-arrival-rate |
秒間リクエスト数を段階的に変化 | Breakpoint Test |
HTTPメソッド
import http from 'k6/http';
const BASE_URL = __ENV.BASE_URL || 'http://localhost:3000';
const params = { headers: { 'Content-Type': 'application/json' } };
// GET
http.get(`${BASE_URL}/api/users`);
// GET(クエリパラメータ付き)
http.get(`${BASE_URL}/api/users?page=1&limit=20`);
// POST
http.post(`${BASE_URL}/api/users`,
JSON.stringify({ name: '太郎', email: 'taro@example.com' }), params);
// PUT
http.put(`${BASE_URL}/api/users/1`,
JSON.stringify({ name: '花子' }), params);
// PATCH
http.patch(`${BASE_URL}/api/users/1`,
JSON.stringify({ email: 'new@example.com' }), params);
// DELETE
http.del(`${BASE_URL}/api/users/1`);
// バッチリクエスト(並列実行)
const responses = http.batch([
['GET', `${BASE_URL}/api/users`],
['GET', `${BASE_URL}/api/products`],
['GET', `${BASE_URL}/api/categories`],
]);
check() パターン集
import { check } from 'k6';
// ステータスコード
check(res, { 'status 200': (r) => r.status === 200 });
// レスポンスタイム
check(res, { '200ms以内': (r) => r.timings.duration < 200 });
// JSONボディ
check(res, {
'success is true': (r) => JSON.parse(r.body).success === true,
'dataが配列': (r) => Array.isArray(JSON.parse(r.body).data),
'データが1件以上': (r) => JSON.parse(r.body).data.length > 0,
});
// レスポンスヘッダー
check(res, {
'Content-Type is JSON': (r) => r.headers['Content-Type'].includes('application/json'),
});
// レスポンスボディのサイズ
check(res, { 'body < 1MB': (r) => r.body.length < 1024 * 1024 });
// Stress/Spike Test用(503やレートリミットも許容)
check(res, {
'リクエスト処理済み': (r) => [200, 429, 503].includes(r.status),
});
負荷パターン
| パターン | 知りたいこと | 負荷の形 | いつやるか |
|---|---|---|---|
| Smoke | そもそも動くか | 1〜2VU、数回 | CI/CDで毎回 |
| Load | 通常アクセスで大丈夫か | 台形(段階的に上下) | スプリントごと |
| Stress | どこまで耐えるか | 階段(段階的に高く) | リリース前 |
| Spike | 急に来たらどうなるか | 針(急増→急減) | スケーリング検証 |
| Soak | 長時間で劣化しないか | 一定負荷を長時間 | 定期的に |
| Breakpoint | 何人で壊れるか | 上げ続ける | キャパシティ計画時 |
stages テンプレート
// === Smoke ===
{ stages: [{ duration: '1m', target: 1 }] }
// === Load ===
{ stages: [
{ duration: '2m', target: 10 }, // ramp-up
{ duration: '5m', target: 10 }, // steady
{ duration: '2m', target: 20 }, // increase
{ duration: '5m', target: 20 }, // steady
{ duration: '2m', target: 0 }, // ramp-down
]}
// === Stress ===
{ stages: [
{ duration: '2m', target: 20 },
{ duration: '5m', target: 20 },
{ duration: '2m', target: 50 },
{ duration: '5m', target: 50 },
{ duration: '2m', target: 100 },
{ duration: '5m', target: 100 },
{ duration: '5m', target: 0 }, // 回復確認
]}
// === Spike ===
{ stages: [
{ duration: '10s', target: 10 },
{ duration: '1m', target: 10 },
{ duration: '10s', target: 200 }, // 🚀 急増
{ duration: '3m', target: 200 },
{ duration: '10s', target: 10 }, // 急減
{ duration: '3m', target: 10 }, // 回復確認
{ duration: '10s', target: 0 },
]}
// === Soak ===
{ stages: [
{ duration: '5m', target: 20 }, // ramp-up
{ duration: '4h', target: 20 }, // 長時間維持
{ duration: '5m', target: 0 },
]}
// === Breakpoint(arrival-rate版)===
{
scenarios: {
breakpoint: {
executor: 'ramping-arrival-rate',
startRate: 10,
timeUnit: '1s',
preAllocatedVUs: 500,
stages: [
{ duration: '10m', target: 500 }, // 10分で秒間500リクエストまで
],
},
},
}
閾値の設計指針
| テストパターン | エラー率 | p(95) | 考え方 |
|---|---|---|---|
| Load Test |
<0.05(5%) |
<500ms |
プロダクトの非機能要件そのもの |
| Stress Test |
<0.1(10%) |
<2000ms |
高負荷なので緩め。回復できるかが重要 |
| Spike Test |
<0.15(15%) |
<5000ms |
急増時のエラーは許容。回復速度を見る |
環境別に閾値を切り替える
const thresholds = {
dev: {
http_req_failed: ['rate<0.1'],
http_req_duration: ['p(95)<1000'],
},
staging: {
http_req_failed: ['rate<0.05'],
http_req_duration: ['p(95)<500'],
},
};
export const options = {
thresholds: thresholds[__ENV.ENVIRONMENT || 'dev'],
};
カスタムメトリクス
import { Counter, Rate, Gauge, Trend } from 'k6/metrics';
const loginAttempts = new Counter('login_attempts'); // 累積カウント
const loginSuccess = new Rate('login_success_rate'); // 成功率(0〜1)
const activeUsers = new Gauge('active_users'); // 現在の値(上下する)
const processingTime = new Trend('processing_time_ms'); // 統計情報(分布)
export default function () {
loginAttempts.add(1);
const start = Date.now();
const res = http.post(`${BASE_URL}/api/auth/login`, payload, params);
processingTime.add(Date.now() - start);
loginSuccess.add(res.status === 200); // true=成功、false=失敗
activeUsers.add(__VU); // 現在のVU番号
}
// 閾値にも使える
export const options = {
thresholds: {
'login_success_rate': ['rate>0.95'],
'processing_time_ms': ['p(95)<300'],
},
};
タグとグループ
タグ:API単位で閾値を分ける
http.get(`${BASE_URL}/api/users`, {
tags: { endpoint: 'users', priority: 'high', operation: 'read' },
});
// 閾値でフィルタ
export const options = {
thresholds: {
'http_req_duration{endpoint:users}': ['p(95)<300'],
'http_req_duration{endpoint:upload}': ['p(95)<2000'],
'http_req_failed{priority:critical}': ['rate<0.001'],
},
};
グループ:ユーザーフロー単位で計測
import { group } from 'k6';
export default function () {
group('会員登録フロー', () => {
group('フォーム表示', () => {
http.get(`${BASE_URL}/api/form`);
sleep(2);
});
group('送信', () => {
http.post(`${BASE_URL}/api/users`, payload, params);
});
});
}
// グループ単位で閾値設定
export const options = {
thresholds: {
'http_req_duration{group:::会員登録フロー}': ['p(95)<1000'],
},
};
メトリクスの読み方
パーセンタイル
http_req_duration ... avg=30ms med=2ms p(90)=5ms p(95)=8ms p(99)=500ms
avgが良くてもp(99)が悪ければ、一部のユーザーに遅い体験が発生している。
実務ではavgではなくp(95)で判断する。
| パーセンタイル | 意味 | よくある用途 |
|---|---|---|
| p(50) | 半分のリクエストがこの時間以内 | 中央値(体感の代表値) |
| p(90) | 90%がこの時間以内 | 一般的なモニタリング |
| p(95) | 95%がこの時間以内 | SLA目標でよく使われる |
| p(99) | 99%がこの時間以内 | 厳しいSLA目標 |
組み込みメトリクス一覧
| メトリクス | 説明 |
|---|---|
http_req_duration |
リクエスト〜レスポンス完了の時間 |
http_req_failed |
失敗率(4xx/5xxの割合) |
http_req_blocked |
TCP接続待ちの時間 |
http_req_connecting |
TCP接続確立の時間 |
http_req_tls_handshaking |
TLSハンドシェイクの時間 |
http_req_sending |
リクエスト送信の時間 |
http_req_waiting |
TTFB(最初のバイトまでの時間) |
http_req_receiving |
レスポンス受信の時間 |
http_reqs |
総リクエスト数 |
iteration_duration |
1イテレーション全体の時間 |
iterations |
完了したイテレーション数 |
vus |
現在のアクティブVU数 |
data_received |
受信データ量 |
data_sent |
送信データ量 |
レポート出力
import { htmlReport } from 'https://raw.githubusercontent.com/benc-uk/k6-reporter/main/dist/bundle.js';
import { textSummary } from 'https://jslib.k6.io/k6-summary/0.0.1/index.js';
export function handleSummary(data) {
return {
'stdout': textSummary(data, { indent: ' ', enableColors: true }),
'summary.json': JSON.stringify(data),
'summary.html': htmlReport(data),
};
}
GitHub Actions連携
name: k6 Load Tests
on:
pull_request:
branches: [main]
jobs:
load-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Run k6
uses: grafana/k6-action@v0.3.0
with:
filename: test.js
env:
ENVIRONMENT: staging
BASE_URL: ${{ secrets.STAGING_API_URL }}
- name: Save report
if: always()
uses: actions/upload-artifact@v3
with:
name: k6-results
path: summary.html
トラブルシューティング
よくあるハマりどころをかるーくまとめました。
setup() の戻り値に関数やコネクションを入れてしまう。
JSONシリアライズされて渡されるため、プリミティブ値・配列・オブジェクトのみ。
sleep() を入れ忘れて非現実的な負荷になる。
ページ閲覧なら1〜3秒、フォーム入力なら5〜10秒が目安。
check() の失敗でテストが止まると思い込む。
check() は検証結果を記録するだけで、テストは続行する。止めたいなら thresholds で checks: ['rate>0.99'] のように設定する。
Stress/Spike Testの閾値をLoad Testと同じにしてしまう。
高負荷テストではエラーや遅延は起きて当然。閾値を厳しくしすぎるとテストの目的(限界値の把握)を見失う。
Docker実行時に --network=host を忘れる。
ホストマシンのlocalhostにアクセスできずにエラーになる。
http.batch() の結果を check() し忘れる。
バッチリクエストは配列で返るので、個別に check() する必要がある。
まとめ
各項目の「なぜそうするのか」「結果をどう分析するか」という考え方の部分は、自社ブログの連載で詳しく書いています。手を動かしながら学びたい方は教材リポジトリと合わせてどうぞ。