実トラフィックから設計する k6 負荷テストの進め方

こんにちは、SREのぐりもお(@gr1m0h)です。

Topotalでは、SRE as a ServiceというSREを軸にした技術支援サービスを提供しています。

topotal.com

SRE as a Service ではお客様の環境で負荷テストを実施することがありますし、私自身も過去何度も負荷テストを設計・実施してきました。

その経験から感じているのは、負荷テストはしっかり設計しないと意味のないテストになってしまう作業である一方、実施までの期間が限られていて時間が足りない、というギャップです。

その結果、よくある失敗例として「とりあえず負荷テストツールを回してみた」で終わってしまうケースがあります。並列数をなんとなく決めて、平均レスポンスタイムだけ確認して、問題ないと判断する。これだとキャパシティの議論にも、スケール設計にも、障害対策にも繋がりません。

限られた時間の中で意味のある結果を出すためには、実データに基づいた負荷値の決定と、監視ダッシュボードによる多角的な観測の 2 つが必要になります。

この記事では、現行トラフィックの計測からテスト計画の立案、実施、結果分析までの一連の流れを整理します。ツールは k6 を前提にしていますが、考え方そのものは Gatling や Locust でも同じはずです。

全体の流れ

負荷テストの進め方は次の 7 ステップです。

  1. 前提条件の確認
  2. システム構成の把握
  3. 現行トラフィックの計測
  4. 監視ダッシュボードの準備
  5. テスト設計
  6. テスト実施
  7. 結果の整理と報告

この順番には意味があります。前提条件を確認しないまま設計に入ると後で手戻りしますし、現行トラフィックを測らないまま負荷値を決めると、過剰負荷か負荷不足のどちらかに振れてしまいます。

1. 前提条件の確認

テスト設計の前に、ステークホルダーと前提条件を明文化しておきます。認識のずれは設計の手戻りに直結します。 最低限おさえておく項目は次の通りです。

  • リソース制約
    • CPU/Memoryは変更する?(インスタンス数で対応する?)
    • インスタンスタイプは固定?
    • コスト上限はどこまで?
  • 目的
    • 安定稼働の確認?
    • 限界値の特定?
    • スケーリング挙動の検証?
  • スケジュール
    • テスト実施の期間と回数は?
  • 環境
    • Production / Staging / Developmentのどこを対象にする?
  • 影響範囲
    • テスト実行による他システムへの影響はある?
    • 連携サービス側へ事前共有が必要?

「とりあえず限界値まで上げてみたい」のような曖昧な目標で始めると、結果が出ても「何がわかったの?」になってしまいます。少なくとも何を検証するか?という目的だけは絶対にずらさない方が良いです。

2. システム構成の把握

構成を理解しないままテストを設計するのは避けます。設計時に考慮すべき要素が抜けると、テスト結果から得られる示唆も限定的になってしまうためです。

リクエストの経路に沿って、各レイヤーのスペックや制約を確認していきます。

確認する項目は次の通りです。

  • リクエストの経路(CDN → LB → App → DB の各レイヤー)
  • 各レイヤーのスペックと制限(ALB のコネクション上限、DB の接続数上限など)
  • オートスケール設定の有無と条件(CPU 閾値、最小・最大タスク数など)
  • キャッシュ層の有無(CDN、Redis、アプリケーションキャッシュ)

構成図がない場合は、ヒアリングや Terraform Configuration、AWS / Google Cloud などの管理コンソールから情報を集めることになります。図が存在しないなら、私はその場で自分で書いてしまうことが多いです。手を動かして書いた方が後の議論も早く進みます。

3. 現行トラフィックの計測

負荷値は必ず実データに基づいて設定します。感覚や推測で決めると、過剰負荷で本来検証したい現象を見落とすか、負荷不足で意味のない結果に終わってしまいます。

メトリクスの取得

CloudWatch などのモニタリング基盤で以下のようなメトリクスを確認します。期間は最低 1 週間分を対象にします。

確認項目 メトリクス例 統計
リクエスト数 ALB RequestCount Sum (1分間隔)
レスポンスタイム ALB TargetResponseTime Average, p90, p99
エラー率 ALB HTTPCode_Target_5XX_Count Sum
リソース使用率 ECS CPUUtilization / MemoryUtilization Average

負荷値の決め方

シナリオごとに基準となる負荷値を決定します。

