EXERCISE 60分

ストーリー

高橋アーキテクト
理論は十分だ。実際にk6のテストスクリプトを書いて、結果を分析してもらう

高橋アーキテクトが仕様書を渡した。

高橋アーキテクト
ECサイト”SpeedShop”のAPIに対する負荷テストだ。4つのミッションを順番にクリアしよう

ミッション概要

ミッションテーマ難易度
Mission 1基本的なロードテスト初級
Mission 2段階的負荷テスト中級
Mission 3マルチシナリオテスト中級
Mission 4結果分析レポート上級

Mission 1: 基本的なロードテスト(10分)

商品一覧APIに対する基本的なロードテストを書いてください。

要件

  • エンドポイント: GET /api/products
  • VU数: 10
  • テスト時間: 30秒
  • 成功基準: ステータス200、レスポンスタイム500ms以下
  • 思考時間: 1秒
解答例
import http from 'k6/http';
import { check, sleep } from 'k6';

export const options = {
  vus: 10,
  duration: '30s',
  thresholds: {
    http_req_duration: ['p(95)<500'],
    http_req_failed: ['rate<0.01'],
  },
};

export default function () {
  const res = http.get('http://localhost:3000/api/products');

  check(res, {
    'status is 200': (r) => r.status === 200,
    'response time < 500ms': (r) => r.timings.duration < 500,
    'body contains products': (r) => {
      const body = JSON.parse(r.body);
      return Array.isArray(body.products) && body.products.length > 0;
    },
  });

  sleep(1);
}

Mission 2: 段階的負荷テスト(15分)

ストレステストのパターンで、段階的に負荷を上げていくテストを書いてください。

要件

  • ステージ1: 2分で20VUまで増加
  • ステージ2: 3分で20VUを維持
  • ステージ3: 2分で50VUまで増加
  • ステージ4: 3分で50VUを維持
  • ステージ5: 2分で100VUまで増加
  • ステージ6: 3分で100VUを維持
  • ステージ7: 2分で0VUまで減少
  • 成功基準: p95 < 300ms、エラー率 < 1%
解答例
import http from 'k6/http';
import { check, sleep } from 'k6';

export const options = {
  stages: [
    { duration: '2m', target: 20 },
    { duration: '3m', target: 20 },
    { duration: '2m', target: 50 },
    { duration: '3m', target: 50 },
    { duration: '2m', target: 100 },
    { duration: '3m', target: 100 },
    { duration: '2m', target: 0 },
  ],
  thresholds: {
    http_req_duration: ['p(95)<300'],
    http_req_failed: ['rate<0.01'],
  },
};

const BASE_URL = 'http://localhost:3000';

export default function () {
  // ランダムにエンドポイントを選択(加重あり)
  const rand = Math.random();

  if (rand < 0.5) {
    // 50%: 商品一覧
    const res = http.get(`${BASE_URL}/api/products`);
    check(res, { 'products 200': (r) => r.status === 200 });
  } else if (rand < 0.8) {
    // 30%: 商品詳細
    const id = Math.floor(Math.random() * 1000) + 1;
    const res = http.get(`${BASE_URL}/api/products/${id}`);
    check(res, { 'product detail 200': (r) => r.status === 200 });
  } else {
    // 20%: 検索
    const queries = ['laptop', 'phone', 'tablet', 'headphones', 'camera'];
    const q = queries[Math.floor(Math.random() * queries.length)];
    const res = http.get(`${BASE_URL}/api/search?q=${q}`);
    check(res, { 'search 200': (r) => r.status === 200 });
  }

  sleep(Math.random() * 3 + 1); // 1-4秒の思考時間
}

Mission 3: マルチシナリオテスト(20分)

閲覧ユーザーと購入ユーザーの2つのシナリオを並行実行するテストを書いてください。

要件

  • シナリオA(閲覧): 30VU、一覧 → 詳細 → 一覧を繰り返し
  • シナリオB(購入): 10VU、詳細 → カート追加 → 注文のフロー
  • 各シナリオに適切な思考時間を設定
  • グループ機能でシナリオを分類
解答例
import http from 'k6/http';
import { check, group, sleep } from 'k6';

const BASE_URL = 'http://localhost:3000';

export const options = {
  scenarios: {
    browsing: {
      executor: 'constant-vus',
      vus: 30,
      duration: '5m',
      exec: 'browsingFlow',
    },
    purchasing: {
      executor: 'constant-vus',
      vus: 10,
      duration: '5m',
      exec: 'purchaseFlow',
    },
  },
  thresholds: {
    http_req_duration: ['p(95)<300', 'p(99)<500'],
    http_req_failed: ['rate<0.01'],
    'group_duration{group:::Browse Products}': ['p(95)<400'],
    'group_duration{group:::Purchase Flow}': ['p(95)<600'],
  },
};

