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

ボトルネックの見つけ方

「遅い」と分かっても、どこが遅いのか分からなければ改善できません。ボトルネックを見つける考え方を学びます。


ボトルネックとは

┌─────────────────────────────────────────────────────────────┐
│ 全体は最も遅い部分で律速される │
├─────────────────────────────────────────────────────────────┤
│ │
│ 処理の流れ: │
│ │
│ ┌────┐ ┌────┐ ┌────┐ ┌────┐ ┌────┐ │
│ │10ms│ → │ 5ms│ → │80ms│ → │10ms│ → │ 5ms│ │
│ └────┘ └────┘ └────┘ └────┘ └────┘ │
│ ↑ │
│ ボトルネック │
│ │
│ 全体: 110ms │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 10ms + 5ms の部分を 0ms にしても 95ms にしかならない│ │
│ │ 80ms の部分を 10ms にすれば 40ms になる │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 教訓: ボトルネック以外を改善しても効果は限定的 │
│ │
└─────────────────────────────────────────────────────────────┘

どこから調べるか

レイヤーで考える

┌─────────────────────────────────────────────────────────────┐
│ システムのレイヤー │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ クライアント │ │
│ └─────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ネットワーク │ │
│ └─────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ロードバランサー │ │
│ └─────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ アプリケーション (JVM) │ │
│ └─────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ データベース │ │
│ └─────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ OS / インフラ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ボトルネックはどのレイヤーにも存在しうる │
│ │
└─────────────────────────────────────────────────────────────┘

調査の順序

┌─────────────────────────────────────────────────────────────┐
│ 外側から内側へ絞り込む │
├─────────────────────────────────────────────────────────────┤
│ │
│ Step 1: どのリクエストが遅い? │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ・特定のエンドポイントだけ遅い? │ │
│ │ ・全体的に遅い? │ │
│ │ ・特定の時間帯だけ遅い? │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ Step 2: どのレイヤーで時間がかかっている? │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ・ネットワーク?(レイテンシ計測) │ │
│ │ ・アプリケーション?(APMで確認) │ │
│ │ ・データベース?(スロークエリログ) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ Step 3: そのレイヤー内のどこ? │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ・特定のメソッド?(プロファイラ) │ │
│ │ ・特定のクエリ?(EXPLAIN) │ │
│ │ ・特定のリソース?(CPU、メモリ、I/O) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘

USEメソッド

┌─────────────────────────────────────────────────────────────┐
│ USE = Utilization, Saturation, Errors │
├─────────────────────────────────────────────────────────────┤
│ │
│ Brendan Gregg が提唱したリソース分析手法 │
│ │
│ 各リソースに対して3つの指標を確認する: │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ U: Utilization(使用率) │ │
│ │ リソースがどれだけ使われているか │ │
│ │ 例: CPU使用率 80% │ │
│ │ │ │
│ │ S: Saturation(飽和度) │ │
│ │ リソースを待っている仕事の量 │ │
│ │ 例: ロードアベレージ、キュー長 │ │
│ │ │ │
│ │ E: Errors(エラー) │ │
│ │ エラーの発生数 │ │
│ │ 例: ネットワークエラー、ディスクエラー │ │
│ │ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘

USEメソッドの適用

┌─────────────────────────────────────────────────────────────┐
│ リソース別のUSE確認項目 │
├─────────────────────────────────────────────────────────────┤
│ │
│ CPU: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ U: CPU使用率 (top, vmstat) │ │
│ │ S: ロードアベレージ、ランキュー長 (uptime) │ │
│ │ E: マシンチェック例外(稀) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ メモリ: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ U: メモリ使用率 (free, vmstat) │ │
│ │ S: スワップ発生頻度 (vmstat si/so) │ │
│ │ E: OOMキラー発動 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ディスクI/O: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ U: ディスク使用率 (iostat %util) │ │
│ │ S: I/O待ちキュー長 (iostat avgqu-sz) │ │
│ │ E: デバイスエラー (dmesg) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ネットワーク: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ U: 帯域使用率 (sar, iftop) │ │
│ │ S: ドロップ/オーバーラン (netstat -s) │ │
│ │ E: パケットエラー (ip -s link) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘

REDメソッド

┌─────────────────────────────────────────────────────────────┐
│ RED = Rate, Errors, Duration │
├─────────────────────────────────────────────────────────────┤
│ │
│ マイクロサービス向けの分析手法 │
│ (USEはリソース視点、REDはリクエスト視点) │
│ │
│ 各サービスに対して3つの指標を確認する: │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ R: Rate(レート) │ │
│ │ 1秒あたりのリクエスト数 │ │
│ │ 例: 500 req/sec │ │
│ │ │ │
│ │ E: Errors(エラー) │ │
│ │ 失敗したリクエストの数/率 │ │
│ │ 例: エラーレート 0.1% │ │
│ │ │ │
│ │ D: Duration(期間) │ │
│ │ リクエストの処理時間 │ │
│ │ 例: P99 = 200ms │ │
│ │ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ これらをダッシュボードで可視化し、異常を検知する │
│ │
└─────────────────────────────────────────────────────────────┘

ボトルネックの連鎖

┌─────────────────────────────────────────────────────────────┐
│ ボトルネックは1つとは限らない │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1つ改善すると、隠れていた次のボトルネックが露出する │
│ │
│ パターン1: DB改善 → CPU露出 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Before: DBクエリ500ms → スループット低い → CPU 30% │ │
│ │ After: DBクエリ100ms → スループット上昇 → CPU 100% │ │
│ │ │ │
│ │ DBが遅くて「詰まっていた」からCPUに余裕があっただけ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ パターン2: CPU増強 → DB露出 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Before: CPU不足 → リクエスト少ない → DB接続に余裕 │ │
│ │ After: CPU増強 → リクエスト増加 → DB接続プール枯渇 │ │
│ │ │ │
│ │ CPUがボトルネックでDBまで負荷が届いていなかっただけ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 教訓: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ・ボトルネック解消後、必ず再計測する │ │
│ │ ・次のボトルネックが露出していないか確認 │ │
│ │ ・全体のバランスを見ながら改善を繰り返す │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘

