Skip to content
Published on

ロードアベレージが CPU 使用率ではない理由 — load average 24 なのに CPU は 30% のとき

シェア
Authors

はじめに — load average 24、しかし CPU は 30% です

明け方にアラートが届きます。8 コアのサーバーのロードアベレージが 24 を超えました。接続して top を開くと、CPU のアイドルが 60% 以上も残っています。ここで次の 2 つの結論のどちらかに流れてしまいがちです。「監視がおかしい」あるいは「CPU を 3 倍に増やすべきだ」。どちらも間違いです。

ロードアベレージは CPU 使用率ではありません。単位からして違います。使用率は割合 (0〜100%) であり、ロードは個数 (タスク数) です。そして Linux はこの個数を数えるとき、他の Unix が数えないものをもう 1 つ数えます。その 1 つがこの記事の半分を占めます。

uptime
#  03:41:52 up 12 days,  3:21,  2 users,  load average: 24.31, 22.08, 14.95

nproc
# 8

cat /proc/loadavg
# 24.31 22.08 14.95 2/1284 48210

最後の行の 2/1284 を見てください。前の 2 は今この瞬間に実行可能なスレッド数で、1284 は全スレッド数です。ロードは 24 なのに、実行可能なスレッドは 2 個です。残りの 22 はどこにいるのでしょうか。この問いの答えが診断の出発点です。

3 つの数字の正体 — 5 秒サンプリングと指数移動平均

カーネルは LOAD_FREQ の周期、つまり 5 秒に 1 回 (正確には 5 秒 + 1 ティックで、周期的な割り込みと位相が重なるのを避ける意図です) 各 CPU のランキューからアクティブなタスク数をサンプリングします。アクティブなタスクは次のように計算されます。

nr_active = rq->nr_running + rq->nr_uninterruptible

nr_running は今実行中または実行可能なタスク、nr_uninterruptible は割り込み不可能スリープ状態のタスクです。この合計をすべての CPU について足した値が、その瞬間の標本です。

その次の更新式が核心です。カーネルは固定小数点演算で次を計算します。

load = (load * EXP + active * (FIXED_1 - EXP)) >> FSHIFT

FSHIFT  = 11,  FIXED_1 = 2048
EXP_1   = 1884   (2048 * e^(-5/60))
EXP_5   = 2014   (2048 * e^(-5/300))
EXP_15  = 2037   (2048 * e^(-5/900))

定数を検算してみると理解が早くなります。5 秒ごとに更新しながら 1 分の時定数を得るには、減衰係数が e のマイナス 5/60 乗、つまり 0.9200 でなければなりません。0.9200 に 2048 を掛けると 1884 です。5 分と 15 分も同じやり方で 2014、2037 になります。

ここから実務上重要な性質が 1 つ出てきます。これは算術平均ではなく指数減衰平均です。負荷が階段状に跳ね上がると、1 分平均はちょうど 1 分後に最終値の 63.2% に達し、99% に達するには約 5 分かかります。15 分平均は同じ理屈で 1 時間以上引きずられます。

経過時間1 分平均の反映率5 分平均の反映率15 分平均の反映率
1 分63.2%18.1%6.5%
5 分99.3%63.2%28.3%
15 分100.0%95.0%63.2%
45 分100.0%100.0%95.0%

そのため「1 分平均が 5 分・15 分より大きければ負荷は上昇中」という慣用的な解釈が成り立ちます。逆に、障害が終わった直後に 15 分平均だけ高く残っているのは正常です。その数字でアラートを設定すると、復旧後 20 分間鳴り続けます。

Linux ならではの決定的な違い — D 状態まで数える

伝統的な Unix はロードアベレージをランキューの長さだけで定義していました。つまり CPU を待つタスク数です。Linux は 1993 年にそこへ TASK_UNINTERRUPTIBLE 状態を加えました。意図は明確でした。CPU の需要だけでなく、システム資源全般に対する需要を測るというものです。

TASK_UNINTERRUPTIBLEpsD と表示される状態です。シグナルでも起こせないカーネル内部の待ちで、代表的にはこういうものです。

  • ブロックデバイス I/O の完了待ち (io_schedule)
  • ページキャッシュのロック待ち (folio_wait_bit_common)
  • ハードマウントされた NFS サーバーの応答待ち
  • 一部のカーネルミューテックスやセマフォの待ち
  • デバイスドライバのファームウェア応答待ち

