【ハンズオン】k6で始める負荷テスト(第2回/全5回)

梅木 和弥
メインビジュアル

はじめに

こんにちは、アプリケーションエンジニアの梅木です。

前回は、「10分で動かせるAPIテスト」として、k6の最小限のチュートリアルを行いました。

手元で k6 run が動くと、「とりあえず VU(ユーザー数)を1000にして回してみよう!」とやりたくなりませんか?
ただ、闇雲に負荷をかけても得られる実績はそう多くありません。

大事なのは、「何を知りたくて負荷をかけるのか」を先に決めることです。
知りたいことが違えば、かけるべき負荷の波形も変わります。

今回は、目的別に使い分ける「負荷テストの型」を解説します。


 

負荷テストの「型」

負荷テストには目的に応じた型があります。
代表的なパターンは以下の6つです。

テスト種類 目的
Smoke Test 基本動作確認
スクリプトが合ってるか、サーバーが死んでないかの確認
Load Test 通常運転確認
平常時のアクセス数で、パフォーマンス目標を達成できるか
Stress Test 限界値の把握
負荷を上げ続けて、システムのボトルネックを探る
Spike Test 急激な負荷変動の挙動確認
Soak Test 長時間稼働の安定性確認
メモリリークなどがないか確認する
Breakpoint Test 破綻点の特定

今回は、Load TestStress TestSpike Test の3つを順番に動かしてみたいと思います。

この3つは知りたいことが段階的に深くなっていく関係にあるため、開発サイクルの中で特によく使われます。

  1. Load Test → 「通常稼働が大丈夫なのか」を確認
  2. Stress Test → 通常稼働が大丈夫だとわかった上で、「じゃあどこまでいける?」を探る
  3. Spike Test → 限界がわかった上で、「急に負荷が来たらどうなる?」を試す

VUの考え方

本連載でも既に何回か出てきていますが、「10VU」「100VU」という単語が負荷テストでは当たり前のように登場します。
今一度、おさらいしましょう。

VU: 同時にリクエストを送るユーザーの数を指します

注意いただきたいのが、「100VU = 秒間100リクエスト」ではありません。
各VUは「リクエスト → レスポンス待ち → sleep(1) → 次のリクエスト」というように実装シナリオで定義した決まったサイクルを繰り返す、といったものです。

例えば。

「リクエスト→レスポンス」が約100msで返ってくるシステムの場合ですと、

  • 1VUは 約1.1秒に1リクエスト
  • つまり100VUなら 1秒間に約90〜100リクエスト程度

この感覚を持っておくと、以降の設定値が理解しやすくなります。

1. Load Test:通常のアクセス量を確認する

Load Testは「今のシステム構成で、想定されるアクセス数をさばけるか?」を確認したいときに使います。
スプリントごとや、大きな機能追加の後に実施するのが一般的です。

負荷のかけ方

いきなりピーク負荷をかけるのではなく、段階的に上げて、維持して、下げるという台形の形にします。
これにより、「何VUまでは安定していたか」が明確になります。

export const options = {
  stages: [
    { duration: '2m', target: 10 },  // 2分かけて10VUに上げる
    { duration: '5m', target: 10 },  // 5分間キープ
    { duration: '2m', target: 20 },  // さらに20VUに上げる
    { duration: '5m', target: 20 },  // 5分間キープ
    { duration: '2m', target: 0 },   // 下げる
  ],
};

上記の「10VU」「20VU」という数字は、あくまで例です。
自社の平常時・ピーク時の同時接続数を見積もって設定してください。

今回は合計16分間で段階的に、10VU→20VUに負荷を上げるような構成で実施します。

閾値の設定

Load Testの閾値は、「プロダクトのパフォーマンス要件そのもの」を指します。
大抵のシステム開発では、非機能要件で「95%のリクエストが500ms以内」「エラー率5%未満」と定まっていることが一般的です。

その定義に沿ったシナリオを作成するよう、ラインを設定します。

thresholds: {
  http_req_failed: ['rate<0.05'],              // エラー率5%未満
  http_req_duration: ['p(95)<500', 'p(99)<1000'], // p95は500ms、p99は1秒以内
  checks: ['rate>0.95'],                       // チェック成功率95%以上
},

シナリオの書き方

テスト関数には、ユーザーの典型的な行動パターンを書きます。
「一覧を見る → 詳細を見る → たまに投稿する」のような流れです。

