メインコンテンツまでスキップ

負荷テスト

システムの限界性能を把握し、本番でのトラブルを未然に防ぐための負荷テストを学びます。


なぜ負荷テストが必要か

┌─────────────────────────────────────────────────────────────┐
│ 本番で初めて分かるのでは遅い │
├─────────────────────────────────────────────────────────────┤
│ │
│ 負荷テストなしで起こること: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 1. 開発環境では快適に動く │ │
│ │ 2. 本番リリース │ │
│ │ 3. 大量アクセスが来る │ │
│ │ 4. システムダウン │ │
│ │ 5. 原因不明、対処できない │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 負荷テストで分かること: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ・最大スループット(何req/secまで耐えられるか) │ │
│ │ ・飽和点(どこからレイテンシが悪化するか) │ │
│ │ ・ボトルネック(何が限界を決めているか) │ │
│ │ ・回復性(負荷が下がったら回復するか) │ │
│ │ ・並行バグ(単体テストでは再現しない問題) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘

負荷テストの種類

┌─────────────────────────────────────────────────────────────┐
│ 目的に応じたテスト種類 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 負荷テスト (Load Test): │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 想定される負荷での動作確認 │ │
│ │ 例: 通常時の2倍の負荷で30分 │ │
│ │ 目的: SLOを満たせるか確認 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ストレステスト (Stress Test): │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 限界を超える負荷での動作確認 │ │
│ │ 例: 負荷を徐々に上げて破綻点を探す │ │
│ │ 目的: 限界値の把握、障害時の挙動確認 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ スパイクテスト (Spike Test): │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 急激な負荷変動への耐性確認 │ │
│ │ 例: 10秒で10倍の負荷をかける │ │
│ │ 目的: オートスケール、バッファの検証 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ソークテスト (Soak Test): │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 長時間の継続負荷での動作確認 │ │
│ │ 例: 通常負荷で24時間 │ │
│ │ 目的: メモリリーク、リソースリークの検出 │ │
│ │ 並行バグの検出(後述) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘

負荷パターン

┌─────────────────────────────────────────────────────────────┐
│ 負荷のかけ方 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ランプアップ (Ramp-up): │
│ 負荷 ↑ │
│ │ ________ │
│ │ / │
│ │ / │
│ │ / │
│ │/ │
│ └──────────────────────→ 時間 │
│ │
│ ステップ (Step): │
│ 負荷 ↑ │
│ │ ┌──────── │
│ │ ┌───┘ │
│ │ ┌───┘ │
│ │────┘ │
│ └──────────────────────→ 時間 │
│ │
│ スパイク (Spike): │
│ 負荷 ↑ │
│ │ ∧ │
│ │ ╱ ╲ │
│ │ ╱ ╲ │
│ │─────╱ ╲───── │
│ └──────────────────────→ 時間 │
│ │
└─────────────────────────────────────────────────────────────┘

テスト設計

シナリオ設計

┌─────────────────────────────────────────────────────────────┐
│ 現実的なシナリオを作る │
├─────────────────────────────────────────────────────────────┤
│ │
│ 悪い例: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ・同じエンドポイントに連続リクエスト │ │
│ │ ・同じユーザーで全リクエスト │ │
│ │ ・キャッシュが効きすぎる │ │
│ │ → 本番とかけ離れた結果になる │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 良い例: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ・実際のユーザー行動をシミュレート │ │
│ │ - ログイン → 一覧取得 → 詳細閲覧 → 更新 │ │
│ │ ・複数ユーザーでリクエスト │ │
│ │ ・Think time(人間の操作間隔)を入れる │ │
│ │ ・エンドポイントの比率を本番に合わせる │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ エンドポイント比率の例: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ GET /api/users : 50% (一覧取得) │ │
│ │ GET /api/users/{id} : 30% (詳細取得) │ │
│ │ POST /api/users : 10% (作成) │ │
│ │ PUT /api/users/{id} : 8% (更新) │ │
│ │ DELETE /api/users/{id}: 2% (削除) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘

テストデータ

