LXCで学ぶコンテナ入門 -軽量仮想化環境を実現する技術

第61回Linuxカーネルのコンテナ機能 - cgroup v2から使うメモリコントローラ(6)

第56回から第60回までで、メモリコントローラを使ってメモリの使用量やOOM Killerの動作を制御する方法を説明しました。

メモリコントローラを設定することで、制限値を上回ったり、保証値を下回ったりした場合、すべてcgroupに処理を任せておいてよいわけではなく、イベントの発生を監視して対処しなければならない場合もあるでしょう。

今回は、メモリコントローラ関係のイベントを監視する際に使えるファイルについて説明します。

memory.eventsファイル

メモリコントローラが有効になったcgroupには、表1のとおり、memory.eventsmemory.events.localというファイルがあります。これらのファイルから、cgroupで起こったメモリコントローラ関係のイベント回数がわかります。

表1 cgroupのメモリ関連イベントの回数を表示するファイル
ファイル名 機能 操作 実装されたバージョン
memory.events cgroupとその子孫のcgroupで起こったイベント数 読み取り 4.5
memory.events.local memory.eventsと内容は同じだが、ファイルが存在するcgroup内のイベント数のみを表示する 読み取り 5.3

これら2つのファイルの中身は同じで、イベント名とそのイベントが発生した回数がスペース区切りで1行に書かれています。メモリコントローラで発生するイベントは表2のとおりです。

表2 メモリコントローラで発生するイベント
イベント名 イベントの説明
low タスクのメモリ使用量がmemory.lowで指定した値を下回っているにも関わらず、高いメモリ圧力によりメモリが回収された。このイベントが発生しているということはメモリ保護の設定がオーバーコミットされていることを示す
high タスクのメモリ使用量がmemory.highの値を超過したためにメモリ回収が実行された
max タスクのメモリ使用量がmemory.maxを超えようとした
oom タスクのメモリ使用量が制限に達して割り当てが失敗しそうになった
oom_kill OOM Killerによってタスクが強制終了させられた
oom_group_kill cgroup単位のOOMが発生した。memory.oom.group1に設定されている場合にcgroup全体のタスクがOOMによってkillされた

2つのファイルの違いは、memory.eventsはファイルが存在するcgroupとその子孫のcgroupで起こったイベントの回数を表示し、memory.events.localはファイルが存在するcgroup内のローカルイベントの回数のみを表示することです。

実際に動作を見る前に、cgroup v2のマウントオプションを確認しておきます。

$ grep cgroup2 /proc/self/mounts
cgroup2 /sys/fs/cgroup cgroup2 rw,nosuid,nodev,noexec,relatime,nsdelegate,memory_recursiveprot 0 0

マウントオプションの中にmemory_localeventsが含まれないことを確認してください。通常はmemory_localeventsオプションは指定されていないはずです。memory_localeventsオプションについては、あとで説明します。

マウントオプションが確認できたところで、実際のmemory.eventsmemory.events.localファイルの動作を確認していきましょう。

memory.eventsファイルとmemory.events.localファイルの動作を確認するために、test01 cgroupと、その子であるtest011 cgroupを作成して、プロセスはtest011 cgroupに所属させ、memory.maxを超えるメモリを消費させてOOM Killerを発動させてみます。

これまでのように、設定したmemory.maxを超える値を指定して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.eventsmemory.events.localファイルを確認してみましょう。

まずは、プロセスが所属した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を超えようとした回数を示すmax行が0ではなく、memory.maxを超えたことがわかります。そして、OOM Killerが発動していますので、oomoom_kill7という数字が入っています。

次に、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.eventsファイルには子孫であるtest011 cgroupで発生したイベントがカウントされています。一方で、test01 cgroupではイベントが起こっていませんので、memory.events.localファイルはすべての値が0のままです。

memory.oom.groupファイルとmemory.eventsファイル

ここまでの実行例は、第60回で紹介したmemory.oom.groupが0に設定された状態で実行していました。

5.16カーネルまでは、oom_group_killというイベントは定義されておらず、memory.oom.groupファイルが1に設定された状態でOOM Killerが発動したときも、oom_killイベントだけが発生していました。

oom_killイベントだけでも、memory.eventsmemory.events.localファイルとmemory.oom.groupの設定を見れば、単独のプロセスがkillされたのか、cgroupに所属するプロセスがまとめてkillされたのかはわかることが多いでしょう。

しかし、子孫のcgroupを持つツリーの中間に存在するcgroupでoom_killイベントが発生しても、killされたプロセスが単独のプロセスなのか、cgroup内のプロセスすべてなのか、また、イベントが発生したのが自身なのか、子孫なのかなどが明確にわかりません。

そこで、5.17カーネルでoom_group_killイベントが定義され、memory.oom.groupを1に設定したことによるOOM Kill発動が明確にわかるようになりました[1]

