CPUは空いているのにスループットが上がらない
サーバーを増やしても速くならない状況の真因を突き止められる。ボトルネックが並列度・ロック・I/O待ち・単一の直列区間のどこにあるかを、リトルの法則と待ち状態の観測で確定できる。
- CPU使用率が低いのに頭打ちなら、CPUはボトルネックではない。どこかで待っているか、そもそも並列に走れていないかのどちらか。
- 最初の判定はリトルの法則。目標スループット×平均レイテンシで必要な並列度を求め、実際の同時実行数が足りているかを見る。足りなければ設定上限が犯人。
- 並列度が足りているのに上がらないなら、ロック競合・共有資源の直列化・単一スレッド区間を疑う。台数を増やしても直列区間は縮まない。
負荷試験でリクエストを増やしても、スループットがある値で頭打ちになる。サーバーの CPU 使用率を見ると 30% 程度で、明らかに余っている。台数を倍にしても、スループットはほとんど変わらない——。
この状況で最初に確認すべきは、「余っている資源を見て安心しない」ことです。CPU が空いているという事実は、CPU がボトルネックでないことしか意味しません。ボトルネックは必ずどこかに存在します。
大きく2つに割る
横にスクロール
リクエストは「待っている」のか「並列に走れていない」のか
待っている
→ I/O待ち、ロック待ち、プール待ち、外部呼び出し待ち
CPUは空くが、その間スロットは占有されている
並列に走れていない
→ 同時実行数の上限、単一スレッド区間、共有資源の直列化
そもそも同時に処理する数が制限されている
この2つは対処がまったく違います。前者は並列度を上げれば改善する(待っている間に別の仕事ができる)。後者は並列度を上げても改善しない(上限そのものが原因、あるいは直列区間が縮まらない)。
最初の判定: リトルの法則で必要な並列度を出す
リトルの法則 L = λW を使います。L は同時実行数、λ はスループット、W は1件あたりの滞在時間です。
必要な同時実行数 L = 目標スループット λ × 平均レイテンシ W
例: 目標 1,000 req/s、平均レイテンシ 200ms
L = 1000 × 0.2 = 200
→ 同時に200件を処理できなければ、1,000 req/s には絶対に届かない
そして実際の同時実行数の上限を確認します。ここに設定された上限があれば、それがそのままスループットの上限になります。
| 上限になりうる設定 | 確認するもの | 典型的な既定値の罠 |
|---|---|---|
| スレッドプール/ワーカー数 | アプリのワーカー設定 | 既定が控えめで、増やす前提の値になっている |
| DBコネクションプール | プールの最大数と待機キュー長 | アプリのワーカー数より小さいと、ここで直列化する |
| HTTPクライアントの接続上限 | ホストあたりの最大同時接続数 | 既定が数本のことがあり、外部API呼び出しが詰まる |
| リバースプロキシの上限 | 上流への同時接続・キープアライブ設定 | アプリより手前で絞られる |
| セマフォ/レート制限 | アプリ内の同時実行制御 | 防御のために入れた値が本番規模に合っていない |
この表のどれか1つでも L を下回っていれば、そこが答えです。 台数を増やしても、1台あたりの上限が変わらなければ全体も比例して増えるだけで、1台が詰まっている理由は解消しません。
経路上に上限が複数あるとき、スループットを決めるのは最も小さい1つです。ワーカー200・プロキシ500・DBプール20なら、実効的な並列度は20で頭打ちになります。ワーカーをいくら増やしてもDBプール待ちの行列が伸びるだけで、レイテンシだけ悪化してスループットは動きません。最小の上限を見つけて、そこだけを上げるのが正しい順序です。
並列度は足りているのに上がらない場合
上限を上げても改善しないなら、同時に走れていない理由があります。
ロック競合
複数のスレッドが同じロックを取り合うと、クリティカルセクションは実質的に直列になります。CPU使用率は低いまま(待っているだけなので)、スループットだけが頭打ちになります。
見分け方は、スレッドの状態を複数回サンプリングして、同じロックで待っているスレッドが多数いるかを見ることです。スレッドダンプを数秒間隔で数回取り、BLOCKED や待機状態のスレッドが同じ場所に集中していれば確定です。
さらに厄介なのは、並列度を上げるほど悪化することです。ロックを待つスレッドが増えると、コンテキストスイッチとキャッシュの追い出しが増え、スループットが下がることすらあります。
共有資源の直列化
ロックが見えなくても、実質的に直列化する場所があります。
- 単一の書き込み先。 同じ行・同じキー・同じファイルへの更新は直列化します。ホットな行への更新が集中すると、それ以上は並列に進めません。
- 単一のコネクション。 1本の接続を共有していると、その上での要求は順に処理されます。
- ログ出力。 同期的にディスクへ書く実装だと、全スレッドがそこで待ちます。意外に多い原因です。
単一スレッド区間(アムダールの法則)
処理のうち直列にしか実行できない割合を p とすると、どれだけ並列化しても速度向上は 1 / p を超えられません。
処理の5%が直列 → 上限は20倍
処理の10%が直列 → 上限は10倍
処理の20%が直列 → 上限は5倍
台数を増やしても頭打ちになる曲線が見えたら、直列区間の存在を疑います。原理はアムダールの法則とスケーラビリティ、さらに競合と一貫性のコストまで含めた挙動は普遍スケーラビリティ法則で扱っています。台数を増やすと逆に遅くなる現象は、後者で説明されます。
CPUが空いていることの見方に注意
「CPU使用率30%」という数字自体にも罠があります。
平均で見ている。 全コアの平均が30%でも、1コアだけ100%に張り付いているなら、そのコアが単一スレッド区間です。コアごとに分けて見る必要があります。
I/O待ちを含めていない。 ロードアベレージが高いのにCPU使用率が低いなら、実行可能状態ではなく待ち状態のプロセスが積み上がっています。この読み方の違いはtop の出力解剖とps aux の出力解剖で詳しく扱っています。
コンテナの制限を見ていない。 ホストのCPUが空いていても、コンテナに割り当てたCPU上限で絞られていれば、そのプロセスは待たされます。スロットリングされた時間を示すメトリクスを確認します。
並列度を上げるとスループットが伸びますが、上げすぎるとレイテンシが急速に悪化します。使用率が高まるにつれ待ち時間は非線形に伸びるためです(詳しくは平均は正常なのにp99だけ遅い)。スループットだけを見て並列度を上げ続けると、SLOを割ってから気づくことになります。両方を並べて、レイテンシが許容範囲に収まる最大の並列度を探すのが正しい調整です。
まとめ
- CPUが空いている=ボトルネックがCPUでないというだけ。ボトルネックは必ずどこかにあり、「待っている」か「並列に走れていない」かのどちらか。
- 最初の判定はリトルの法則で必要な同時実行数
L = λWを求め、実際の上限と比べること。 - 経路上の上限のうち最も小さい1つだけがスループットを決める。他を上げてもレイテンシが悪化するだけ。
- 並列度が足りているのに伸びないなら、ロック競合・共有資源の直列化・単一スレッド区間を疑う。ロック競合は並列度を上げると悪化することすらある。
- 直列区間があると速度向上は
1 / pで頭打ちになる。台数を増やして横ばいになる曲線はその指紋。 - CPU使用率はコアごとに分けて見る。平均30%でも1コアが飽和していれば、それが単一スレッド区間。
トラブルシュート実戦の記事ガイド
CPUは空いているのにスループットが上がらないを実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
トラブルシュート
比較で見る軸
難易度: advanced / カテゴリ: トラブルシュート実戦 / タグ数: 5
導入後に効く点
最初の判定はリトルの法則。目標スループット×平均レイテンシで必要な並列度を求め、実際の同時実行数が足りているかを見る。足りなければ設定上限が犯人。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- トラブルシュート実戦
- タグ数
- 5
判断チェックリスト
- 自社の用途が「トラブルシュート / パフォーマンス」に近いか確認する。
- 強みである「CPU使用率が低いのに頭打ちなら、CPUはボトルネックではない。どこかで待っているか、そもそも並列に走れていないかのどちらか。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。