┌─────────────────────────────────────────────────────────────┐
│ テストデータの準備 │
├─────────────────────────────────────────────────────────────┤
│ │
│ データ量: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ・本番と同等のデータ量を用意する │ │
│ │ ・少ないデータでは問題が見つからない │ │
│ │ - インデックスの効果 │ │
│ │ - ページネーションの負荷 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ データの多様性: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ・同じデータばかりだとキャッシュが効きすぎる │ │
│ │ ・ランダムなIDでアクセス │ │
│ │ ・様々なパターンのデータを用意 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ テストユーザー: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ・十分な数のテストユーザーを用意 │ │
│ │ ・同じユーザーで全リクエストは非現実的 │ │
│ │ ・例: 仮想ユーザー1000人分の認証情報 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘

結果の読み方

┌─────────────────────────────────────────────────────────────┐
│ 何を見るか │
├─────────────────────────────────────────────────────────────┤
│ │
│ 基本指標: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ・スループット: 1秒あたりの成功リクエスト数 │ │
│ │ ・レイテンシ: P50, P95, P99 │ │
│ │ ・エラーレート: 失敗リクエストの割合 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ グラフの見方: │
│ │
│ レイテンシ │
│ ↑ │
│ │ / │
│ │ / │
│ │ ─────────/ ← ここが飽和点 │
│ │ ───────── │
│ └──────────────────────→ スループット │
│ │
│ エラーレート │
│ ↑ │
│ │ / │
│ │ / │
│ │ ──────────────────────── ← エラーが出始める点 │
│ └──────────────────────────→ スループット │
│ │
│ 判断基準: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ・SLOを満たせるスループットはどこまでか │ │
│ │ ・エラーが出始めるのはどこからか │ │
│ │ ・余裕を持った運用ラインはどこか(70-80%) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ エラーログの確認(並行バグの検出): │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 負荷テスト中のエラーは「量」だけでなく │ │
│ │ 「種類」を必ず確認する。 │ │
│ │ │ │
│ │ 要注意のエラーパターン: │ │
│ │ ・ArrayIndexOutOfBoundsException(散発的) │ │
│ │ ・ConcurrentModificationException │ │
│ │ ・NullPointerException(低負荷では再現しない) │ │
│ │ ・不正なレスポンスデータ(ハッシュ値の不一致等) │ │
│ │ │ │
│ │ これらは並行バグの兆候。 │ │
│ │ 単体テストでは再現せず、負荷テストで初めて │ │
│ │ 顕在化することが多い。 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘

負荷テストで見つかる並行バグ

負荷テストの目的はパフォーマンス計測だけではありません。単体テストでは再現しないスレッドセーフの問題を検出する重要な機会です。

なぜ単体テストでは見つからないのか

┌─────────────────────────────────────────────────────────────┐
│ 単体テスト vs 負荷テスト │
├─────────────────────────────────────────────────────────────┤
│ │
│ 単体テスト: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ・シングルスレッドで実行 │ │
│ │ ・共有状態への同時アクセスが起きない │ │
│ │ ・タイミングに依存するバグは再現しない │ │
│ │ → テストは全てグリーン │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 負荷テスト: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ・数百の並行リクエストが同時に処理される │ │
│ │ ・共有オブジェクトに同時アクセスが発生 │ │
│ │ ・タイミング次第で内部状態が破壊される │ │
│ │ → 散発的にエラーが発生 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘

実例: enum フィールドの MessageDigest 共有