以下は、リポジトリの scenarios/02-load-patterns/02-load-test.js のシナリオを抜粋したものです。

export default function () {
  // 一覧を見る(全ユーザーがやる操作)
  http.get(`${BASE_URL}/api/users`);
  sleep(1);

  // 詳細を見る
  http.get(`${BASE_URL}/api/users/${Math.floor(Math.random() * 10) + 1}`);
  sleep(2);

  // 新規作成(10%の確率 = 実際の利用比率に近づける)
  if (Math.random() < 0.1) {
    http.post(`${BASE_URL}/api/users`, payload, params);
    sleep(1);
  }
}

sleep() の値や操作の比率は、実際のユーザー行動に近づけるほどテストの精度が上がります。

実行

k6 run scenarios/02-load-patterns/02-load-test.js

分析

実行後のレポートです。

  █ THRESHOLDS

    checks
    ✓ 'rate>0.95' rate=100.00%

    http_req_duration
    ✗ 'p(95)<500' p(95)=1.7s
    ✗ 'p(99)<1000' p(99)=1.93s

    http_req_failed
    ✓ 'rate<0.05' rate=0.00%


  █ TOTAL RESULTS

    checks_total.......: 7643    7.937091/s
    checks_succeeded...: 100.00% 7643 out of 7643
    checks_failed......: 0.00%   0 out of 7643

    ✓ ユーザー一覧取得成功
    ✓ ユーザー詳細取得成功
    ✓ ランダム遅延エンドポイント成功
    ✓ ユーザー作成成功

    HTTP
    http_req_duration..............: avg=331.02ms min=262.11µs med=2.8ms max=2s p(90)=1.37s p(95)=1.7s
      { expected_response:true }...: avg=331.02ms min=262.11µs med=2.8ms max=2s p(90)=1.37s p(95)=1.7s
    http_req_failed................: 0.00%  0 out of 7643
    http_reqs......................: 7643   7.937091/s

    EXECUTION
    iteration_duration.............: avg=5.13s    min=4.1s     med=5.13s max=7s p(90)=5.91s p(95)=6.04s
    iterations.....................: 2461   2.555696/s
    vus............................: 1      min=1         max=20
    vus_max........................: 20     min=20        max=20

    NETWORK
    data_received..................: 3.1 MB 3.2 kB/s
    data_sent......................: 662 kB 688 B/s

閾値をすべてクリアすれば、「想定アクセス数までは問題なくさばける」と言えます。
もしクリアできなかった場合は、どの段階で劣化したかを見ましょう。
「10VUでは問題ないが20VUで遅くなる」なら、ボトルネックは10〜20VUの間にあると分析することが出来ますね。

2. Stress Test:限界値の把握

Load Testが通った後、「じゃあ限界はどこなのか?」を知りたいときに使います。
リリース前やインフラ構成の変更時に実施して、キャパシティの見通しを立てるのが目的です。

Load Testとの違い

大きな違いは2つです。

  • 負荷を高く設定する。
    LoadTestで指定した想定ピークをさらに引き上げて、システムが悲鳴をあげる所まで段階的に負荷を設定します。
  • 閾値を緩める。
    高負荷状態では多少のエラーや遅延が起きるのは当然、と許容します。
    「壊れないか」「回復できるか」が検証の目的ですので閾値を厳しくしすぎるとStressTest本来の目的がブレます。

負荷設定

Load Testと同じ台形ですが、段を3つに増やして最終的に100VUまで引き上げます。

高負荷から回復できるかが重要な確認事項ですので、最後にランプダウン(負荷を下げる)区間を長めに取るのがポイントです。

export const options = {
  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 },    // ランプダウン(回復を確認)
  ],
  thresholds: {
    http_req_failed: ['rate<0.1'],      // Load Testの5%→10%に緩和
    http_req_duration: ['p(95)<2000'],  // 500ms→2秒に緩和
  },
};

シナリオの書き方

テスト関数では、checkに 503(サービス過負荷)も許容ステータスとして加えます。

以下は、リポジトリの scenarios/02-load-patterns/03-stress-test.js のシナリオを抜粋したものです。

export default function () {
  // シンプルなリクエストパターン
  
  const response = http.get(`${BASE_URL}/api/users`);
  
  check(response, {
    'ステータスは200または503': (r) => r.status === 200 || r.status === 503,
    'レスポンスタイムは5秒以内': (r) => r.timings.duration < 5000,
  });
  
  sleep(1);
}

実行

※ 本番環境では実行しないこと

