第56回から第60回までで、メモリコントローラを使ってメモリの使用量やOOM Killerの動作を制御する方法を説明しました。
メモリコントローラを設定することで、制限値を上回ったり、保証値を下回ったりした場合、すべてcgroupに処理を任せておいてよいわけではなく、イベントの発生を監視して対処しなければならない場合もあるでしょう。
今回は、メモリコントローラ関係のイベントを監視する際に使えるファイルについて説明します。
memory.eventsファイル
メモリコントローラが有効になったcgroupには、表1のとおり、memory.とmemory.というファイルがあります。これらのファイルから、cgroupで起こったメモリコントローラ関係のイベント回数がわかります。
| ファイル名 | 機能 | 操作 | 実装されたバージョン |
|---|---|---|---|
memory. |
cgroupとその子孫のcgroupで起こったイベント数 | 読み取り | 4. |
memory. |
memory.と内容は同じだが、ファイルが存在するcgroup内のイベント数のみを表示する |
読み取り | 5. |
これら2つのファイルの中身は同じで、イベント名とそのイベントが発生した回数がスペース区切りで1行に書かれています。メモリコントローラで発生するイベントは表2のとおりです。
| イベント名 | イベントの説明 |
|---|---|
low |
タスクのメモリ使用量がmemory.で指定した値を下回っているにも関わらず、高いメモリ圧力によりメモリが回収された。このイベントが発生しているということはメモリ保護の設定がオーバーコミットされていることを示す |
high |
タスクのメモリ使用量がmemory.の値を超過したためにメモリ回収が実行された |
max |
タスクのメモリ使用量がmemory.を超えようとした |
oom |
タスクのメモリ使用量が制限に達して割り当てが失敗しそうになった |
oom_ |
OOM Killerによってタスクが強制終了させられた |
oom_ |
cgroup単位のOOMが発生した。memory.が1に設定されている場合にcgroup全体のタスクがOOMによってkillされた |
2つのファイルの違いは、memory.はファイルが存在するcgroupとその子孫のcgroupで起こったイベントの回数を表示し、memory.はファイルが存在するcgroup内のローカルイベントの回数のみを表示することです。
実際に動作を見る前に、cgroup v2のマウントオプションを確認しておきます。
$ grep cgroup2 /proc/self/mounts cgroup2 /sys/fs/cgroup cgroup2 rw,nosuid,nodev,noexec,relatime,nsdelegate,memory_recursiveprot 0 0
マウントオプションの中にmemory_が含まれないことを確認してください。通常はmemory_オプションは指定されていないはずです。memory_オプションについては、あとで説明します。
マウントオプションが確認できたところで、実際のmemory.とmemory.ファイルの動作を確認していきましょう。
memory.ファイルとmemory.ファイルの動作を確認するために、test01 cgroupと、その子であるtest011 cgroupを作成して、プロセスはtest011 cgroupに所属させ、memory.を超えるメモリを消費させてOOM Killerを発動させてみます。
これまでのように、設定したmemory.を超える値を指定してstress-ngコマンドを実行します。コマンド実行前には、OOM Killerを発動させるために、スワップをオフにします。
$ sudo mkdir -p /sys/fs/cgroup/test01/test011 (test01とその子のtest011を作成) $ echo $$ | sudo tee /sys/fs/cgroup/test01/test011/cgroup.procs (シェルをtest011に所属させる) 1008 $ echo "+memory" | sudo tee /sys/fs/cgroup/cgroup.subtree_control (test01でmemoryコントローラを使えるようにする) +memory $ echo "+memory" | sudo tee /sys/fs/cgroup/test01/cgroup.subtree_control (test011でmemoryコントローラを使えるようにする) +memory $ echo 192M | sudo tee /sys/fs/cgroup/test01/test011/memory.max (test011でメモリ上限を192MBに設定) 192M $ sudo swapoff -a (スワップをオフ) $ stress-ng --vm 1 --vm-bytes 256M --vm-hang 0 -v (256MBメモリを消費する設定でコマンドを起動) :(略) stress-ng: debug: [1112] vm: child died: signal 9 'SIGKILL' (instance 0) stress-ng: debug: [1112] vm: assuming killed by OOM killer, restarting again (instance 0)
192MBに上限を設定したcgroupで、メモリを256MB消費するようにコマンドを実行したので、OOM Killerが発動し、プロセスがkillされています。
それでは、各cgroupのmemory.とmemory.ファイルを確認してみましょう。
まずは、プロセスが所属したtest011 cgroupです。
$ cat /sys/fs/cgroup/test01/test011/memory.events low 0 high 0 max 771 oom 7 oom_kill 7 oom_group_kill 0 $ cat /sys/fs/cgroup/test01/test011/memory.events.local low 0 high 0 max 771 oom 7 oom_kill 7 oom_group_kill 0
memory.を超えようとした回数を示すmax行が0ではなく、memory.を超えたことがわかります。そして、OOM Killerが発動していますので、oom、oom_に7という数字が入っています。
次に、test011 cgroupの親cgroupであるtest01 cgroupで両方のファイルを確認してみましょう。
$ cat /sys/fs/cgroup/test01/memory.events low 0 high 0 max 771 oom 7 oom_kill 7 oom_group_kill 0 $ cat /sys/fs/cgroup/test01/memory.events.local low 0 high 0 max 0 oom 0 oom_kill 0 oom_group_kill 0
このように、memory.ファイルには子孫であるtest011 cgroupで発生したイベントがカウントされています。一方で、test01 cgroupではイベントが起こっていませんので、memory.ファイルはすべての値が0のままです。
memory.oom.groupファイルとmemory.eventsファイル
ここまでの実行例は、第60回で紹介したmemory.が0に設定された状態で実行していました。
5.oom_というイベントは定義されておらず、memory.ファイルが1に設定された状態でOOM Killerが発動したときも、oom_イベントだけが発生していました。
oom_イベントだけでも、memory.、memory.ファイルとmemory.の設定を見れば、単独のプロセスがkillされたのか、cgroupに所属するプロセスがまとめてkillされたのかはわかることが多いでしょう。
しかし、子孫のcgroupを持つツリーの中間に存在するcgroupでoom_イベントが発生しても、killされたプロセスが単独のプロセスなのか、cgroup内のプロセスすべてなのか、また、イベントが発生したのが自身なのか、子孫なのかなどが明確にわかりません。
そこで、5.oom_イベントが定義され、memory.を1に設定したことによるOOM Kill発動が明確にわかるようになりました[1]。
oom_の値がカウントされることを、実際に試して確認しておきましょう。
$ sudo mkdir -p /sys/fs/cgroup/test01/test011 $ echo $$ | sudo tee /sys/fs/cgroup/test01/test011/cgroup.procs 1459 $ echo "+memory" | sudo tee /sys/fs/cgroup/cgroup.subtree_control +memory $ echo "+memory" | sudo tee /sys/fs/cgroup/test01/cgroup.subtree_control +memory $ echo 192M | sudo tee /sys/fs/cgroup/test01/test011/memory.max 192M $ echo 1 | sudo tee /sys/fs/cgroup/test01/test011/memory.oom.group 1 $ sudo swapoff -a $ stress-ng --vm 1 --vm-bytes 256M --vm-hang 0 -v
stress-ngコマンドでメモリを256MB使用するように指定して実行すると、test011 cgroup内のプロセスがすべてkillされますので、起動していたシェル自体もkillされてしまいます。別のシェルから、test011 cgroupのmemory.とmemory.ファイルを確認してみます。
$ grep oom_group_kill /sys/fs/cgroup/test01/test011/memory.events oom_group_kill 1 $ grep oom_group_kill /sys/fs/cgroup/test01/test011/memory.events.local oom_group_kill 1
このようにtest011 cgroupでは、両方のファイルでoom_が1となっており、cgroup内のプロセスがまとめてkillされるイベントが発生したことがわかります。
このとき親cgroupであるtest01 cgroupでは、memory.ファイルのoom_は1になっています。一方で、自身のcgroup内のイベントだけをカウントするmemory.ファイルのoom_は0のままであることも確認できます。
$ grep oom_group_kill /sys/fs/cgroup/test01/memory.events oom_group_kill 1 $ grep oom_group_kill /sys/fs/cgroup/test01/memory.events.local oom_group_kill 0
メモリコントローラの状態通知
ここまでで説明したように、memory.ファイルやmemory.ファイルの中を見ると、メモリコントローラに関係するイベントの発生回数を知ることができます。
cgroupを使って、メモリの消費状況を監視する場合、メモリコントローラでイベントが発生したことを知ることができると便利です。メモリコントローラでは、第39回で紹介したcgroup.ファイルと同様に、memory.ファイルとmemory.ファイルの中身が変化したときに通知を受け取れます。
カーネル付属ドキュメントのmemory.の項には、"the file modified event can be generated"とだけ書かれています。memory.ファイルとmemory.ファイルでも、cgroup.ファイルと同様にinotify(7)や、poll(2)を使ってイベントを受け取れます。
cgroup(7)を見ると、
The cgroup.
events file can be monitored, in order to receive notification when the value of one of its keys changes. Such monitoring can be done using inotify(7), which notifies changes as IN_ MODIFY events, or poll(2), which notifies changes by returning the POLLPRI and POLLERR bits in the revents field.
と書かれています。
inotifyの場合はIN_イベントを受け取ることにより、pollの場合はreventsにセットされるPOLLPRIやPOLLERRビットをチェックすることでメモリに関するイベントが受け取れます。
poll(2)のPOLLPRIの項目には、
A cgroup.
events file has been modified (see cgroups(7)).
と書かれており、cgroup関連のファイル変更ではPOLLPRIが設定されることが明記されています。
memory.ファイルやmemory.ファイルで、これらの通知が受け取れるかを、これまでと同様の実行例で確認してみます。
$ sudo mkdir -p /sys/fs/cgroup/test01/test011 $ echo $$ | sudo tee /sys/fs/cgroup/test01/test011/cgroup.procs 1127 $ echo "+memory" | sudo tee /sys/fs/cgroup/cgroup.subtree_control +memory $ echo "+memory" | sudo tee /sys/fs/cgroup/test01/cgroup.subtree_control +memory $ echo 192M | sudo tee /sys/fs/cgroup/test01/test011/memory.max 192M
ここで、別のシェルを起動し、inotifywaitコマンドでtest01 cgroupとtest011 cgroupのmemory.ファイルを監視します。
そして、stress-ngコマンドでmemory.の制限値を超えるメモリを指定して実行すると、OOM Killerが発動します。
$ sudo swapoff -a $ stress-ng --vm 1 --vm-bytes 256M --vm-hang 0 -v :(略)
このとき、test011 cgroupのmemory.ファイルとmemory.ファイルを指定したinotifywaitコマンドでは、次のような出力が受け取れました。
$ inotifywait /sys/fs/cgroup/test01/test011/memory.events (test011のmemory.eventsを監視) Setting up watches. Watches established. /sys/fs/cgroup/test01/test011/memory.events MODIFY
$ inotifywait /sys/fs/cgroup/test01/test011/memory.events.local (test011のmemory.events.localを監視) Setting up watches. Watches established. /sys/fs/cgroup/test01/test011/memory.events.local MODIFY
また、その親であるtest01 cgroupでも、次のように同様にイベントが受け取れました。
$ inotifywait /sys/fs/cgroup/test01/memory.events (test01のmemory.eventsを監視) Setting up watches. Watches established. /sys/fs/cgroup/test01/memory.events MODIFY
test01 cgroupのmemory.ファイルでは、自身で発生したOOM Killerではないので、イベントは発生していません。
$ inotifywait /sys/fs/cgroup/test01/memory.events.local (test01のmemory.events.localを監視) Setting up watches. Watches established. (待受状態のまま)
このように、memory.ファイルとmemory.ファイルを監視することで、メモリコントローラで発生したイベントを監視できることが確認できました。
memory_localeventsオプション
ここまでの説明で、memory.は階層構造の子孫で発生したイベントを扱い、memory.では自身のcgroupで発生したイベントのみを扱うことを説明しました。
しかし、実はcgroup v2が導入された当初はmemory.ファイルは、現在のmemory.ファイルのように動作していました。つまり、子孫のイベントは考慮していませんでした。
cgroup v2全体としては、たとえばmemory.ファイルなどは階層構造を考慮し、子孫の統計情報を含んで表示しているのに、memory.は子孫のイベントを考慮しないで表示することは、混乱の原因となります。
そこで、5.memory.は子孫で発生したイベントを扱うように変更されました[2]。
この変更は、本来あるべき姿への変更ですが、従来の動作との互換性は保たれません。そこで、従来どおりの動作を期待するユーザのために、cgroup v2をマウントする際のオプションとして、memory_オプションが追加されました。
memory_オプションを指定してcgroup v2をマウントすると、memory.ファイルは自身のcgroup内で発生したイベントのみを扱います。
試してみましょう。
まず、memory_オプションを指定して、cgroup v2を再マウントします。
$ sudo mount -t cgroup2 -o remount,rw,nosuid,nodev,noexec,relatime,nsdelegate,memory_recursiveprot,memory_localevents cgroup2 /sys/fs/cgroup/ (再マウントのためのremountとmemory_localeventsを指定してcgroup v2を再マウント) $ grep cgroup2 /proc/self/mounts cgroup2 /sys/fs/cgroup cgroup2 rw,nosuid,nodev,noexec,relatime,nsdelegate,memory_localevents,memory_recursiveprot 0 0 (memory_localeventsが指定されている)
もし、cgroup v2がマウントされていない環境で試す場合は、memory_オプションを指定してcgroup v2をマウントします。
$ sudo mount -t cgroup2 -o memory_localevents cgroup2 /sys/fs/cgroup/
これまでの実行例と同様にtest01 cgroupを作成し、その子cgroupとしてtest011 cgroupを作成し、test011 cgroup内でOOM Killerを発動させます。
$ sudo mkdir -p /sys/fs/cgroup/test01/test011 :(これまでの実行例と同じなので省略) $ stress-ng --vm 1 --vm-bytes 256M --vm-hang 0 -v :(略)
OOM Killerが発動され、stress-ngプロセスがkillされていることを確認したら、test01とtest011 cgroupのmemory.を確認します。
$ cat /sys/fs/cgroup/test01/test011/memory.events low 0 high 0 max 2339 oom 16 oom_kill 16 oom_group_kill 0 (test011 cgroupではイベントがカウントされている) $ cat /sys/fs/cgroup/test01/memory.events low 0 high 0 max 0 oom 0 oom_kill 0 oom_group_kill 0 (test01 cgroupではイベントがカウントされていない)
test011 cgroupでは、これまでの実行例と同様にイベントがカウントされています。
一方で、今回の実行例ではtest011 cgroupの親であるtest01 cgroupでは一切イベントがカウントされていません。
ここまでで紹介したmemory_オプションを指定した動作が、5.memory.ファイルの動作でした。
5.memory.ファイルの動作が変更されましたが、5.memory.ファイルは存在していませんでした。
このため、特定のcgroup内でのイベントのみを監視したいという要求に応えることができませんでした。そこで、5.memory.が追加され、階層構造を考慮したイベントについても、特定のcgroup内のイベントについても監視できるようになりました[3]。
まとめ
今回は、メモリコントローラ関係のイベント回数を表示したり、イベントが起こったことを監視できるファイルについて説明しました。
メモリコントローラは機能が多く、毎回、
そういうわけで、次回もまだ紹介していないメモリコントローラの機能を紹介する予定です。