シナリオ 基準 根拠
疎通確認 (smoke) 1 〜 5 req/s 程度の低 rate スクリプトと環境の疎通確認のみ。本番負荷を意図しない
平常時 (Average) 日中の持続的な帯域(目視で帯の中心) ピークではなく、日常的に持続している負荷
ストレス (Stress) 日次ピーク × 1.5 日次ピークを超えた場合の耐性検証
スパイク (Spike) 観測されたスパイク値の再現 瞬間的な急増への対応検証(必要な場合のみ)

よくある間違いは次の通りです。

  • ピーク値を「平常時」として設定してしまい、過剰な負荷になる
  • 1 週間のうち一瞬のスパイクをピークとして扱う(日次ピークとスパイクは区別する)
  • グラフの期間設定を見落とす(1 分間隔 / 5 分間隔で数値が大きく変わる)

シナリオ分類については Grafana の整理が分かりやすいです。

grafana.com

4. 監視ダッシュボードの準備

テスト実施前にダッシュボードを作成しておきます。テスト中にメトリクスを探し始めるのでは遅く、ボトルネックの兆候を見逃す原因となってしまいます。

構成要素ごとに、最低限以下のメトリクスを並べます。

レイヤー   メトリクス
LB (ALB)   RequestCount, TargetResponseTime(p90), 5XX Count, Active Connection Count
App (ECS) CPUUtilization, MemoryUtilization, Running Task Count, DesiredCount
DB         CPUUtilization, DatabaseConnections, ReadLatency/WriteLatency
Cache     CurrConnections, CacheHitRate, EngineCPUUtilization

ポイントは、すべてのメトリクスを同一の時間軸で並べることです。レイヤー間の相関を見るためであり、これができていないと「App の CPU が跳ねた」のか「DB の遅延が App に波及した」のかを判別できません。

私は AWS 向けに Terraform でテンプレ化したものを公開しています。手元でゼロから組むのが面倒な場合に使ってもらえると嬉しいです。

github.com

5. テスト設計

ここからが k6 スクリプトの設計の話になります。

テスト対象エンドポイントの選定

サービス特性に応じて以下の観点で選びます。

  • トラフィックの多いページ(トップ、一覧、詳細)
  • 負荷が高い処理(検索、フォーム送信)
  • クリティカルパス(決済、認証)

注意点として、1 イテレーションで複数エンドポイントを叩く場合、iterations/min × エンドポイント数 = 実際の req/min になります。複数エンドポイントを混ぜるなら、rate を「目標 req/s ÷ エンドポイント数」に分割する必要があります。これを忘れると過剰負荷になってしまいます。

k6 スクリプトの組み立て

3 章で計測した現行トラフィック (req/s) と前節で選定したエンドポイントから、k6 の設定値 (executor / VUs / duration) を決定します。

判断に迷うのは主に次の 3 点です。

  1. どの executor を使うか
  2. VUs をいくつにするか
  3. duration をどれだけ取るか

順に決めていきます。

executor を選ぶ

現行トラフィックの req/s を基準にする場合、arrival-rate 系を第一候補にしますvus 系は応答時間に依存して req/s が変動するため、目標スループットを保証できません。

grafana.com

目的 executor 主な指定値
一定の req/s を再現する constant-arrival-rate rate, duration, preAllocatedVUs
req/s を段階的に変化させる ramping-arrival-rate stages, preAllocatedVUs
一定 VUs で負荷をかける constant-vus vus, duration
VUs を段階的に変化させる ramping-vus stages
1 VU あたりの実行回数を固定したい per-vu-iterations vus, iterations

シナリオ別の対応は次の通りです。

シナリオ 推奨 executor 理由
疎通確認 (smoke) constant-arrival-rate 低 rate で疎通確認するだけなので最もシンプルなものを使う
平常時 (Average) constant-arrival-rate 一定の req/s を長時間維持して安定性を確認する
ストレス (Stress) ramping-arrival-rate 平常時から目標値まで段階的に上昇させて挙動を観察する
スパイク (Spike) ramping-arrival-rate 短時間で急上昇させて急峻な負荷変動を再現する

VUs を見積もる

arrival-rate 系の executor では preAllocatedVUs(必要に応じてmaxVUs)を指定します。。k6 公式が示す見積もり式は次の形で、内容的にはリトルの法則と等価です。

grafana.com

