Dev Study
← 性能分析の土台
レッスン 7 / 10·約 9 分·第13期 性能分析

使用率は、占有率でしかない

リクエストがアプリケーションに届くまで
  1. ブラウザURL入力
  2. DNS名前→IP
  3. ルータ経路選択
  4. ロードバランサ振り分け
  5. Nginx受け口
  6. アプリ処理今ここ
  7. DB保存

前回、利用率を上げると待ち時間が伸びることを見ました。今回はその「利用率」という数字が、何を数えて何を数えていないかを見ます。

まず結論

使用率が答えているのは「その期間のうち、何割の時間ふさがっていたか」だけで、「あとどれだけ入るか」は答えていません。

そう読めてしまうのは、ふさがっていた時間を数える仕組みが、同時に何本走っていたかを一切数えていないからです。

? 読む前に予測

あるサーバの CPU 使用率が 20% と出ています。この数字から「まだ8割の余力がある」と言えるでしょうか。

当てることが目的ではありません。外した予測ほど記憶に残るので、直感で選んでください。

抽象原理

この数字が答えている問い

前回出てきた「利用率」を、今回は指標として解剖します。

使用率の定義は、どの層でも同じ形です。ある期間を区切り、そのうち「ふさがっていた」時間が何割だったかを出す。

この作りは、2つのことを記録していません。

  • ふさがっていた間に、何本が同時に走っていたか。 1本でも64本でも「ふさがっていた」は同じ1つの状態です。
  • 枠がどこまでなのか。 割合は分母を決めた瞬間に決まりますが、その分母がこちらの知りたい枠と同じとは限りません。

分散編『見えるのは遅れた信号だけ — 他ノードの状態は直接には測れない』で、CPU 使用率は捌けているかどうかの代理だと書きました。使用率は正しく時間を数えています。ただし数えているのは占有時間であって、余力ではありません。

具体実物

ディスクの %util は、本数を数えていない

iostat が出す %util は /proc/diskstats の Field 10 から計算されます(sysstat v12.8.0 のソースで確認)。Field 10 の意味は、kernel の Documentation/admin-guide/iostats.rst が次のように書いています。

Documentation/admin-guide/iostats.rst(逐語・抜粋。以下略)コマンド出力
Field 9 -- # of I/Os currently in progress
The only field that should go to zero. Incremented as requests are
given to appropriate struct request_queue and decremented as they finish.
 
Field 10 -- # of milliseconds spent doing I/Os (unsigned int)
This field increases so long as field 9 is nonzero.
Since 5.0 this field counts jiffies when at least one request was
started or completed. If request runs more than 2 jiffies then some
I/O time might be not accounted in case of concurrent requests.

This field increases so long as field 9 is nonzero. — Field 10 が見ているのは、Field 9 がゼロか否かだけです。処理中の I/O が1本でも64本でも、同じ速さで増えます。

だから %util は「1本以上走っていた時間」しか数えていません。何本走っていたかを掛けて数えているのは Field 11(weighted)の方で、こちらが aqu-sz の元です。見たいものによって、読むフィールドが違います。

最後の一文も重要です。some I/O time might be not accounted in case of concurrent requests — 並行要求では I/O 時間が計上漏れしうると、カーネル自身が書いています。

✓ ここまでの確認

Linux の `/proc/diskstats` の Field 10(I/O に費やしたミリ秒)は、同時に 64 本の I/O が走っているときどう増えますか。

抽象原理

占有率から余力を引き算できない理由

一般化します。占有率から余力を出す計算は、2つの仮定を黙って置いています。

  1. ふさがっていた時間の割合が、詰め込める仕事の割合と対応していること
  2. 分母の範囲が、こちらの知りたい枠と一致していること

1 が崩れるのが並列に処理するデバイス、2 が崩れるのが範囲の違う場所から読んだときです。

「ふさがっていた時間」から余力を引く途中で、何が落ちているか
記録されたものふさがっていた時間の割合
ここで2つが落ちる
同時本数1本でも64本でも同じ1状態
枠の範囲分母が知りたい枠とは限らない
落ちたまま引き算する
余力という結論根拠が途中で抜けている
  • ・数字は誤っていない。答えている問いが違う
  • ・本数は Field 11 系、枠は cgroup 側に別に書かれている

第12週『OSの役割 — 1つのCPUを、みんなで分け合う』で、CPU 使用率が100%を超えて見えるのは数え方の話だと見ました。分母を決めているのは誰かを確かめないと、割合は読めません。

具体実物

使用率20%のまま、仕事量が半分になる

範囲が食い違うと何が起きるかを測りました。測定環境は Docker Desktop の Linux VM(カーネル 6.10.14-linuxkit、aarch64)上の --cpus=2 コンテナで、VM 自体は 10 CPU を持ちます。回り続けるプロセス(スピナー)を増やしながら、中から読んだ /proc/stat 由来の使用率、こなしたループ回数、cgroup の cpu.stat が持つ止められた時間を同時に取りました。

測定1回・Docker Desktop の Linux VM 上の --cpus=2 コンテナ(work units はループ回数で、秒でも命令数でもない)コマンド出力
cgroup cpu.max = 200000 100000 (quota period -> 2.0 CPUs)
/proc/stat reports 10 CPUs (the VM's, not the container's)
 
