サーバーが遅い理由 #1 CPU 使用率は低いのに遅いとき — run queue・I/O wait・ロック競合

読了 7分

モニタリングダッシュボードの CPU 使用率は 20% なのに、応答時間は伸び続けています。CPU は余っているのに、なぜ遅いのでしょうか? 運用をしていれば必ず遭遇する状況で、このシリーズの出発点でもあります。

シリーズは 5 編です。「スペックは足りているのに遅い」という症状を、リソースごとに一つずつ追跡します。CPU 使用率は低いのに遅い場合(1 編)、メモリを増やしても速くならない場合(2 編)、SSD なのに遅い場合(3 編)、帯域は足りているのにネットワークが遅い場合(4 編)、データベースが遅くなる場合(5 編)の順です。ハードウェア基礎中級シリーズがリソースと指標の概念を固めるカリキュラムだとすれば、このシリーズは症状から出発して原因へ掘り下げていく事例中心の診断です。

使用率という指標が見落とすもの #

最初に確認すべきなのは、問いそのものです。「CPU 使用率が低い」という事実は「CPU がボトルネックではない」ことを保証しません。使用率は一定区間の平均であり、コアにタスクが載っていた時間の割合にすぎないからです。ここでは二つのことが抜け落ちます。

  • 平均に埋もれるバースト — 1 分平均 20% は、59 秒間 5% で 1 秒間 100% に達するパターンと区別できません。リクエストが集中する瞬間の待ち行列は、平均のグラフには現れません。
  • CPU の外の待ち — プロセスがディスクの応答、ロック、他サービスの応答を待つ時間は CPU を使わない時間なので、使用率には載りません。ユーザーが体感する応答時間の大部分がこの待ちから来ているケースは珍しくありません。

したがって診断の方向は「CPU がどれだけ忙しいか」ではなく「リクエストはどこで待っているか」に変わるべきです。待つ場所は大きく三つ、CPU の前の行列(run queue)、ディスクの応答(I/O wait)、そしてロックです。

run queue — CPU の前の行列 #

実行の準備はできているのにコアが空くのを待っているスレッドの行列が run queue です。使用率が低くてもこの行列は長くなり得ます。短いタスクが瞬間的にまとめて到着すると、平均使用率をほとんど上げないまま、個々のタスクは列に並んで遅延します。

vmstat の最初のカラムがまさにこの行列の長さです。

vmstat 1
$ vmstat 1
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
12  0      0 811240 219880 5480032    0    0     0    24 9821 21453 18  4 78  0  0
14  0      0 810988 219880 5480040    0    0     0     0 10233 22871 19  5 76  0  0

r が実行待ちのスレッド数です。このサーバーは使用率(us+sy)23% 前後なのに、r がコア数(例: 8)を大きく上回っています。コア数より長い行列が続くと、スケジューリング遅延がそのまま応答時間に上乗せされます。ハードウェア中級 #1 で見た load average も同じ信号を出しますが、load average は I/O 待ちのタスクまで含む値なので、原因の切り分けには vmstatrb(I/O 待ち)を分けて見るほうが向いています。

列に並ぶ時間そのものを測りたければ、eBPF ツールの runqlat がスケジューリング遅延の分布をヒストグラムで見せてくれます。ツール自体は ハードウェア上級 #2 で扱いました。使用率が低いのに runqlat の裾がミリ秒単位まで伸びているなら、瞬間的なバーストか、コア数に対して過剰なスレッド数を疑います。スレッドプールをコア数よりはるかに大きく取ったアプリケーションが典型例です。

I/O wait — idle のもう一つの顔 #

top%wa(iowait)は「実行できるタスクがなく、未完了のディスク I/O がある」時間です。ここには落とし穴が二つあります。

  • iowait は idle の一種です。CPU 使用率には含まれないので、「使用率 20% + iowait 40%」のサーバーは使用率グラフだけ見ると暇そうに見えます。
  • 逆に iowait が低いからといって I/O ボトルネックがないとも言えません。他のタスクが CPU を使っていれば、同じ I/O 待ちは iowait として集計されません。

そこで iowait は「高ければディスクを見る」という出発の合図としてだけ使い、判定はディスク側の指標で行います。iostat -xawait(I/O 一つがキュー待ちを含めてかかった平均時間)と %util を見て、どのプロセスかは pidstat -d で絞り込みます。