VUs = TargetRPS × AverageResponseTime(秒) × SafetyFactor
  • TargetRPS: シナリオの目標 req/s
  • AverageResponseTime: 現行トラフィック計測時の平均レスポンスタイム(秒に変換)
  • SafetyFactor: レスポンス遅延時のバッファとして 1.5 〜 2.0

ただしこの式はあくまで目安です。公式も「実際の値は試走しながら調整する」前提を取っているので、最初の見積もりに使い、あとは挙動を見ながら詰めていきます。

例として、目標 100 req/s、平均レスポンスタイム 200ms、SafetyFactor 2.0 のケースを計算してみます。

VUs = 100 × 0.2 × 2.0 = 40
preAllocatedVUs: 40

ここで一点注意しておきたいのが maxVUs の扱いです。k6 公式は maxVUs の使用を「ほとんどの場合は避けるべき」としています。テスト実行中に動的に VU を確保する動作は CPU・メモリのコストがかかり、負荷ジェネレータ自体を不安定化させて結果を歪める可能性があるためです。preAllocatedVUs を最初から十分大きく取り、不足したら preAllocatedVUs を増やして再実行する運用が安全です。

grafana.com

"Though it seems convenient, you should avoid using maxVUs in most cases." "Allocating VUs has CPU and memory costs, and allocating VUs as the test runs can overload the load generator and skew results." "In almost all cases, the best thing to do is to pre-allocate the number of VUs you need beforehand."

VUs が不足すると k6 側で Insufficient VUs の警告が出て、dropped_iterations メトリクスが増え、実 rate が目標を下回ります。これらが観測されたら preAllocatedVUsを増やして再実行します。

duration を決める

オートスケールの反応や持続耐性を観察するために、十分な長さを確保します。

シナリオ duration 目安 根拠
疎通確認 (smoke) 1 〜 2 分 疎通とエラー有無の確認のみ
平常時 (Average) 10 〜 30 分 オートスケールのアラーム評価期間(数分)を複数回跨ぐ
ストレス (Stress) 15 〜 30 分 上昇 + ピーク維持で持続耐性を確認する
スパイク (Spike) 2 〜 5 分 急上昇 + 維持 + 急減衰の合計

duration を 1 〜 2 分に短縮すると、オートスケールが反応する前に終わってしまい、本来見たい挙動が観察できなくなります。

サンプルスクリプト

k6 v2.0.0 で動作するサンプルを公開しています。コピーしてファイル冒頭の「書き換えポイント」だけ修正すれば動くようにしてあります。

github.com

書き換えポイントは次の通りです。

項目 何を入れるか
BASE_URL 対象環境のホスト
ENDPOINTS エンドポイント選定で挙げたエンドポイント。name はメトリクス分離用のタグ
TARGET_RPS.* 「負荷値の決め方」で決めた各シナリオの目標 req/s
AVG_RESPONSE_TIME_SEC 計測した平均レスポンスタイム(秒)
SAFETY_FACTOR VUs 見積もりのバッファ(1.5 〜 2.0)
thresholds 「閾値」で決める SLO 基準

実行方法はシナリオを --env SCENARIO= で切り替えます。

k6 run --env SCENARIO=smoke   load-testing-sample.ts
k6 run --env SCENARIO=average load-testing-sample.ts
k6 run --env SCENARIO=stress  load-testing-sample.ts
k6 run --env SCENARIO=spike   load-testing-sample.ts

TypeScript は k6 v0.57 以降、esbuild により標準でサポートされています(.ts ファイルなら追加設定不要で実行できます)。エディタの補完を効かせるなら npm install --save-dev @types/k6 を入れておくと良いです。

grafana.com

k6 MCP Server を使った作成・検証

最近のk6周りで便利になったと感じているのが、MCP server mcp-k6 の存在です。Claude Code から script の検証・試走・公式ドキュメント参照ができるため、書き換えとデバッグの反復が明らかに速くなります。

grafana.com

セットアップは次のいずれかで行います。

# k6 が入っていれば一発でエージェント設定まで済む
k6 x agent init claude-code

# 手動: k6 を直接 MCP として登録
claude mcp add --scope=user --transport=stdio k6 mcp-k6

# Docker 経由
claude mcp add --scope=user --transport=stdio k6 docker run --rm -i grafana/mcp-k6:latest

提供される主なツールは次の通りです(ツール名や挙動はバージョンで変わる可能性があるので、最新は grafana/mcp-k6 を確認してください)。

