7
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【k6】負荷テストチートシート

7
Posted at

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() は検証結果を記録するだけで、テストは続行する。止めたいなら thresholdschecks: ['rate>0.99'] のように設定する。

Stress/Spike Testの閾値をLoad Testと同じにしてしまう。
高負荷テストではエラーや遅延は起きて当然。閾値を厳しくしすぎるとテストの目的(限界値の把握)を見失う。

Docker実行時に --network=host を忘れる。
ホストマシンのlocalhostにアクセスできずにエラーになる。

http.batch() の結果を check() し忘れる。
バッチリクエストは配列で返るので、個別に check() する必要がある。

まとめ

各項目の「なぜそうするのか」「結果をどう分析するか」という考え方の部分は、自社ブログの連載で詳しく書いています。手を動かしながら学びたい方は教材リポジトリと合わせてどうぞ。

7
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?