export function browsingFlow() {
  group('Browse Products', function () {
    // 商品一覧を閲覧
    const listRes = http.get(`${BASE_URL}/api/products`);
    check(listRes, {
      'list 200': (r) => r.status === 200,
    });
    sleep(2 + Math.random() * 3); // 2-5秒閲覧

    // 商品詳細を閲覧
    const productId = Math.floor(Math.random() * 100) + 1;
    const detailRes = http.get(`${BASE_URL}/api/products/${productId}`);
    check(detailRes, {
      'detail 200': (r) => r.status === 200,
    });
    sleep(3 + Math.random() * 5); // 3-8秒閲覧

    // カテゴリ一覧
    const catRes = http.get(`${BASE_URL}/api/categories`);
    check(catRes, {
      'categories 200': (r) => r.status === 200,
    });
    sleep(1);
  });
}

export function purchaseFlow() {
  group('Purchase Flow', function () {
    const productId = Math.floor(Math.random() * 100) + 1;

    // 商品詳細を閲覧
    const detailRes = http.get(`${BASE_URL}/api/products/${productId}`);
    check(detailRes, { 'detail 200': (r) => r.status === 200 });
    sleep(3);

    // カートに追加
    const cartRes = http.post(
      `${BASE_URL}/api/cart`,
      JSON.stringify({ productId: productId, quantity: 1 }),
      { headers: { 'Content-Type': 'application/json' } }
    );
    check(cartRes, { 'cart 200': (r) => r.status === 200 || r.status === 201 });
    sleep(2);

    // カート確認
    const cartViewRes = http.get(`${BASE_URL}/api/cart`);
    check(cartViewRes, { 'cart view 200': (r) => r.status === 200 });
    sleep(2);

    // 注文確定
    const orderRes = http.post(
      `${BASE_URL}/api/orders`,
      JSON.stringify({ paymentMethod: 'credit_card' }),
      { headers: { 'Content-Type': 'application/json' } }
    );
    check(orderRes, {
      'order created': (r) => r.status === 200 || r.status === 201,
    });
    sleep(1);
  });
}

Mission 4: 結果分析レポート(15分)

以下のテスト結果を分析し、レポートを作成してください。

テスト結果データ

テスト種類: ロードテスト(100VU、10分間)

http_req_duration......: avg=125ms  min=8ms  med=78ms  max=8234ms
                         p(90)=210ms  p(95)=345ms  p(99)=1823ms
http_req_failed........: 0.45%   ✗ 135    ✓ 29865
http_reqs..............: 30000   50.0/s

エンドポイント別:
  GET /api/products     p95=120ms  errors=0.1%
  GET /api/products/:id p95=180ms  errors=0.2%
  GET /api/search       p95=890ms  errors=1.2%
  POST /api/cart        p95=250ms  errors=0.3%
  POST /api/orders      p95=420ms  errors=0.8%

リソース使用率:
  CPU: avg=65%, max=92%
  Memory: avg=70%, max=85%
  DB connections: avg=18/20, max=20/20(飽和)

作成するレポート項目

  1. 全体評価(合格/不合格)
  2. ボトルネックの特定
  3. 改善提案(優先順位付き)
解答例
=== 負荷テスト結果レポート ===

## 1. 全体評価: 不合格

SLO判定:
  - p95 = 345ms → 目標300ms: 未達
  - p99 = 1823ms → 目標500ms: 未達(大幅超過)
  - エラー率 = 0.45% → 目標0.1%: 未達
  - スループット = 50 RPS → 想定より低い

## 2. ボトルネック分析

ボトルネック1: 検索API(最優先)
  - p95 = 890ms(全体を大きく押し上げている)
  - エラー率 = 1.2%(他エンドポイントの6-12倍)
  - 原因推定: 検索クエリの最適化不足、インデックス未設定

ボトルネック2: DBコネクションプール
  - 最大20/20で飽和している
  - これが注文API(p95=420ms)の遅延要因と推定
  - 注文APIのエラー率0.8%もコネクション枯渇が原因の可能性

ボトルネック3: テールレイテンシ
  - p99/p95 = 1823/345 = 5.3倍(危険水準)
  - max=8234msは明らかに異常
  - GC停止やコネクション待ちが原因の可能性

## 3. 改善提案(優先順位順)

1. [緊急] 検索APIの最適化
   - 検索カラムにインデックスを追加
   - 検索結果のキャッシュ導入(TTL: 5分)
   - 期待効果: 検索p95を300ms以下に改善

2. [高] DBコネクションプールの拡張
   - 現在max=20 → max=50に拡張
   - コネクションの再利用設定を確認
   - 期待効果: 注文APIのエラー率と遅延を改善

3. [中] テールレイテンシの調査
   - GCログの確認(Node.jsのヒープサイズ設定確認)
   - スロークエリログの確認
   - 期待効果: p99を500ms以下に改善

改善後に再テストを実施し、SLO達成を確認すること。

達成度チェック

ミッションテーマ完了
Mission 1基本的なロードテスト[ ]
Mission 2段階的負荷テスト[ ]
Mission 3マルチシナリオテスト[ ]
Mission 4結果分析レポート[ ]

チェックリスト

  • k6で基本的なロードテストを書ける
  • 段階的な負荷パターンを設定できる
  • 複数シナリオを並行実行するテストを書ける
  • テスト結果を分析してレポートを作成できる

次のステップへ

お疲れさまでした。負荷テストの実践力が身についたはずです。

次のセクションでは、Step 3の理解度チェックです。


推定所要時間: 60分