ツール 用途
validate_script 1 VU / 1 iteration / 30s timeout で文法・import・export を事前チェック
run_script ローカルで小規模実行(最大 50 VU / 5 分)。本実施前の挙動確認用
list_sections k6 公式ドキュメントの目次をツリー取得(コンテキスト節約)
get_documentation 特定セクションの Markdown 全文を取得

アンチパターン

設計でよく見る失敗パターンを整理しておきます。

アンチパターン なぜダメか 正しい設計
実トラフィック基準のテストで constant-vus を使う サーバーが遅くなるほど自然と req/s が下がり、目標負荷に届かない constant-arrival-rate で req/s を直接固定する
preAllocatedVUs を低めに設定しmaxVUs で逃げる 動的な VU 確保は負荷ジェネレータ自体を不安定化させ、結果を歪める恐れがある 目安式 RPS × avgResp × 1.5 〜 2.0 で見積もり、preAllocatedVUs を最初から十分大きく取る
疎通確認 / 平常時 の duration を数十秒に短縮する オートスケールが反応する前に終わり、スケール挙動が観察できない 平常時以上は 10 分以上を確保し、アラーム評価期間を複数回跨ぐようにする
1 イテレーションで複数エンドポイントを叩くのに rate を合計 req/s にする 実 req/s がエンドポイント数倍になり、過剰負荷で別の現象を見ることになる rate = 目標 req/s ÷ エンドポイント数 にする(サンプルの iterRate() 参照)
iterations 指定で組む(per-vu-iterations / shared-iterations) 総回数しかコントロールできず、req/s が固定されないのでキャパシティを語れない キャパシティ検証は arrival-rate 系を使い、iterations 系は機能テスト用に限定

閾値 (Thresholds)

合意済みの SLO に基づいて設定します。なければ一般的な基準を提案する形でも良いです。

grafana.com

メトリクス 基準例
レスポンスタイム p90 < 500-1000ms
エラー率 < 1%

閾値は「テストを止める基準」でもあります。Threshold を超えた瞬間に k6 を abortOnFail で落とすかどうかも事前に決めておきます。

6. テスト実施

実施順序は次の通りです。

  1. 疎通確認 (smoke) でスクリプトと環境の疎通を確認する
  2. 平常時 (Average) で平常時の安定性を確認する
  3. 結果を確認し、問題があれば調整してから次へ進む
  4. ストレス (Stress) でピーク超え時の挙動を確認する

実施中に観察するのは次の点です。

  • ダッシュボードをリアルタイムで監視する
  • スケールアウト / インの挙動を観察する
  • エラーが急増した場合は即座に中断を判断する

「テスト中はとにかく完走させたい」という気持ちになりがちですが、エラーが立ち上がってきたら原因が掴めなくなる前に止めて、状況を整理した方が良いと思います。

7. 結果の整理と報告

最低限まとめる内容は次の通りです。

  • テスト条件(シナリオ、負荷値、対象エンドポイント、テスト環境のスペック)
  • 結果サマリ(レスポンスタイム p90/p99、エラー率、スループット)
  • ダッシュボードのスクリーンショット(テスト前後の比較)
  • 発見事項と推奨アクション

よくある推奨アクションは次の通りです。

  • オートスケール閾値の調整
  • タスク数の最小値・最大値の見直し
  • スケジュールドスケーリングの追加(予測可能なパターンの場合)
  • リソース配分の変更(前提条件で許可されている場合)

「結果報告書を書くために負荷テストをやる」のではなく、結果から何を変えるかまで含めて初めて負荷テストの価値が出ると考えています。

さいごに

ここまでの内容をまとめると、意味のある負荷テストを行うために必要なこと=次の 3 点になります。

  • 実データで決める: 平常時もピークも、現行トラフィックを 1 週間単位で観測してから決めます
  • 同じ時間軸で見る: LB / App / DB / Cache のメトリクスを揃え、相関を取れる状態で実施します
  • arrival-rate 系で組む: キャパシティを語るなら req/s を直接固定します。vus 系は応答時間に揺さぶられます

k6 はツールとしては素直で、シナリオ設計さえまっとうに組めば結果は信用できます。mcp-k6 が登場したことで、スクリプトの書き換え反復もかなり楽になりました。

「とりあえず負荷をかけてみる」から一歩進めたい方の参考になれば幸いです。