このリストに CPU はありません。つまり CPU が完全に遊んでいてもロードアベレージはいくらでも上がります。NFS サーバーが 1 台応答しなくなると、そのマウントにアクセスしたプロセスがすべて D 状態で積み上がり、8 コアのサーバーのロードが 200 を記録する一方で CPU のグラフは平坦なままです。これが正常な動作です。

1 つの例外を知っておくと役に立ちます。カーネル 4.2 から TASK_IDLE (TASK_UNINTERRUPTIBLE | TASK_NOLOAD) が導入され、やることがなく待機しているカーネルスレッドはロードに含まれなくなりました。psI に見えるものがここに該当します。

ps -eo state,pid,comm --no-headers | awk '{c[$1]++} END {for (s in c) print c[s], s}' | sort -rn
#    1183 S
#      68 I
#      22 D
#       2 R
#       9 Z

D が 22、R が 2。先ほど見たロード 24 と正確に一致します。この時点で結論は 1 つです。このサーバーは CPU が足りないのではなく、どこかで I/O を待っています

ここでよく見かける誤った説明を押さえておきます。「ロードアベレージは CPU の待ち行列の長さだ」という文は、他の Unix では正しく Linux では誤りです。そして「ロードがコア数より低ければ安全だ」というルールも、その値に D 状態が混ざっている以上、安全を保証しません。

「コア数で割れ」はどこで破綻するのか

ロードを nproc で割って 1.0 未満なら余裕、1.0 以上なら飽和とする慣習は、出発点としては使えます。しかし次の 5 つの状況で破綻します。

第一に、D 状態による汚染です。前の節そのままです。24/8 = 3.0 ですが、CPU の飽和とは何の関係もありません。

第二に、平均の遅れです。30 秒のスパイクは 1 分平均には半分も現れません。逆に、すでに終わった障害が 15 分平均に残り続けます。

第三に、SMT(ハイパースレッディング)です。nproc は論理 CPU を数えます。物理コア 8 個で SMT が有効なら 16 と出ますが、実際の処理能力は 16 コアではありません。ロード 12 は「16 コアのうち 75%」を意味しません。

lscpu | grep -E '^CPU\(s\)|Thread|Core|Socket'
# CPU(s):                          16
# Thread(s) per core:              2
# Core(s) per socket:              8
# Socket(s):                       1

第四に、cgroup のクォータです。コンテナに CPU limit がかかってスロットリングされるタスクはランキューから完全に外れるため、ロードアベレージに現れません。64 コアのホストで 0.5 CPU に制限されたポッドがひどく遅くても、ホストのロードは静かなままです。この組み合わせは後でもう一度扱います。

第五に、帰属情報の欠如です。ロードはシステム全体でスカラー値 1 つです。どのコンテナ、どのサービス、どの資源が原因なのか何も教えてくれません。アラートを設定することはできますが、そのアラートでできることは「入って見ろ」だけです。

ロードは高いのに CPU が暇な場合 — 診断の道筋

順序があります。上から下へ 3 分で終わります。

# ステップ 1: ロードの構成を確認 — R が少なく D が多ければ I/O 経路
vmstat 1 5
# procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
#  r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
#  2 22      0 512344  84120 6231044    0    0     0     0 2103 4102  4  3 21 72  0
#  1 23      0 511208  84120 6231180    0    0 132096   412 2288 4390  3  4 19 74  0
#  2 22      0 510992  84120 6231212    0    0 128512   380 2251 4301  4  3 20 73  0

b 列が 22〜23 です。この列こそが割り込み不可能スリープのタスク数です。wa (iowait) が 72% なのも同じことを語っています。iowait は「CPU がアイドルなのに、その CPU に I/O 待ちのタスクがある時間」なので、実はアイドル時間の一種です。高いこと自体が悪いわけではありませんが、b 列と一緒に見れば方向は明らかです。