spinners /proc/stat %busy work units throttled_usec delta nr_throttled delta
1 10.5% 83804478 0 0
2 20.2% 162524627 7223 9
4 20.5% 147266659 8073352 41
8 21.3% 82550941 23972547 40

スピナーを2本から8本に増やしても、使用率は 20.2% から 21.3% までしか動いていません。一方で仕事量は 162,524,627 から 82,550,941 へ、ほぼ半分に落ちています。 その裏で cgroup の throttled_usec は 23.97 秒分増えていました(区間の長さは未記録で、割合は読めません)。

使用率が動かない直接の原因は、分母です。 中から読んだ /proc/stat は VM の 10 CPU を見せており、コンテナの枠が 2 CPU であることを知りません。1本で 10.5% は 10 分の 1 で、この分母と一致します。2 CPU を使い切っても 10 分の 2 = 20% にしかならず、そこから先は増えようがありません。

ただし分母が説明するのは使用率が 20% で止まることまでで、仕事量が半分に落ちた原因は説明できません。この測定では特定していません。

「10 CPU のうち何割がふさがっていたか」に正しく答えています。こちらが「2 CPU の枠にあとどれだけ入るか」を尋ねたつもりでいただけです。 なお nr_throttled は 41 回から 40 回とほぼ同じで、回数だけを見ると「変わっていない」と読めます。増えたのは時間の方です。

VM 上のコンテナでの1回の測定です。一般に CPU 使用率が当てにならない、という話には広げられません。 言えるのは、その数字がどの範囲を数えているかを知らないと余力の話はできない、というところまでです。

✓ ここまでの確認

コンテナの中から見た CPU 使用率が 20% のまま、仕事量だけが半分に落ちました。最も妥当な読み方はどれですか。

⇄ 2つを見比べる
具体例 A

ディスクの %util(並列に処理するデバイス)

元になる Field 10 は、処理中の I/O 数が非ゼロである限り増えます。1本しか走っていなくても、時間が埋まれば 100% に届きます。100% を飽和と読めるのは直列に処理するデバイスに限る、と公式 man 自身が書いています。

同じ100%が意味しうること表
同時に走っていた本数 Field 10 の増え方 %util
1本 増える 100%になりうる
64本 同じ速さで増える 100%になりうる
具体例 B

コンテナの中から見た CPU 使用率

コンテナの中から読んだ /proc/stat は VM の CPU を分母にしています。枠を使い切っても、分母が大きいぶん割合は小さいままです。止められた時間は cpu.stat にあり、使用率の内訳には入りません。

本教材の測定(--cpus=2 のコンテナ、1回)表
スピナー 使用率 仕事量(ループ回数) 止められた時間
2本 20.2% 162524627 7223 usec
8本 21.3% 82550941 23972547 usec

ディスクの %util と、コンテナから見た CPU 使用率。測っている対象も単位も違いますが、共通する構造は何でしょうか。

どちらも「100%なら限界」「小さいなら余っている」と読みたくなります。その読み方が、記録されていない何を前提にしているかを考えてみてください。

この章のまとめ

  • 使用率が答えているのは「その期間のうち何割の時間ふさがっていたか」だけ。余力は答えていない。
  • %util の元である Field 10 は、処理中の I/O 数が非ゼロである限り増える。1本でも64本でも同じ速さで増え、本数を一切数えていない。本数を掛けて数えているのは Field 11。
  • sysstat 公式 man は、100% を飽和と読めるのは要求を直列に処理するデバイスに限るとし、RAID アレイや modern SSD では性能限界を反映しないと書いている。
  • /proc/stat が持つのは各状態で過ごした時間だけで、余力を表す欄は無い。
  • 本教材の測定(VM 上の --cpus=2 コンテナ、1回)では、使用率 20% のまま仕事量が半減した。20% で止まる原因は分母の範囲の違いで、指標の欠陥ではない。半減の原因は未特定。

自分の現場で確かめる

読んだだけでは身につきません。手元の環境で実際に見てみてください。

環境:

理解度チェック

読んだ直後は「分かった」と感じやすいものです。実際に解いて確かめてください。

問題 1 / 4正解: 0

`iostat` の `%util` が 100% に近いとき、デバイスが飽和していると言えるのはどんな場合ですか。

用語集(5語)
使用率(占有率)
ある期間のうち、対象がふさがっていた時間の割合。その間に何本の要求が走っていたかは含まない。
%util
iostat が出すディスクの値。sysstat 公式 man では「経過時間のうち I/O 要求がデバイスへ発行されていた時間の割合」とされ、100% が飽和を意味するのは要求を直列に処理するデバイスに限ると明記されている。
Field 9 / Field 10 / Field 11
`/proc/diskstats` のフィールド。Field 9 は処理中の I/O 数、Field 10 は I/O に費やしたミリ秒(Field 9 が非ゼロである限り増える)、Field 11 は本数で重み付けしたミリ秒。`%util` の元は Field 10。
USER_HZ
`/proc/stat` の CPU 時間の単位。`proc_stat(5)` は「多くのアーキテクチャで1/100秒」とし、正確な値は `sysconf(_SC_CLK_TCK)` で取るよう書いている。
throttling(cgroup)
cgroup の CPU 枠を使い切った実行単位が、期間の残りを強制的に止められること。止められた時間は `cpu.stat` の `throttled_usec` に記録される。