k6 run scenarios/02-load-patterns/03-stress-test.js
# # BASE_URL=http://{your-api}.com k6 run scenarios/02-load-patterns/03-stress-test.js

分析

以下に注目するとよいレポートがかけると思います。

  • どの時点でレスポンスタイムが劣化し始めるか
  • どの時点でエラー率が上昇するか
  • 負荷が下がった後、システムが正常に回復するか

3. Spike Test:突発的なアクセス増

イベント告知やSNSでのバズなど、突発的なアクセス急増に対するシステムの反応を確認するテストです。
オートスケーリングの動作確認にも使えます。

他のテストとの違い

Load TestやStress Testは「段階的に上げる」のが基本でしたが、Spike Testでは意図的に急激な変化を作ります
「10秒で200VUに急増」のように、システムが準備する間もなく負荷がかかる状況を再現します。

閾値はさらに緩めます。
急増時にエラーが出ること自体は問題ではなく、「どれくらいの時間で安定するか」「スパイク後に回復するか」が重要だからです。

負荷設定

構成は以下のようなイメージです。
「通常時の想定」から一気に「10秒 で 200VU 状態」に持っていきます。

export const options = {
  stages: [
    { duration: '10s', target: 10 },    // 通常状態
    { duration: '1m', target: 10 },
    { duration: '10s', target: 200 },   // 10秒で一気に200 VUへ!(スパイク)
    { duration: '3m', target: 200 },    // 耐える
    { duration: '10s', target: 10 },    // 急減
  ],
  thresholds: {
    http_req_failed: ['rate<0.15'],     // 失敗率15%未満(スパイク時は許容)
    http_req_duration: ['p(99)<5000'],  // 99%のリクエストが5秒未満
  },
};

シナリオの書き方

テスト関数では、sleep(0.5) と短めに設定して高負荷を演出しています。
また、checkで 429(レートリミット)も許容ステータスに加えています。

以下は、リポジトリのscenarios/02-load-patterns/04-spike-test.jsのシナリオを抜粋したものです。

export default function () {
  const response = http.get(`${BASE_URL}/api/users`);
  check(response, {
    'リクエスト成功': (r) => r.status === 200 || r.status === 429 || r.status === 503,
  });
  sleep(0.5);
}

実行

※ 本番環境では実行しないこと

k6 run scenarios/02-load-patterns/04-spike-test.js
# BASE_URL=http://{your-api}.com k6 run scenarios/02-load-patterns/04-spike-test.js

分析ポイント

  • スパイク発生時のレスポンスタイム
  • エラー率の変化
  • オートスケーリングが発動するか
  • レートリミットやキューイングの動作
  • スパイク後の回復時間

先程も記載しましたが、オートスケーリングの確認などに最適です。
「負荷が増えてからコンテナが増えるまでの間に、どれくらいエラーが出るか」を測定できます。

まとめ

今回は6つの負荷パターンを紹介し、実際に3つの負荷テストのハンズオンを行いました。

アプリエンジニアとして開発サイクルに組み込むなら、まずは以下のフローを意識すると良いかなと。

  1. Smoke Test
    CI/CDで毎回実行。デプロイごとの健全性確認。
  2. Load Test
    スプリントごと、あるいは大きな機能追加時。パフォーマンス要件を守れているか確認。
  3. Stress Test
    リリース前やインフラ構成変更時。システムの限界容量を把握する。

教材scenarios/02-load-patterns/ には、Soak TestやBreakpoint Testも含め、すべてのテンプレートを用意しています。
ぜひご活用下さい。

※ 実施する際は充分確認を行ったうえでご実施ください。

次回はより実践的なシナリオに挑戦します。
「認証が必要なAPI」「ファイルアップロード」など、複雑なシナリオの書き方に踏み込んでいきたいなと。

以上です。お楽しみに!

梅木 和弥/ アプリケーションエンジニア

Webのシステム開発における、設計・実装に携わっています。
業務ドメインを技術に翻訳する工程に注力しております。

最近はトムとジェリーにハマってます。

梅木 和弥 の書いた記事一覧

最新の関連記事

Download 資料ダウンロード

Drupalでの開発・運用、サーバー構築、Webサイト構築全般、制作費用などに関してお気軽にご相談ください。


Contact お問い合わせ

Drupalでの開発・運用、サーバー構築、Webサイト構築全般についてお気軽にご相談ください。専門スタッフによるDrupal無料相談も行なっております。