今回から、LinuxカーネルのPSI機能について紹介していきます。
この連載では、Linuxカーネルに実装されているコンテナ関連の機能を紹介してきました。今回から紹介するPSI機能自体は、コンテナ専用の機能ではありません。しかし、cgroup v2と連携でき、cgroupfs内に関連するファイルが出現しますので、コンテナ環境でも役に立つ機能でしょう。
今回は、このPSI機能の概要がつかめるように説明していきます。
今回の実行例は、特に断りのない限りUbuntu 26.
PSI
PSIは"Pressure Stall Information"の略で、CPU、メモリ、I/
PSIで得られる情報は、システムが次のような状態になり、タスクがリソースを待機している時間の割合です。
- リソースが不足し、タスクの実行が停止したり遅延したりしている状態
- タスクがリソース待ちの状態になっており、実行が停止したり遅延したりしている状態
このような状態をストール
それぞれのリソースでは、表1のような状態がストールに相当するでしょう。
| リソース | 各リソースでストールの原因となる状態 |
|---|---|
| CPU | 実行可能状態のタスクがCPU時間を待っている状態 |
| メモリ | メモリ割り当てに失敗したり、スワップインを待ったりしている状態など |
| I/ |
ストレージからのI/ |
PSIを使うと、リソース不足に関する情報を得られます。この情報をカーネルから得られる他の情報やユーザー空間のツールと組み合わせれば、リソース不足を細かく検出したり予測したりして、問題にすばやく対応できるようになるでしょう。
また、cgroup v2と連携することで、コンテナ環境や、ワークロードごとにcgroupが分かれているような場合に、より細かくリソースを監視できます。
PSI機能は、4.
| カーネルバージョン | 変更点 |
|---|---|
| 4. |
PSI導入 |
| 5. |
しきい値超過モニタリング機能追加 |
| 5. |
cgroupごとのcpuにfull導入 |
| 5. |
しきい値超過モニタリングがCAP_を持つユーザに許可されるようになった |
| 5. |
cgroupごとのPSI機能を起動パラメータで制御可能になった |
| 5. |
システム全体のCPUのPSIで、fullを常に0にする修正 |
| 6. |
割り込み処理 |
| 6. |
しきい値超過モニタリングが非特権ユーザにも許可されるようになった |
PSIの算出
PSIはファイルから取得します。PSIの値が格納されたファイルの内容は次のようなフォーマットです。
some avg10=0.00 avg60=0.00 avg300=0.00 total=0
full avg10=0.00 avg60=0.00 avg300=0.00 total=0
このように、someとfullという2行があります。
この2項目はそれぞれ次の内容を表しています。
some: 対象となるリソースにおいて、少なくとも1つのタスクがリソース不足のためストールした時間の割合full: 対象となるリソースにおいて、アイドル状態ではないすべてのタスクが同時にリソース不足のためストールした時間の割合
PSIファイルを見ると、someとfullについてそれぞれいくつかの値があります。これらの値は、それぞれ単位時間の中でどれだけタスクがストールしていたかを表します。
avg10は10秒、avg60は60秒、avg300は300秒の間で何%ストールしていたかを表しています。totalは合計のストール時間で、単位はマイクロ秒です。
これらの値は2秒ごとに集計されますので、それより短い周期でこれらのファイルを読む意味はありません[1]。
この10秒、60秒、300秒のそれぞれに表示される値の計算方法を見てみましょう。
図1は、システム上で2つのタスクのみが並行して実行されている様子を表しています。このうち、"Task A"は60秒間ストールすることなく実行されましたが、"Task B"はリソース不足のため30秒ストールしました。
このとき、"Task B"だけがストールしたので、someが50%になります。"Task A"はストールすることなく実行されていますので、fullは0%です。
図2は、"Task B"が図1と同様に30秒間ストールし、そのうち30秒のうち10秒間は"Task A"もストールした様子を表しています。つまり、60秒の間にすべてのタスクが10秒間ストールしたということです。
このとき、someは図1と同様に50%となります。fullは"Task A"と"Task B"両方がストールした時間の割合ですので、16.
図1が示すのは、一部のタスクについて実行時間が延びる可能性がある状態です。それに対して、図2のようにfullの値が0でない場合は、システム全体でリソース不足が起こっており、システム全体のスループットが低下していることを示します。
複数CPUの場合の算出モデル
PSIの考え方は、ここまで説明したとおりそれほど複雑ではありません。ただし、CPUが複数ある場合の計算は少しイメージしづらいでしょう。そこで、カーネルがどのようなモデルにもとづいて計算しているのかを紹介します。
このモデルについては、カーネルソースのkernel/の冒頭にコメントがあります。
threads = min(アイドル状態ではないタスク数, CPU数)
SOME = min(遅延しているタスクの数 / threads, 1)
FULL = (threads - min(生産的なタスクの数, threads)) / threads
「生産的なタスク」
- I/
Oの場合、実行中のタスク - メモリの場合、回収処理中ではない実行中のタスク
- CPUの場合、CPU上にある
(=実際にCPUで実行状態である) タスク
を意味します。
たとえば、256個のCPU上で257個の数値計算タスクを動かす場合は、次のようになります。
threads = min(257, 256)
SOME = min(1 / 256, 1) = 0.4%
FULL = (256 - min(256, 256)) / 256 = 0%
4つのタスクのうち1つがメモリ待ちで遅延している場合は、次のようになります。
threads = min(4, 4)
SOME = min(1 / 4, 1) = 25%
FULL = (4 - min(3, 4)) / 4 = 25%
実際の実装についても、psi.のコメントで説明がありますので、興味のある方はご覧になってみてください。
割り込み処理のPSI
割り込み処理のPSIは、他のリソースの値とは少し意味合いが違います。
irqファイルから得られるPSIは、割り込み処理
割り込み処理のPSIは、割り込み処理がなければタスクが実行に使えたはずの時間のうち、実際には割り込み処理によって奪われた時間の割合を示します。このように導出する値のため、someに相当する概念は定義されておらず、irqファイルにはfullの行のみが存在します。
図3の場合、60秒のうち、10秒が割り込み処理に使われているので、16.
システム全体のPSIの取得
システム全体のPSIは、/proc/ディレクトリー以下のファイルから取得できます。
$ ls /proc/pressure/ cpu io memory $ cat /proc/pressure/cpu some avg10=5.48 avg60=3.00 avg300=0.75 total=2314283 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=481 full avg10=0.00 avg60=0.00 avg300=0.00 total=481 $ cat /proc/pressure/io some avg10=0.66 avg60=0.52 avg300=0.14 total=522892 full avg10=0.40 avg60=0.29 avg300=0.07 total=330247
この例のように、CPU、メモリ、I/
このうち、システム全体のCPUに対するfullの情報は、5.cpuファイルにはfullの行はありません。
cpuファイルにfullが表示されるようになったのは、5.cpuファイルにfullが表示されるようになってしまいました。
しかし、システム全体で全タスクが同時にCPUを待つという状態は存在しないはずです。システム全体のCPUに対するfullは定義されていません。何を表す値なのかもわからず、表示自体に意味がないため、混乱を招くだけでした。
そのため、5.cpuファイルのfullにはすべて0を表示するように修正されました。行そのものを削除しなかったのは、一度表示してしまった行を急に消すと、ファイルを読んでいるプログラムの誤動作を引き起こす可能性があるためです。後方互換性を考慮したわけです。
したがって、cpuファイルのfull行は5.0が表示されます。5.
6.CONFIG_が有効になっている場合は、これらのファイルの他にirqというファイルが追加されます。次に示すのは、7.CONFIG_を有効にし、ソースからビルドしたカーネル環境での実行例です。
$ uname -r 7.1.3-plamo64 (7.1カーネル環境) $ ls /proc/pressure/ cpu io irq memory (irqが存在)
irqファイルにはfullの行しかありません。
$ cat /proc/pressure/irq full avg10=0.00 avg60=0.00 avg300=0.00 total=6928825
PSIの動き
ここからは、実際に/proc/以下のファイルを読み、システム全体のPSIの動きを追ってみましょう。
なおシステム全体のPSIは、システムのスペック、システム上で動いているプロセスなど、さまざまな要因によって変化します。このため、
システムの負荷を上げると、PSIがそれに応じて変化する様子を見てもらうための実験です。得られた値はあくまで一例ですので、値ではなく動きのほうに注目してご覧いただければと思います。
メモリ
まずはメモリに負荷をかけてみましょう。連載でこれまでも使ったstress-ngコマンドでメモリを確保します。
テスト環境は4GBのメモリを持つVMです。次のように--vm 1を指定し、負荷をかけるワーカーを1つだけ起動します。あわせて--vm-hang 0を指定して、一度確保したメモリを確保しつづけるようにします。
$ stress-ng --vm 1 --vm-bytes 3700M --vm-hang 0 --timeout 120s
結果を見てみましょう
まずこのシステムは、stress-ng以外がほとんど動いていないため、someとfullはほぼ同じ値になりました。
負荷をかけると同時にavg10の値が上がり、その後は急激に下がって0に近づきます。avg60とavg300の値はゆっくり立ち上がり、大きくはなりません。avg10の値が大きい状態が長く続かないため、長期間の値が大きくならないのは当然です。
このような結果になる理由は、--vm-hang 0を指定し、確保したメモリを保持しつづけているからです。最初にメモリを確保した時点では負荷がかかりますが、その後は負荷がかかりません。このため、avg10の値は急激に下がっていきます。最初に負荷がかかってストールした時間がavg60とavg300に影響するため、これらの値はゆっくり下がっていきます。
次にstress-ngコマンドに--vm-hang 10を指定し、確保したメモリを10秒後に解放して、再度確保します。
$ stress-ng --vm 1 --vm-bytes 3700M --vm-hang 10 --timeout 120s
結果を見てみましょう
こちらも--vm 1を指定していますので、someとfullの値はほぼ同じになりました。
--vm-hang 10を指定したため、定期的にメモリを確保し、そのたびにメモリ負荷が上がります。avg10の値に定期的に山があることがわかります。avg60とavg300の値は長い時間でならされますので、値の上下は少ないです。定期的に負荷がかかるので、--vm-hang 0のときの結果と違い、avg60とavg300の値は下がりません。
CPU
次はCPUです。
CPUの実験にもstress-ngコマンドを使用します。stress-ngでは--cpuで指定した数だけ、CPUに負荷をかけるワーカーを起動します。
そこでまずは、CPU数と同じワーカー数を指定してCPUに負荷をかけてみます。その後、ワーカー数をCPU数より多くしていき、PSIの値がどう変わるかを見てみましょう。
今回使用している環境はCPU数が2です。
$ grep processor /proc/cpuinfo processor : 0 processor : 1
そこで、--cpu 2として、ワーカー数2で実行します。
$ stress-ng --cpu 2 --timeout 60s
結果を見てみましょう
このように実行すると、CPU使用率はいずれも100%近くに張り付きます。しかし、タスクはほぼストールせずに動作しますので、PSIの値は小さいままです。
次は実験環境のCPU数より1つ多いワーカー数となる--cpu 3を指定して実行しました
図7のように、avg10の値がほぼ50%付近で頭打ちになっていることがわかります。
この実験を先に説明したモデルに代入してみましょう。
threads = min(3, 2) = 2
SOME = min(1 / 2, 1) = 50%
このように、実行例とモデルが一致していることがわかります。
次に実験環境のCPU数よりかなり多い--cpu 6を指定して実行しました
図8のように、avg10の値が100%付近まで上がっていることがわかります。
threads = min(6, 2) = 2
SOME = min(4 / 2, 1) = 100%
モデルに代入すると、確かに100%になります。
なお先に説明したとおり、CPUのfullは常に0ですので、グラフにはプロットしていません。
I/O
I/fioコマンドで実験しました。実行したコマンドは次のとおりです。
$ fio -filename=/data/testfile -direct=1 -rw=randwrite -bs=4k -numjobs=1 -time_based -runtime=30 -size=100M -name=psi_test_1
このうち、同時にワークロードを実行するジョブ数である-numjobsの値を1、2、4で順に変化させました。
まずは-numjobs=1を指定したときの結果です
図9のように、単独のタスクでもavg10の値は50%以上上がっており、この環境ではストールするsomeとfullの値はほぼ同じです。このホスト上ではfioコマンド以外のタスクを実行していませんので、納得できる結果でしょう。
次に-numjobs=2を指定したときの結果です
図10のように、PSIの値については-numjobs=1のときと大きく変わりません。ただし、-numjobs=1のときはsomeとfullの値がほぼ同じでしたが、-numjobs=2になると、少し乖離していることがわかります。
2つのタスクは、それぞれストールはしているものの、常に同じタイミングでストールしているわけではないことがわかります。
次に-numjobs=4を指定したときの結果です
図11でavg10の値を見ると、someは-numjobs=1や-numjobs=2のときよりは大きくなっています。タスクの数が増えたため負荷が上がり、ストールすることが多くなったことがわかります。一方でfullは、-numjobs=1や-numjobs=2のときより下がっていることがわかります。これは、単独ではストールするものの、全タスクが同時にストールする時間が減っていることを示します。
この結果から、この環境で-numjobs=4程度まで負荷を上げた場合、次のような状況であるといえるでしょう。
- タスク数が増えたことで、同時に発行されるI/
Oの数が増え、個々のタスクがストールする機会は増えた (= someの上昇) - しかし、タスクごとにI/
O発行・ 完了のタイミングがずれるため、 「全タスクが寸分違わず同時にストールする」 という状況は起きにくくなった (= fullの低下) - つまりストールは発生しているが、あるタスクがストールしている間に別のタスクは処理が進んでおり、全体としては並行して処理が進んでいる
割り込み処理
最後に割り込み処理のPSIの動きを見ておきましょう。
ここまでの実験で使ってきたUbuntu 26.CONFIG_が有効になっていないため、irqファイルがありません。そこで、割り込み処理の実験では、筆者がメンテナを務めているPlamo LinuxをインストールしたVM環境を使いました。このVM上で、CONFIG_を有効にしてビルドした7.
割り込み負荷は、stress-ngコマンドで発生させました。次のように--timerオプションを使用し、高頻度のタイマーイベントを発生させ、タイマー割り込みを生成しました。--timer-freq 100000で、タイマーを1秒間に100,000回発生させました。
$ stress-ng --timer 1 --timer-freq 100000 --timeout 60s
結果は図12のようになりました。
stress-ngコマンドの実行直後から値が上がり、実行停止とともに下がっていく様子がわかります。
まとめ
今回は、PSI機能の実装がカーネルでどのように進んだのかを紹介しました。そして、PSIの算出方法を説明したうえで、実際にホストに負荷をかけてPSIを取得しました。
先にも書いたとおり、PSI機能では、cgroupで制限値を設定した場合のように
実際にシステムを運用する環境では、今回の実験のように単一のリソースに負荷がかかっているようなケースは珍しいはずです。複数のリソースに負荷がかかり、あるリソースの負荷が他のリソースにも影響を与えるような場合がほとんどです。そのような場合にも、それぞれのPSIを取得することで、リソース不足の様子を細かく検出して対応したり、リソース不足を予測したりできるでしょう。
次回はcgroup v2とPSIの連携について紹介する予定です。
参考文献
- PSI - Pressure Stall Information (カーネル付属文書)
- Using PSI (Engineering at Meta)