iostat -x 1
$ iostat -x 1
Device            r/s     w/s   rkB/s   wkB/s  await  %util
nvme0n1         210.0  1830.0   840.0 29280.0   8.42   96.4

使用率の低いサーバーで await が普段の数倍に跳ね、%util が 90% を超えているなら、ボトルネックは CPU ではなくストレージです。SSD なのにこうした数字が出る理由は 3 編で扱います。

ロック競合 — CPU でもディスクでもない待ち #

run queue も短く、ディスクも暇なのに遅いなら、残る候補はプロセス同士が互いを待っているケースです。ミューテックス、DB の行ロック、コネクションプールの空きコネクション、アップストリーム API の応答がここに該当します。これらの待ちは CPU をまったく使わないため、システム指標にはほとんど痕跡が残りません。スレッドは眠っていて、使用率は低く、応答だけが遅いのです。

システム側で拾える手がかりはコンテキストスイッチです。ロックを取れなかったスレッドが眠っては起きるのを繰り返すと、vmstatcs(context switch)が負荷に対して異常に高くなります。pidstat -w でプロセスごとの自発的コンテキストスイッチ(cswch/s)を見れば、どのプロセスが何かを待って眠り続けているのかを絞り込めます。

pidstat -w 1
$ pidstat -w 1 -p 4321
UID       PID   cswch/s nvcswch/s  Command
 1001     4321   8412.0      12.0  api-server

毎秒 8 千回の自発的スイッチは、このプロセスが働いては止まるのではなく、待っては目覚めるのを繰り返しているという意味です。ここから先はアプリケーション内部の問題なので、言語ごとのプロファイラでスレッドがどのロックで待っているかを見る off-CPU 分析へ進みます。実務で最も多い犯人は、コネクションプールの枯渇と、遅いクエリを抱えたまま長く続くトランザクションの二つで、この二つは 5 編でデータベース側の視点から見直します。

クラウドなら — スチールタイムの確認 #

仮想マシンではもう一つあります。top%st(steal)は、ゲストは実行の準備ができているのにハイパーバイザーが物理コアを割り当てなかった時間です。自分のサーバーの使用率は低いのに、同じ物理ホストの別テナントがコアを持っていっているなら、自分のプロセスは理由なく遅くなります。%st が継続的に数パーセントを超えるなら、インスタンスの再配置(別ホストへの移行)やタイプ変更を検討します。バースト型インスタンス(t 系)の CPU クレジット枯渇も同じ症状を作ります。詳しくは ハードウェア中級 #2 で扱いました。

診断手順のまとめ #

症状が「CPU 使用率は低いのに遅い」のとき、手順は次のとおりです。

  1. まず平均の罠を除く — モニタリング間隔を狭めて(1 秒単位)、瞬間的なバーストの有無を確認します。
  2. vmstat 1r がコア数を上回るならスケジューリング遅延(スレッド数過多、バースト)を疑い、b が積み上がるなら I/O 側を見ます。
  3. iostat -xawait%util でストレージのボトルネックを判定します。該当すれば 3 編の領域です。
  4. pidstat -w — 自発的コンテキストスイッチが高ければロック、コネクションプール、アップストリーム待ちを疑い、アプリケーションのプロファイリングへ進みます。
  5. 仮想マシンなら %st — スチールタイムと CPU クレジットを確認します。

まとめ #

  • CPU 使用率はコア占有時間の平均にすぎず、リクエストがどこで待っているかは教えてくれません。診断の問いは「CPU が忙しいか」ではなく「どこで待っているか」です。
  • run queue(vmstatr)がコア数を継続的に上回るなら、使用率が低くてもスケジューリング遅延が応答時間に上乗せされます。
  • iowait は idle の一種で使用率には載らず、低いからといって I/O ボトルネックがないわけでもありません。判定は iostatawait%util で行います。
  • ロック、コネクションプール、アップストリーム待ちはシステム指標にほとんど残りません。手がかりは高い自発的コンテキストスイッチで、確定は off-CPU 分析です。
  • 仮想マシンではスチールタイムと CPU クレジットが同じ症状を作ります。

次回はメモリです。メモリを増やしたのに速くならない理由を、ページキャッシュ、スワップ、ワーキングセットで追跡します。

X