本プロジェクトで実際に発生した事例です(PR #1343)。

┌─────────────────────────────────────────────────────────────┐
│ HashAlgorithm enum のスレッド安全性バグ │
├─────────────────────────────────────────────────────────────┤
│ │
│ 問題のコード: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ enum HashAlgorithm { │ │
│ │ SHA_256("SHA-256"); │ │
│ │ │ │
│ │ MessageDigest messageDigest; // 全スレッドで共有 │ │
│ │ } │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 何が起きたか: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 1. 低負荷(開発環境)→ 問題なし │ │
│ │ 2. 負荷テストで並行リクエストを増加 │ │
│ │ 3. 複数スレッドが同じ MessageDigest に同時アクセス │ │
│ │ 4. 内部バッファが破壊される │ │
│ │ 5. ArrayIndexOutOfBoundsException が散発的に発生 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 厄介な点: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ・毎回発生するわけではない(タイミング依存) │ │
│ │ ・例外が出ないケースもある(不正なハッシュ値が │ │
│ │ サイレントに返される) │ │
│ │ ・エラーレートが低いと見逃しやすい │ │
│ │ → 0.1% のエラー率を「ネットワークの問題」と │ │
│ │ 誤判断してしまう危険がある │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 修正: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ MessageDigest をフィールドにキャッシュせず、 │ │
│ │ messageDigest() で毎回新規生成するように変更 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘

負荷テストで並行バグを検出するためのチェックリスト

┌─────────────────────────────────────────────────────────────┐
│ 並行バグ検出のチェックリスト │
├─────────────────────────────────────────────────────────────┤
│ │
│ テスト実行時: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ □ エラーレート 0% を確認(0.01% でも調査する) │ │
│ │ □ HTTP 500 の内訳をアプリログで確認 │ │
│ │ □ レスポンスボディの整合性を検証 │ │
│ │ (ステータス200でもデータが壊れている可能性) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ アプリケーションログで確認: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ □ ArrayIndexOutOfBoundsException │ │
│ │ □ ConcurrentModificationException │ │
│ │ □ NullPointerException(予期しない箇所) │ │
│ │ □ IllegalStateException │ │
│ │ □ 「壊れた」データに起因するビジネスロジックエラー │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 再現性の確認: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ □ 並行数を増やすとエラー率が上がるか │ │
│ │ (並行バグなら並行数に比例して増える傾向) │ │
│ │ □ 同じエンドポイントで繰り返し発生するか │ │
│ │ □ シングルスレッドでは再現しないか │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
関連ドキュメント

並行バグの原因と対策の詳細は Java 落とし穴と注意事項 を参照してください。


主要なツール

┌─────────────────────────────────────────────────────────────┐
│ 負荷テストツール │
├─────────────────────────────────────────────────────────────┤
│ │
│ k6: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ・JavaScriptでシナリオを書ける │ │
│ │ ・軽量、高性能 │ │
│ │ ・CI/CDに組み込みやすい │ │
│ │ ・Grafanaとの連携が良い │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ JMeter: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ・GUIでシナリオ作成可能 │ │
│ │ ・長い歴史、豊富な機能 │ │
│ │ ・プラグインが豊富 │ │
│ │ ・重い(大規模テストには分散実行が必要) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ Gatling: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ・Scalaでシナリオを書く │ │
│ │ ・高性能 │ │
│ │ ・レポートが見やすい │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ Locust: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ・Pythonでシナリオを書ける │ │
│ │ ・Web UIでリアルタイム監視 │ │
│ │ ・分散実行が簡単 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘

負荷テストの注意点

┌─────────────────────────────────────────────────────────────┐
│ やってはいけないこと │
├─────────────────────────────────────────────────────────────┤
│ │
│ 本番環境への負荷テスト: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ❌ 本番DBに大量リクエスト │ │
│ │ ❌ 本番の外部APIを叩きまくる │ │
│ │ ❌ 実ユーザーに影響を与える │ │
│ │ │ │
│ │ → 必ずステージング環境で実施 │ │
│ │ → 本番同等の構成を用意する │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ テストクライアントの限界: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ・テストクライアント自体がボトルネックになる │ │
│ │ ・1台で出せる負荷には限界がある │ │
│ │ │ │
│ │ → クライアントのリソースも監視する │ │
│ │ → 必要なら分散クライアントを使う │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ネットワークの考慮: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ・同一ネットワーク内は本番より速い │ │
│ │ ・インターネット経由の遅延を考慮 │ │
│ │ │ │
│ │ → 本番に近いネットワーク構成で実施 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘

まとめ

┌─────────────────────────────────────────────────────────────┐
│ 負荷テストの心得 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 目的を明確に: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ・限界を知りたい → ストレステスト │ │
│ │ ・SLOを確認したい → 負荷テスト │ │
│ │ ・リークを見つけたい → ソークテスト │ │
│ │ ・並行バグを見つけたい → ストレス/ソークテスト │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 現実的なシナリオ: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ・本番のアクセスパターンを再現 │ │
│ │ ・十分なデータ量とユーザー数 │ │
│ │ ・Think timeを入れる │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 結果を活かす: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ・飽和点を把握してキャパシティ計画に反映 │ │
│ │ ・ボトルネックを特定して改善 │ │
│ │ ・定期的に実施して劣化を検知 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘

次のステップ