oom_group_killの値がカウントされることを、実際に試して確認しておきましょう。

$ 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.eventsmemory.events.localファイルを確認してみます。

$ 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_group_killが1となっており、cgroup内のプロセスがまとめてkillされるイベントが発生したことがわかります。

このとき親cgroupであるtest01 cgroupでは、memory.eventsファイルのoom_group_kill1になっています。一方で、自身のcgroup内のイベントだけをカウントするmemory.events.localファイルのoom_group_kill0のままであることも確認できます。

$ 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.eventsファイルやmemory.events.localファイルの中を見ると、メモリコントローラに関係するイベントの発生回数を知ることができます。

cgroupを使って、メモリの消費状況を監視する場合、メモリコントローラでイベントが発生したことを知ることができると便利です。メモリコントローラでは、第39回で紹介したcgroup.eventsファイルと同様に、memory.eventsファイルとmemory.events.localファイルの中身が変化したときに通知を受け取れます。

カーネル付属ドキュメントmemory.eventsの項には、"the file modified event can be generated"とだけ書かれています。memory.eventsファイルとmemory.events.localファイルでも、cgroup.eventsファイルと同様に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_MODIFYイベントを受け取ることにより、pollの場合はreventsにセットされるPOLLPRIPOLLERRビットをチェックすることでメモリに関するイベントが受け取れます。

poll(2)POLLPRIの項目には、

A cgroup.events file has been modified (see cgroups(7)).

と書かれており、cgroup関連のファイル変更ではPOLLPRIが設定されることが明記されています。

memory.eventsファイルやmemory.events.localファイルで、これらの通知が受け取れるかを、これまでと同様の実行例で確認してみます。

$ 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.eventsファイルを監視します。

そして、stress-ngコマンドでmemory.maxの制限値を超えるメモリを指定して実行すると、OOM Killerが発動します。

$ sudo swapoff -a
$ stress-ng --vm 1 --vm-bytes 256M --vm-hang 0 -v
  :(略)

このとき、test011 cgroupのmemory.eventsファイルとmemory.events.localファイルを指定した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.events.localファイルでは、自身で発生したOOM Killerではないので、イベントは発生していません。

$ inotifywait /sys/fs/cgroup/test01/memory.events.local
(test01のmemory.events.localを監視)
Setting up watches.
Watches established.
(待受状態のまま)

このように、memory.eventsファイルとmemory.events.localファイルを監視することで、メモリコントローラで発生したイベントを監視できることが確認できました。

memory_localeventsオプション

ここまでの説明で、memory.eventsは階層構造の子孫で発生したイベントを扱い、memory.events.localでは自身のcgroupで発生したイベントのみを扱うことを説明しました。

しかし、実はcgroup v2が導入された当初はmemory.eventsファイルは、現在のmemory.events.localファイルのように動作していました。つまり、子孫のイベントは考慮していませんでした。

cgroup v2全体としては、たとえばmemory.statファイルなどは階層構造を考慮し、子孫の統計情報を含んで表示しているのに、memory.eventsは子孫のイベントを考慮しないで表示することは、混乱の原因となります。

そこで、5.2カーネルでmemory.eventsは子孫で発生したイベントを扱うように変更されました[2]

この変更は、本来あるべき姿への変更ですが、従来の動作との互換性は保たれません。そこで、従来どおりの動作を期待するユーザのために、cgroup v2をマウントする際のオプションとして、memory_localeventsオプションが追加されました。

memory_localeventsオプションを指定してcgroup v2をマウントすると、memory.eventsファイルは自身のcgroup内で発生したイベントのみを扱います。

試してみましょう。

まず、memory_localeventsオプションを指定して、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_localeventsオプションを指定して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されていることを確認したら、test01test011 cgroupのmemory.eventsを確認します。

$ 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_localeventsオプションを指定した動作が、5.1カーネルまでのmemory.eventsファイルの動作でした。

5.2カーネルで、memory.eventsファイルの動作が変更されましたが、5.2カーネルの時点ではmemory.events.localファイルは存在していませんでした。

このため、特定のcgroup内でのイベントのみを監視したいという要求に応えることができませんでした。そこで、5.3カーネルでmemory.events.localが追加され、階層構造を考慮したイベントについても、特定のcgroup内のイベントについても監視できるようになりました[3]

まとめ

今回は、メモリコントローラ関係のイベント回数を表示したり、イベントが起こったことを監視できるファイルについて説明しました。

メモリコントローラは機能が多く、毎回、⁠メモリコントローラの説明は今回が最後」と思いつつ書き進めると、書き残しが発生し、次回もやっぱりメモリコントローラの説明をしないといけなくなっています(笑⁠⁠。

そういうわけで、次回もまだ紹介していないメモリコントローラの機能を紹介する予定です。

おすすめ記事

記事・ニュース一覧