# ステップ 2: どのデバイスが飽和しているのか
iostat -x 1 3
# Device      r/s     rkB/s  rrqm/s %rrqm r_await rareq-sz    w/s   wkB/s  w_await  aqu-sz  %util
# nvme0n1   1842.0   58944.0    0.0  0.00   12.85    32.00  210.0  3360.0     3.02   24.30  99.60
# nvme1n1      2.0      64.0    0.0  0.00    0.31    32.00    1.0    12.0     0.22    0.00   1.20

読むべきものは 3 つです。%util が 100% に張り付いているか (デバイスが休めていない)、r_await/w_await が普段の何倍か (リクエストあたりの遅延)、aqu-sz がどれだけ深いか (キューに溜まった平均リクエスト数)。NVMe では %util 100% が並列処理のせいで飽和を意味しないことがあるので、判断は await とキュー深度で行ってください。

# ステップ 3: D 状態のプロセスを個別に確認
ps -eo state,pid,ppid,comm --no-headers | awk '$1 == "D"' | head
# D  4821  4102 postgres
# D  4822  4102 postgres
# D  7710     1 kworker/u33:2
# ステップ 4: そのプロセスがカーネルのどこで止まっているのか (root 権限が必要)
sudo cat /proc/4821/stack
# [<0>] io_schedule+0x46/0x80
# [<0>] folio_wait_bit_common+0x131/0x330
# [<0>] filemap_fault+0x5f0/0xa50
# [<0>] __do_fault+0x39/0x120
# [<0>] handle_mm_fault+0xd4e/0x1090

スタックの最上段が io_schedule ならブロック I/O、rpc_wait_bit_killablenfs_wait_on_request が見えれば NFS、__lock_page 系ならページキャッシュの競合です。/proc/PID/stack が空だったり権限エラーになったりする場合は、CONFIG_STACKTRACE がないカーネルか、権限が足りないかのどちらかです。

NFS が疑わしければマウントの統計を直接見ます。

# サーバーごとの RTT と再送を確認
nfsstat -c | head -20
mountstats --nfs /mnt/shared 2>/dev/null | grep -A3 'READ:'

# 応答しないサーバーを探す (ハードマウントは無限に待ちます)
grep nfs /proc/mounts
# 10.0.9.4:/export/data /mnt/shared nfs4 rw,hard,proto=tcp,timeo=600,retrans=2 0 0

hard マウントはサーバーが戻るまでプロセスを D 状態で掴んだままにします。この状態では kill -9 も効きません。運用の観点では softerr や短い timeo を検討しつつ、データ整合性の要件を先に確認する必要があります。

ロードは低いのに遅い場合 — ロードが見られないもの

逆の方向もよくあります。ロードは 0.8 なのに p99 が 3 秒です。このときロードが構造的に見られないものを並べると、候補が絞られます。

  • 割り込み可能な待ち(S 状態)はロードに含まれません。ソケットの応答を待つ時間、つまり下流の API や DB の遅延はすべてここに該当します。アプリケーションが遅い理由の多くはこちら側です。
  • ユーザー空間のロック競合もほとんどが S 状態です。futex の待ちは割り込み可能なので、どれほどひどくてもロードは上がりません。
  • cgroup の CPU スロットリングは先ほど述べたとおり、ランキューからタスクを外すため見えません。
  • シングルスレッドのボトルネックは定義上ロード 1 です。コアが 64 個あってもロードは 1 を超えません。
  • ハイパーバイザのスティールタイムは CPU を奪われたのであって、待ち行列が長くなったわけではありません。

この 5 つを切り分けるコマンドは次のとおりです。

# S 状態で何を待っているのか (ソケット待ちはここに出ないのでスタックで)
sudo cat /proc/9931/stack | head -5

# ロック競合: コンテキストスイッチが急増していないか
vmstat 1 3        # cs 列が普段の何倍か
pidstat -w -p 9931 1 3
# UID  PID   cswch/s nvcswch/s  Command
# 1000 9931  18422.0   34110.0  java

# スロットリング (cgroup v2)
cat /sys/fs/cgroup/cpu.stat
# nr_periods 86400
# nr_throttled 41287
# throttled_usec 2914837261

# steal
mpstat -P ALL 1 3 | tail -3

nvcswch/s (非自発的コンテキストスイッチ) が大きければ CPU の競合、cswch/s (自発的) が大きければロックや I/O 待ちで眠っては起きるパターンです。

