こんにちは、SREのぐりもお(@gr1m0h)です。
Topotalでは、SRE as a ServiceというSREを軸にした技術支援サービスを提供しています。
SRE as a Service ではお客様の環境で負荷テストを実施することがありますし、私自身も過去何度も負荷テストを設計・実施してきました。
その経験から感じているのは、負荷テストはしっかり設計しないと意味のないテストになってしまう作業である一方、実施までの期間が限られていて時間が足りない、というギャップです。
その結果、よくある失敗例として「とりあえず負荷テストツールを回してみた」で終わってしまうケースがあります。並列数をなんとなく決めて、平均レスポンスタイムだけ確認して、問題ないと判断する。これだとキャパシティの議論にも、スケール設計にも、障害対策にも繋がりません。
限られた時間の中で意味のある結果を出すためには、実データに基づいた負荷値の決定と、監視ダッシュボードによる多角的な観測の 2 つが必要になります。
この記事では、現行トラフィックの計測からテスト計画の立案、実施、結果分析までの一連の流れを整理します。ツールは k6 を前提にしていますが、考え方そのものは Gatling や Locust でも同じはずです。
全体の流れ
負荷テストの進め方は次の 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 の整理が分かりやすいです。
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 でテンプレ化したものを公開しています。手元でゼロから組むのが面倒な場合に使ってもらえると嬉しいです。
5. テスト設計
ここからが k6 スクリプトの設計の話になります。
テスト対象エンドポイントの選定
サービス特性に応じて以下の観点で選びます。
- トラフィックの多いページ(トップ、一覧、詳細)
- 負荷が高い処理(検索、フォーム送信)
- クリティカルパス(決済、認証)
注意点として、1 イテレーションで複数エンドポイントを叩く場合、iterations/min × エンドポイント数 = 実際の req/min になります。複数エンドポイントを混ぜるなら、rate を「目標 req/s ÷ エンドポイント数」に分割する必要があります。これを忘れると過剰負荷になってしまいます。
k6 スクリプトの組み立て
3 章で計測した現行トラフィック (req/s) と前節で選定したエンドポイントから、k6 の設定値 (executor / VUs / duration) を決定します。
判断に迷うのは主に次の 3 点です。
- どの executor を使うか
- VUs をいくつにするか
- duration をどれだけ取るか
順に決めていきます。
executor を選ぶ
現行トラフィックの req/s を基準にする場合、arrival-rate 系を第一候補にします。vus 系は応答時間に依存して req/s が変動するため、目標スループットを保証できません。
| 目的 | 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 公式が示す見積もり式は次の形で、内容的にはリトルの法則と等価です。
VUs = TargetRPS × AverageResponseTime(秒) × SafetyFactor
TargetRPS: シナリオの目標 req/sAverageResponseTime: 現行トラフィック計測時の平均レスポンスタイム(秒に変換)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 を増やして再実行する運用が安全です。
"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 で動作するサンプルを公開しています。コピーしてファイル冒頭の「書き換えポイント」だけ修正すれば動くようにしてあります。
書き換えポイントは次の通りです。
| 項目 | 何を入れるか |
|---|---|
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 を入れておくと良いです。
k6 MCP Server を使った作成・検証
最近のk6周りで便利になったと感じているのが、MCP server mcp-k6 の存在です。Claude Code から script の検証・試走・公式ドキュメント参照ができるため、書き換えとデバッグの反復が明らかに速くなります。
セットアップは次のいずれかで行います。
# 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 に基づいて設定します。なければ一般的な基準を提案する形でも良いです。
| メトリクス | 基準例 |
|---|---|
| レスポンスタイム | p90 < 500-1000ms |
| エラー率 | < 1% |
閾値は「テストを止める基準」でもあります。Threshold を超えた瞬間に k6 を abortOnFail で落とすかどうかも事前に決めておきます。
6. テスト実施
実施順序は次の通りです。
- 疎通確認 (smoke) でスクリプトと環境の疎通を確認する
- 平常時 (Average) で平常時の安定性を確認する
- 結果を確認し、問題があれば調整してから次へ進む
- ストレス (Stress) でピーク超え時の挙動を確認する
実施中に観察するのは次の点です。
- ダッシュボードをリアルタイムで監視する
- スケールアウト / インの挙動を観察する
- エラーが急増した場合は即座に中断を判断する
「テスト中はとにかく完走させたい」という気持ちになりがちですが、エラーが立ち上がってきたら原因が掴めなくなる前に止めて、状況を整理した方が良いと思います。
7. 結果の整理と報告
最低限まとめる内容は次の通りです。
- テスト条件(シナリオ、負荷値、対象エンドポイント、テスト環境のスペック)
- 結果サマリ(レスポンスタイム p90/p99、エラー率、スループット)
- ダッシュボードのスクリーンショット(テスト前後の比較)
- 発見事項と推奨アクション
よくある推奨アクションは次の通りです。
- オートスケール閾値の調整
- タスク数の最小値・最大値の見直し
- スケジュールドスケーリングの追加(予測可能なパターンの場合)
- リソース配分の変更(前提条件で許可されている場合)
「結果報告書を書くために負荷テストをやる」のではなく、結果から何を変えるかまで含めて初めて負荷テストの価値が出ると考えています。
さいごに
ここまでの内容をまとめると、意味のある負荷テストを行うために必要なこと=次の 3 点になります。
- 実データで決める: 平常時もピークも、現行トラフィックを 1 週間単位で観測してから決めます
- 同じ時間軸で見る: LB / App / DB / Cache のメトリクスを揃え、相関を取れる状態で実施します
arrival-rate系で組む: キャパシティを語るなら req/s を直接固定します。vus系は応答時間に揺さぶられます
k6 はツールとしては素直で、シナリオ設計さえまっとうに組めば結果は信用できます。mcp-k6 が登場したことで、スクリプトの書き換え反復もかなり楽になりました。
「とりあえず負荷をかけてみる」から一歩進めたい方の参考になれば幸いです。