load-testing-plan について
サービスの負荷テスト計画を作成するスキルです。明確なシナリオと合否基準を必要とするエンジニアリングチーム向けに、目標、閾値、ツール、環境、指標、分析、CI 連携を扱います。
主な機能
- 01基準、ストレス、スパイク、耐久シナリオ
- 02SLO に基づく合否基準
- 03ツールとテスト環境の指針
- 04指標の収集と結果分析
- 05CI のリリースゲート
活用シーン
- 01サービスリリースの性能テスト計画
- 02サービス SLO に沿った閾値設定
- 03定期的なストレス・耐久テストの実施
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ mohitagw15856/ pm-claude-skills --skill load-testing-planインストール後、Claude Code で/load-testing-planを入力して読み込まれたか確認します。
詳細
負荷テスト計画スキル
サービスの包括的な負荷・パフォーマンステスト計画を作成します。テストの目的、シナリオの定義、ツールの設定、合格基準、CI への統合を網羅します。優れた負荷テスト計画では、「許容できるパフォーマンス」とは何かを明確にするため、エンジニアは生の数値を解釈せずにテストを実行し、合否を判断できます。
必要な入力
まだ提供されていない場合は、以下を確認してください。
- サービス名と主要なエンドポイント — テスト対象のエンドポイント(パス、メソッド、一般的なリクエスト/レスポンスの形式)
- 現在のトラフィックのベースライン — 通常負荷時の現在のリクエスト数/秒、p50/p99 レイテンシ、エラー率
- ピークトラフィックの想定 — 想定されるピーク RPS(例:フラッシュセール時のベースラインの 10 倍、または季節的なピーク)
- SLO の目標 — レイテンシ SLO(p99 < X ms)、エラー率 SLO(< Y%)、可用性の目標
- 希望するテストツール — k6、Locust、JMeter、Gatling、または指定なし
- テスト環境の利用可否 — 専用の負荷テスト環境、ステージング環境、または本番環境(トラフィック制御を実施)
出力形式
負荷テスト計画:[Service Name]
作成者: [Name] | チーム: [Team name] 日付: [Date] | レビュー周期: 主要リリース前、および四半期ごと テストツール: [k6 / Locust / JMeter / Gatling] テスト環境: [Environment name and URL]
1. 目的と範囲
テスト対象: [Service name] は、[機能の説明 — 例:「モバイルクライアントおよびウェブクライアントからのユーザー認証リクエスト」] を処理します。この計画では、想定されるトラフィックおよび高負荷時にサービスが SLO を満たすことを検証します。
対象範囲:
- [Endpoint 1: METHOD /path — 説明]
- [Endpoint 2: METHOD /path — 説明]
- [Endpoint 3: METHOD /path — 説明]
対象外:
- [明示的に除外するエンドポイントとその理由 — 例:「管理 API — トラフィックが少ないため、負荷テストから除外」]
- [負荷テストを実施できないサードパーティ連携 — 代わりにモックを使用]
2. パフォーマンス目標(合格基準)
すべてのシナリオに明示的な合否基準を設定します。いずれかの基準に抵触した場合、テスト実行は不合格となります。
| 指標 | ベースラインシナリオ | ストレスシナリオ | スパイクシナリオ | ソークシナリオ |
|---|---|---|---|---|
| p50 レイテンシ | < [X] ms | < [X × 1.5] ms | < [X × 2] ms | < [X] ms |
| p95 レイテンシ | < [Y] ms | < [Y × 1.5] ms | < [Y × 2] ms | < [Y] ms |
| p99 レイテンシ | < [Z] ms | < [Z × 2] ms | < [Z × 3] ms | < [Z] ms |
| エラー率 | < [0.1]% | < [1]% | < [2]% | < [0.1]% |
| スループット | ≥ [N] RPS | ≥ [N × 3] RPS | 該当なし | ≥ [N] RPS |
| 失敗したリクエスト | 0 (5xx) | < [threshold] | < [threshold] | 0 (5xx) |
SLO の参照値: これらの基準はサービスの SLO(p99 < [Z ms]、エラー率 < [0.1]%、可用性 [99.9]%)に基づいています。
3. トラフィックモデル
ベースラインのトラフィック(現在の本番環境):
- 平均 RPS:[N] req/sec
- ピーク RPS(観測値):[N] req/sec
- エンドポイント別のリクエスト分布:
- [Endpoint 1]:[X]% のトラフィック
- [Endpoint 2]:[Y]% のトラフィック
- [Endpoint 3]:[Z]% のトラフィック
シミュレーションするユーザーの行動:
- リクエスト間の思考時間:[X–Y] 秒(ランダム化)
- セッション時間:平均 [N] 分
- 認証済みユーザーと匿名ユーザーの比率:[X]%/[Y]%
- 地理的な分布:[Region 1 X]%、[Region 2 Y]%
4. テストシナリオ
シナリオ 1:ベースライン(定常状態)
目的: 通常の本番負荷でサービスが許容可能なパフォーマンスを発揮することを確認します。 時間: 10 分 負荷プロファイル: 2 分かけて [N] RPS まで段階的に上げ、8 分間維持します。 同時実行数: [N] 仮想ユーザー
合格基準: 上記の目標テーブルの「ベースライン」列にあるすべての基準を満たすこと。
シナリオ 2:ストレステスト
目的: 限界点を見つけます。SLO に違反する前に、サービスがどれだけの負荷を処理できるかを確認します。 所要時間: 20~30 分 負荷プロファイル: 5 分ごとに [N] RPS(ベースライン)から [N × 5] RPS まで段階的に増加させます。各段階を 5 分間維持します。最初に SLO に違反した時点で停止します。 同時実行数: RPS の目標値に応じて増加します
記録する内容:
- p99 レイテンシが初めて SLO を超えたときの RPS
- エラー率が初めて SLO を超えたときの RPS
- 負荷をベースラインに戻したときにサービスが回復するかどうか
シナリオ 3:スパイクテスト
目的: 突然のトラフィック急増(フラッシュセール、バイラルイベント、ボット攻撃)をシミュレートします。 所要時間: 15 分 負荷プロファイル: [N] RPS(ベースライン)を 3 分間維持し、瞬時に [N × 10] RPS まで増加させて 5 分間維持した後、ベースラインに戻して 7 分間維持します。
記録する内容:
- スパイク中および回復中のレイテンシ
- サービスが負荷を適切に抑制できるかどうか(レート制限、キューの深さ)
- スパイク終了後、ベースラインのレイテンシに戻るまでの時間
シナリオ 4:ソークテスト/耐久テスト
目的: メモリリーク、コネクションプールの枯渇、時間経過に伴う緩やかな劣化を検出します。 所要時間: 4~8 時間(夜間に実行) 負荷プロファイル: 全期間にわたり、[N × 1.5] RPS(ベースラインより 50%高い値)を一定に維持します。
監視する項目:
- 時間経過に伴うメモリ使用量の傾向(無制限に増加しないこと)
- エラー率の傾向(上昇し続けず、横ばいであること)
- GC 停止の頻度(JVM/Go サービス)
- データベース接続プールの使用率
- p99 レイテンシの傾向(数時間かけて上昇しないこと)
5. テスト環境の要件
インフラストラクチャ
| コンポーネント | 要件 | 備考 |
|---|---|---|
| テスト対象のサービス | 本番環境から分離 | [N] 個のレプリカ。本番環境と同等のリソース上限 |
| データベース | 本番環境規模のデータを含む別インスタンス | セクション 7 のシードスクリプト |
| キャッシュ(Redis/Memcached) | テスト開始時に空 | コールドスタート条件でテストされることを保証します |
| 負荷生成ツール | テスト対象のサービスとは別に配置 | 最低 [N] vCPU、[N] GB RAM |
| ネットワーク | サービスへの低レイテンシ経路 | 生成ツールを同じホストで実行しないでください |
データの投入
各テストの実行前に、環境が以下の状態であることを確認します。
# Seed test users (needed for authenticated endpoint tests)
[seed command or script path — e.g. python scripts/seed_load_test_users.py --count 10000]
# Seed test data for read endpoints
[seed command — e.g. ./scripts/seed_products.sh --count 50000]
# Verify seed completed
[verification command — e.g. psql $DB_URL -c "SELECT COUNT(*) FROM users WHERE load_test=true"]
テストデータのルール:
- 負荷テストで本番環境の実ユーザーデータを使用しないでください
- テストで生成したすべてのレコードに、簡単にクリーンアップできるよう
load_test=trueタグを付けてください - 各テスト後にクリーンアップを実行してください:[cleanup command]
6. ツールのセットアップ
k6 スクリプトのひな形
import http from 'k6/http';
import { check, sleep } from 'k6';
import { Rate, Trend } from 'k6/metrics';
// Custom metrics
const errorRate = new Rate('error_rate');
const endpointLatency = new Trend('endpoint_latency', true);
// Test configuration — override per scenario
export const options = {
scenarios: {
baseline: {
executor: 'ramping-vus',
startVUs: 0,
stages: [
{ duration: '2m', target: [BASELINE_VUS] },
{ duration: '8m', target: [BASELINE_VUS] },
{ duration: '1m', target: 0 },
],
},
},
thresholds: {
http_req_duration: [
'p(95)<[Y_MS]',
'p(99)<[Z_MS]',
],
error_rate: ['rate<0.01'],
http_req_failed: ['rate<0.01'],
},
};
// Auth helper — get token once per VU
export function setup() {
const loginRes = http.post('[BASE_URL]/auth/login', JSON.stringify({
username: `load_test_user_${Math.floor(Math.random() * 10000)}@example.com`,
password: '[LOAD_TEST_PASSWORD]',
}), { headers: { 'Content-Type': 'application/json' } });
check(loginRes, { 'login ok': (r) => r.status === 200 });
return { token: loginRes.json('access_token') };
}
export default function (data) {
const headers = {
Authorization: `Bearer ${data.token}`,
'Content-Type': 'application/json',
};
// Endpoint 1: [Description]
const res1 = http.get('[BASE_URL]/[endpoint-1]', { headers });
check(res1, {
'[endpoint-1] status 200': (r) => r.status === 200,
'[endpoint-1] latency < [X]ms': (r) => r.timings.duration < [X],
});
errorRate.add(res1.status >= 400);
endpointLatency.add(res1.timings.duration, { endpoint: '[endpoint-1]' });
sleep(Math.random() * [THINK_TIME_MAX] + [THINK_TIME_MIN]);
// Endpoint 2: [Description]
const res2 = http.post('[BASE_URL]/[endpoint-2]',
JSON.stringify({ [key]: '[value]' }),
{ headers }
);
check(res2, {
'[endpoint-2] status 201': (r) => r.status === 201,
});
errorRate.add(res2.status >= 400);
}
Locust スクリプトのひな形(代替)
from locust import HttpUser, task, between
import random
class [ServiceName]User(HttpUser):
wait_time = between([THINK_TIME_MIN], [THINK_TIME_MAX])
token = None
def on_start(self):
"""Called once per simulated user — authenticate."""
user_id = random.randint(1, 10000)
response = self.client.post("/auth/login", json={
"username": f"load_test_user_{user_id}@example.com",
"password": "[LOAD_TEST_PASSWORD]",
})
self.token = response.json()["access_token"]
self.headers = {"Authorization": f"Bearer {self.token}"}
@task([WEIGHT_1]) # Weight = relative frequency
def [endpoint_1_task](self):
"""[Endpoint 1 description]"""
with self.client.get(
"/[endpoint-1]",
headers=self.headers,
catch_response=True
) as response:
if response.elapsed.total_seconds() > [LATENCY_THRESHOLD]:
response.failure(f"Too slow: {response.elapsed.total_seconds()}s")
@task([WEIGHT_2])
def [endpoint_2_task](self):
"""[Endpoint 2 description]"""
self.client.post(
"/[endpoint-2]",
json={"[key]": "[value]"},
headers=self.headers,
)
テストの実行
# k6 — run baseline scenario
k6 run --env BASE_URL=https://[test-env-url] scripts/load_test.js
# k6 — run stress scenario with output to InfluxDB
k6 run --out influxdb=http://[influxdb-host]:8086/k6 \
--env SCENARIO=stress \
scripts/load_test.js
# Locust — headless run
locust -f locustfile.py \
--headless \
--users [N] \
--spawn-rate [N] \
--run-time 10m \
--host https://[test-env-url] \
--csv=results/[run-id]
# Locust — web UI (interactive)
locust -f locustfile.py --host https://[test-env-url]
7. 取得するメトリクス
各テストの実行中に、以下すべてを取得してください。いずれかが欠けると、結果の比較の信頼性が損なわれます。
| メトリクス | ソース | 重要な理由 |
|---|---|---|
| エンドポイントごとの p50、p95、p99、p999 レイテンシ | 負荷ツール | SLO の検証 |
| エンドポイントごとのエラー率(4xx、5xx) | 負荷ツール | SLO の検証 |
| リクエスト数/秒(スループット) | 負荷ツール | 容量のベースライン |
| CPU 使用率(%) | インフラ監視 | 飽和の兆候 |
| メモリ使用率(%) | インフラ監視 | リークの検出 |
| GC 停止時間/頻度 | JVM/Go のメトリクス | レイテンシ急増の根本原因 |
| DB 接続プール:アクティブ/アイドル/待機中 | DB メトリクス | プール枯渇の検出 |
| DB クエリレイテンシ(p99) | DB メトリクス | 下流のボトルネック |
| キャッシュヒット率 | キャッシュメトリクス | ミスストームの検出 |
| Pod/インスタンス数(オートスケーリングの場合) | インフラ | スケーリングの挙動 |
| ネットワークの受信/送信バイト数 | インフラ | 帯域幅の飽和 |
8. 結果分析フレームワーク
各テストの実行後、次の順序で分析を進めます。
ステップ 1 — 合否の確認 取得したすべてのメトリクスをセクション 2 のしきい値と比較します。シナリオごとに合否を記録します。
ステップ 2 — レイテンシ分布 パーセンタイルだけでなく、レイテンシの全体的なヒストグラムをプロットします。二峰性分布(山が 2 つある分布)は、異なるコードパスが 2 つあることを示しています。遅いほうの山を調査します。
ステップ 3 — エラーの相関分析 エラーが発生した場合は、次の項目と関連付けて分析します。
- 発生時刻(ランプアップ中、定常状態、またはスパイク中のどのタイミングか)
- 特定のエンドポイント(1 つのエンドポイントか、すべてか)
- インフラストラクチャのイベント(CPU スパイク、OOM、DB 接続の枯渇など)
ステップ 4 — 飽和状態の分析 CPU、メモリ、接続プールを時系列でグラフ化します。リソースのいずれかが容量の 80% 以上に達した場合、今回の実行で SLO を達成していても、ボトルネック候補となります。
ステップ 5 — ベースライン実行との比較 すべての実行結果を前回の実行結果と比較します。p99 レイテンシが 10% 悪化した場合、SLO の範囲内でも調査が必要です。
リグレッションの分類:
| 変化 | 分類 | 対応 |
|---|---|---|
| p99 が前回の実行結果の 5% 以内 | 緑 — リグレッションなし | 対応不要 |
| p99 が前回より 5~15% 悪化 | 黄 — 要監視 | 次回リリース前に調査 |
| p99 が前回より 15% 超悪化 | 赤 — リグレッション | リリースをブロックし、チケットを起票 |
| エラー率が前回より上昇 | 赤 — リグレッション | リリースをブロック |
| SLO のしきい値を超過 | 重大 | リリースをブロックし、オンコール担当者に通知 |
9. CI への統合
リリースパイプラインのゲート付きステップとして負荷テストを追加します。リリース候補ごとにベースラインシナリオを実行し、すべてのシナリオを毎週実行します。
# Example: GitHub Actions step (adapt for your CI platform)
load-test:
runs-on: ubuntu-latest
needs: [deploy-staging]
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v3
- name: Install k6
run: |
curl -s https://dl.k6.io/key.gpg | sudo apt-key add -
echo "deb https://dl.k6.io/deb stable main" | sudo tee /etc/apt/sources.list.d/k6.list
sudo apt-get update && sudo apt-get install k6
- name: Seed test data
run: [seed command]
- name: Run baseline load test
run: |
k6 run \
--env BASE_URL=${{ secrets.LOAD_TEST_ENV_URL }} \
--out json=results.json \
scripts/load_test.js
env:
LOAD_TEST_ENV_URL: ${{ secrets.LOAD_TEST_ENV_URL }}
- name: Check thresholds
run: |
# k6 exits with non-zero if any threshold fails — this step fails the build
echo "k6 threshold check complete"
- name: Upload results
uses: actions/upload-artifact@v3
if: always()
with:
name: load-test-results-${{ github.run_id }}
path: results.json
- name: Cleanup test data
if: always()
run: [cleanup command]
CI ゲートの概要:
- ステージングへのリリースごとにベースラインシナリオを実行
- 全シナリオスイート(ストレス、スパイク、ソーク)をスケジュールに従って毎週実行
- しきい値に達しない場合は、本番環境への昇格をブロック
- 傾向分析のため、結果をアーカイブ
品質チェック
- すべての主要なエンドポイントを少なくとも 1 つのテストシナリオでカバーしている — 本番環境のエンドポイントに未テストのものがない
- しきい値は推測ではなく、実際の SLO 目標に基づいて設定している
- テストデータのシード処理をスクリプト化し、再現可能にしている — 既存の環境状態に依存しない
- 負荷ジェネレーターは、テスト対象のサービスとは別のインフラストラクチャで実行している
- CI 統合によって、しきい値に達しない場合は昇格をブロックしている — 結果を記録するだけではない
- メモリと接続プールのベースラインを確立するため、ソークテストを少なくとも一度実行している
- 前回の実行結果との比較を分析に含めている — 絶対的な合否判定だけではない
アンチパターン
- 実際の SLO 目標や本番環境のベースラインを根拠にせず、しきい値を設定しないでください — 恣意的な数値では、意味のない合否判定になります
- 負荷ジェネレーターをテスト対象のサービスと同じホストで実行しないでください — テスト結果とサービスのメトリクスの両方が汚染されます
- 負荷テスト用のシードデータに本番環境のユーザーデータを使用しないでください — すべてのテストデータは合成データとし、タグを付けて、各実行後にクリーンアップする必要があります
- 初回デプロイ時にソークテストを省略しないでください — 短時間のテストでは見つからない、緩やかなメモリリークや接続プールの枯渇を明らかにできるのはソークテストだけです
- ベースラインテストに合格したことを、サービスがスパイクに対応できる証拠として扱わないでください — ベースライン、ストレス、スパイク、ソークの各シナリオでは、根本的に異なる障害モードをテストします