PSI — ロードアベレージが答えられなかった問いに答える指標

ロードアベレージの根本的な問題は、待っているタスクの個数しか語らず、どれだけ長く止まっていたかを語らないという点です。カーネル 4.20 から入った PSI がまさに後者を測ります。

cat /proc/pressure/io
# some avg10=78.23 avg60=71.04 avg300=48.92 total=8172635241
# full avg10=61.17 avg60=55.83 avg300=37.10 total=6019283746

cat /proc/pressure/cpu
# some avg10=2.41 avg60=1.88 avg300=1.02 total=91827364
# full avg10=0.00 avg60=0.00 avg300=0.00 total=0

cat /proc/pressure/memory
# some avg10=0.00 avg60=0.00 avg300=0.00 total=142
# full avg10=0.00 avg60=0.00 avg300=0.00 total=88

読み方はこうです。

  • some は少なくとも 1 個のタスクがその資源のせいで止まっていた時間の割合です。
  • full は実行可能なすべてのタスクが同時に止まっていた時間の割合です。つまりその間、システムは何ひとつ有用な仕事をできませんでした。
  • avg10avg60avg300 はそれぞれ 10 秒、60 秒、300 秒の窓の百分率で、total は累積マイクロ秒です。
  • システム全体の cpufull は定義上意味がないため常に 0 です (カーネル 5.13 から行自体は出力されます)。

上の出力が語ることは明快です。I/O の full が 61% というのは、直近 10 秒のうち 6 秒以上システム全体が I/O のせいで止まっていたという意味です。ロード 24 という数字よりはるかに直接的な情報であり、何より失われたスループットへそのまま翻訳できます

PSI の 2 つ目の利点は cgroup 単位で存在することです。ロードアベレージにはない帰属情報です。

# どの cgroup が I/O で最も長く止まっていたか
for f in /sys/fs/cgroup/**/io.pressure; do
  v=$(awk '/^full/ {print $2}' "$f" | cut -d= -f2)
  printf '%6s  %s\n' "$v" "${f%/io.pressure}"
done 2>/dev/null | sort -rn | head -5
#  58.12  /sys/fs/cgroup/system.slice/postgresql.service
#   3.04  /sys/fs/cgroup/system.slice/containerd.service
#   0.00  /sys/fs/cgroup/user.slice

/proc/pressure がなければ、CONFIG_PSI が無効になっているか、CONFIG_PSI_DEFAULT_DISABLED=y でビルドされたカーネルのためブートパラメータ psi=1 が必要な場合です。

指標測るものD 状態の反映時間の窓cgroup 単位
load average待っているタスクの個数含む1/5/15 分の EMA不可
CPU 使用率CPU を実際に使った時間の割合含まない任意可能
iowaitアイドル CPU に I/O 待ちがあった分間接的任意不可
PSI cpu.someCPU のせいで止まっていた時間含まない10/60/300 秒可能
PSI io.fullI/O で全体が止まっていた時間事実上これ10/60/300 秒可能

おわりに — ロードアベレージは待ち行列の長さであって使用率ではありません

1 つだけ覚えるならこれです。Linux のロードアベレージはCPU を待つタスクと I/O を待つタスクを区別せずに合算した個数の指数移動平均です。ですからこの数字ひとつだけでは「CPU を増やすべきか」に答えられません。

実務のルールとしてまとめるとこうなります。

  1. ロードが高ければ、まず /proc/loadavg の 4 番目のフィールドと vmstat 1b 列を見ます。R と D の比率が方向を決めます。
  2. D が多ければ iostat -x の await と /proc/PID/stack へ降りていきます。CPU の増設は答えではありません。
  3. アラートはロードではなく PSI に設定します。cpu.some avg60io.full avg60 が閾値を超えるほうが、はるかに少ない誤検知で実際の遅延と相関します。
  4. ロードはダッシュボードに残しつつ、傾向の指標としてだけ使います。1 分の値が 5 分・15 分より大きければ上昇中である、読み取るのはちょうどそれだけです。
  5. コンテナ環境ならホストのロードはポッドの性能を説明できません。cgroup の cpu.stat*.pressure を見る必要があります。