アプリケーションの本質的なボトルネック

┌─────────────────────────────────────────────────────────────┐
│ 最終的なボトルネックはアプリ特性で決まる │
├─────────────────────────────────────────────────────────────┤
│ │
│ アプリケーションの処理特性によって、 │
│ 最終的にボトルネックになる場所は決まっている │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ CPUバウンド: │ │
│ │ 暗号処理、JWT署名、画像処理、圧縮 │ │
│ │ 例: 認証サーバー、FIDOサーバー │ │
│ │ │ │
│ │ I/Oバウンド: │ │
│ │ ファイル読み書き、大量データ処理 │ │
│ │ 例: ファイルサーバー、バッチ処理 │ │
│ │ │ │
│ │ メモリバウンド: │ │
│ │ 大量データのインメモリ処理、キャッシュ │ │
│ │ 例: データ分析、検索エンジン │ │
│ │ │ │
│ │ ネットワークバウンド: │ │
│ │ 外部API呼び出し、リクエスト転送 │ │
│ │ 例: APIゲートウェイ、プロキシ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ この特性を理解していると: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ・チューニングの方向性が分かる │ │
│ │ ・「最終的にここに行き着く」と予測できる │ │
│ │ ・無駄な改善を避けられる │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘

よくあるボトルネック

アプリケーション層

┌─────────────────────────────────────────────────────────────┐
│ アプリケーションのボトルネック │
├─────────────────────────────────────────────────────────────┤
│ │
│ CPU bound: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ・重い計算(暗号化、圧縮、JSON解析) │ │
│ │ ・無限ループ、非効率なアルゴリズム │ │
│ │ ・過度な正規表現 │ │
│ │ → CPU使用率が高い │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ Memory bound: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ・大量のオブジェクト生成 │ │
│ │ ・メモリリーク │ │
│ │ ・GC頻発 │ │
│ │ → ヒープ使用率が高い、GC時間が長い │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ I/O bound: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ・外部API呼び出し待ち │ │
│ │ ・DB接続待ち │ │
│ │ ・ファイルI/O │ │
│ │ → スレッドがブロック状態で待機 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ Lock contention: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ・synchronizedブロックでの待ち │ │
│ │ ・データベースのロック待ち │ │
│ │ → スレッドがロック待ちで停滞 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘

データベース層

┌─────────────────────────────────────────────────────────────┐
│ データベースのボトルネック │
├─────────────────────────────────────────────────────────────┤
│ │
│ クエリ: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ・フルテーブルスキャン(インデックス未使用) │ │
│ │ ・N+1問題(ループ内でクエリ発行) │ │
│ │ ・非効率なJOIN │ │
│ │ → スロークエリログで発見 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 接続: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ・接続プール枯渇 │ │
│ │ ・接続のリーク(closeし忘れ) │ │
│ │ → 接続待ちタイムアウト │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ロック: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ・長時間のトランザクション │ │
│ │ ・デッドロック │ │
│ │ → ロック待ちタイムアウト │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘

ネットワーク層

┌─────────────────────────────────────────────────────────────┐
│ ネットワークのボトルネック │
├─────────────────────────────────────────────────────────────┤
│ │
│ レイテンシ: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ・地理的な距離(光の速度の限界) │ │
│ │ ・DNSルックアップ │ │
│ │ ・TCP接続確立(3-way handshake) │ │
│ │ ・TLSハンドシェイク │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 帯域: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ・大きなペイロード │ │
│ │ ・帯域の飽和 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 接続管理: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ・コネクションプール不足 │ │
│ │ ・Keep-Alive未使用 │ │
│ │ ・ポート枯渇 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘

調査ツール

┌─────────────────────────────────────────────────────────────┐
│ レイヤー別の調査ツール │
├─────────────────────────────────────────────────────────────┤
│ │
│ アプリケーション: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ・APM(Datadog, New Relic, Elastic APM) │ │
│ │ ・プロファイラ(async-profiler, JFR) │ │
│ │ ・ログ分析(Elasticsearch, Loki) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ データベース: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ・スロークエリログ │ │
│ │ ・EXPLAIN / EXPLAIN ANALYZE │ │
│ │ ・pg_stat_statements(PostgreSQL) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ OS / インフラ: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ・top, htop, vmstat, iostat │ │
│ │ ・perf, bpftrace │ │
│ │ ・Prometheus + Grafana │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ネットワーク: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ・ping, traceroute, mtr │ │
│ │ ・tcpdump, Wireshark │ │
│ │ ・netstat, ss │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘

まとめ

┌─────────────────────────────────────────────────────────────┐
│ ボトルネック発見の心得 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. 推測しない、計測する │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 「たぶんここが遅い」→ 外れることが多い │ │
│ │ 計測して「ここが遅い」と確認してから改善 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 2. 外側から内側へ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ どのリクエスト → どのレイヤー → どの処理 │ │
│ │ 広く浅く → 狭く深く │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 3. USE/REDメソッドを使う │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ USE: リソース視点(CPU、メモリ、I/O) │ │
│ │ RED: リクエスト視点(レート、エラー、時間) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 4. 1つずつ改善する │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 複数同時に変えると効果が分からない │ │
│ │ 改善 → 計測 → 次の改善 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘

次のステップ