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

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

気づくとこの連載も60回目です。

最近は、Linuxカーネルに実装されるコンテナ関連の機能が高度になり、機能を調べて理解するのが難しくなってきました。さらに、どのように紹介すればよいかというところも悩ましいところです。そのため、連載のペースが落ちっぱなしですが、連載自体は続けていきたいと思っていますので、引き続きよろしくお願いいたします。

OOM Killerについて

これまで第56回第57回で見たように、cgroupに所属するタスクが消費するメモリがmemory.maxで設定した制限値を超えた場合、OOM Killerが呼ばれ、プロセスがkillされました。

OOM Killerによってkillされるプロセスは、プロセスが持つOOMスコアをもとに選ばれ、もっともOOMスコアが高いプロセスが選ばれます。

このOOMスコアは、procファイルシステム内のプロセスディレクトリーである/proc/[PID]/内にあるoom_scoreファイルから取得できます。また、oom_score_adjファイルで優先度を調整できます。OOMスコアを計算する際には、このoom_score_adjの値が考慮されます。

表1 OOM Killerに関係するファイル
ファイル名 説明
oom_score プロセスの現在のOOMスコア(0〜2000)
oom_score_adj OOMスコアの計算の際に足される調整値(-1000〜+1000⁠⁠。-1000の場合はOOM Killerの対象から外れる
oom_adj 昔のカーネルで使われていた調整値(-17〜+15⁠⁠。互換性のために存在

本連載では、OOMスコアの計算についてはこれ以上説明しません。詳細を知りたい方は、カーネル付属文書の"The /proc Filesystem"をご覧ください。

cgroupにおいてOOM Killerが発動する際は、デフォルトではこのOOMスコアによって、cgroupに所属するプロセスからkillするプロセスが選ばれます。OOMスコアによって選ばれた、単一のプロセスがkillの候補となります。

OOM Killerの動きは、システム全体とcgroupでは変わりません。違いは、システム全体のプロセスから選ぶか、cgroupに所属するプロセスのみを対象にするかの違いです。

memory.oom.group

前述のように、cgroup内でmemory.maxで設定した制限値を超えるメモリが使用されると、OOM Killerが発動し、高いOOMスコアを持つプロセスがkillされます。

ところが、アプリケーションコンテナ環境では、コンテナ内のタスクをすべて同一のcgroupに所属させることが多く、コンテナ内では必要なタスクのみが動いているはずです。この状態でOOM Killerが発動し、コンテナ内のタスクが1つだけkillされると、コンテナ内で実行されているワークロードの動作に不都合が生じる可能性が高くなります。

そこで4.19カーネルでmemory.oom.groupという設定ファイルがcgroupに追加されました。

表2 memory.oom.groupの値と動作
memory.oom.groupの値 動作
0 デフォルト値。従来どおりのOOM Killerの動き。OOMスコアに応じてcgroupに所属するプロセスを選び、killする
1 1に設定されているcgroupにプロセスが所属している場合はそのcgroupに所属するプロセスすべてを、子孫のcgroupを持つ場合は、その子孫cgroupに所属するプロセスすべてをまとめてkillする

表2のように、このファイルの内容は、デフォルトでは0であり、この機能は無効です。ファイルに1を書き込むと、機能が有効になります。

機能を有効にすると、cgroup内で実行されているプロセスすべてを分割できないワークロードとして扱い、制限値を超えた場合はcgroupに所属するプロセスをまとめてkillします。

memory.oom.groupを有効にしたcgroupに子孫cgroupが存在する場合は、その子孫cgroupに所属するプロセスすべてがまとめてkillされます。

0 の場合

memory.oom.groupが0に設定されている場合は、これまでの回で紹介したような動きになります。比較のために今回も動作を確認しておきましょう。

cgroupでメモリ制限値を設定し、プロセスをいくつか所属させた上で、メモリを制限値以上に消費するプログラムを実行してcgroupに所属するプロセスの様子を確認します。

次のように、root cgroup直下にtest01 cgroupを、その子cgroupとしてtest011 cgroupを作成し、test011 cgroupでmemoryコントローラが使えるように設定します。

$ sudo mkdir -p /sys/fs/cgroup/test01/test011 (cgroupを作成)
$ echo $$ | sudo tee /sys/fs/cgroup/test01/test011/cgroup.procs 
(シェルをtest011 cgroupに登録)
1041
$ echo "+memory" | sudo tee /sys/fs/cgroup/cgroup.subtree_control
(子cgroupでmemoryコントローラを使えるようにroot cgroupを設定)
+memory
$ echo "+memory" | sudo tee /sys/fs/cgroup/test01/cgroup.subtree_control
(子cgroupでmemoryコントローラを使えるようにtest01 cgroupを設定)
+memory

次に、メモリ制限値を設定します。

$ echo 192M | sudo tee /sys/fs/cgroup/test01/test011/memory.max 
(test011 cgroupで192MBのメモリ制限値を設定)
192M
$ sudo swapoff -a
(スワップの使用を停止)

test011 cgroupで192MBにメモリ制限値を設定し、OOM Killerを発動しやすくするためにスワップの使用を停止しました。

メモリを大量に消費するプロセス以外はkillされないことを確認するために、あらかじめsleepコマンドを2つ実行しておき、test011 cgroupに所属させます。

$ sleep 7200 &
[1] 1083
$ sleep 7200 &
[2] 1084
$ echo 1083 | sudo tee /sys/fs/cgroup/test01/test011/cgroup.procs 
1083
$ echo 1084 | sudo tee /sys/fs/cgroup/test01/test011/cgroup.procs 
1084
(sleepプロセスを起動し、test011 cgroupにプロセスを追加)

これで準備ができました。test011 cgroupに所属するmemory.oom.groupの値が0であることを確認して、ここまでの実行例と同様にstress-ngコマンドを実行し、メモリを制限値以上に消費させます。

$ cat /sys/fs/cgroup/test01/test011/memory.oom.group 
0
(memory.oom.groupの値が0であることを確認)
$ stress-ng --vm 1 --vm-bytes 256M --vm-hang 0 -v
  :(略)
stress-ng: debug: [1107] vm: child died: signal 9 'SIGKILL' (instance 0)
stress-ng: debug: [1107] vm: assuming killed by OOM killer, restarting again (instance 0)
stress-ng: debug: [1107] vm: child died: signal 9 'SIGKILL' (instance 0)
stress-ng: debug: [1107] vm: assuming killed by OOM killer, restarting again (instance 0)
  :(略)

すると、このように制限値を超えたためにOOM Killerが発動し、stress-ngコマンドのプロセスがkillされます。このとき、test011 cgroupに所属するその他のプロセスがどうなっているかを確認します。

$ cat /sys/fs/cgroup/test01/test011/cgroup.procs 
1041
1083
1084
1120
(test011 cgroupに所属するstress-ng以外のプロセスは所属したまま)
$ ps -p 1083,1084
    PID TTY          TIME CMD
   1083 pts/1    00:00:00 sleep
   1084 pts/1    00:00:00 sleep
(起動していたsleepプロセスは実行中のまま)

test011 cgroupで実行していたシェルとsleepコマンドはいずれも実行中で、killされていないことが確認できました。

1 の場合

それでは、memory.oom.groupに1を設定して、この機能を有効にしたときの動きを見ていきましょう。

まずは、さきほど起動したsleepコマンドによるプロセスを終了させます。そして、再度sleepコマンドをnohupで実行します。

$ killall sleep
$ nohup sleep 7200 &
[1] 1135
$ nohup sleep 7200 &
[2] 1136
$ cat /sys/fs/cgroup/test01/test011/cgroup.procs 
1041
1135
1136
1137

これで、シェルと、シェルがkillされても停止しないsleepコマンドがtest011 cgroupに所属しました。

次のようにmemory.oom.groupに1を設定し、stress-ngで制限値以上にメモリを消費させます。

$ echo 1 | sudo tee /sys/fs/cgroup/test01/test011/memory.oom.group 
(memory.oom.groupに1を設定)
1
$ cat /sys/fs/cgroup/test01/test011/memory.oom.group 
1
(設定された)
$ stress-ng --vm 1 --vm-bytes 256M --vm-hang 0 -v
(実行していたシェルが kill される)

stress-ngを実行した途端に、stress-ngを実行していたシェルごと強制終了させられました。

別シェルからsleepコマンドのプロセスを調べると存在しませんし、test011 cgroupに所属するプロセスもありません。

$ ps -p 1135,1136
  PID TTY          TIME CMD
(存在しない)
$ cat /sys/fs/cgroup/test01/test011/cgroup.procs
(test011 cgroupにもプロセスがいない)

dmesgでカーネルメッセージを確認すると、次のように、Tasks in /test01/test011 are going to be killed due to memory.oom.group setという説明とともに、test011 cgroupに所属していたプロセスがすべてkillされていることがわかります。

[  655.137094] oom-kill:constraint=CONSTRAINT_MEMCG,nodemask=(null),cpuset=/,mems_allowed=0,oom_memcg=/test01/test011,task_memcg=/test01/test011,task=stress-ng-vm,pid=1160,uid=1000
[  655.137110] Memory cgroup out of memory: Killed process 1160 (stress-ng-vm) total-vm:525564kB, anon-rss:176556kB, file-rss:640kB, shmem-rss:0kB, UID:1000 pgtables:444kB oom_score_adj:1000
[  655.137740] Tasks in /test01/test011 are going to be killed due to memory.oom.group set
[  655.137753] Memory cgroup out of memory: Killed process 1041 (bash) total-vm:9008kB, anon-rss:1920kB, file-rss:3968kB, shmem-rss:0kB, UID:1000 pgtables:56kB oom_score_adj:0
[  655.137973] Memory cgroup out of memory: Killed process 1135 (sleep) total-vm:5684kB, anon-rss:0kB, file-rss:2048kB, shmem-rss:0kB, UID:1000 pgtables:64kB oom_score_adj:0
[  655.138205] Memory cgroup out of memory: Killed process 1136 (sleep) total-vm:5684kB, anon-rss:0kB, file-rss:2048kB, shmem-rss:0kB, UID:1000 pgtables:52kB oom_score_adj:0
[  655.138495] Memory cgroup out of memory: Killed process 1153 (stress-ng) total-vm:263416kB, anon-rss:8832kB, file-rss:6784kB, shmem-rss:16512kB, UID:1000 pgtables:364kB oom_score_adj:0
[  655.138716] Memory cgroup out of memory: Killed process 1159 (stress-ng-vm) total-vm:263420kB, anon-rss:9000kB, file-rss:1280kB, shmem-rss:0kB, UID:1000 pgtables:116kB oom_score_adj:0
[  655.138929] Memory cgroup out of memory: Killed process 1160 (stress-ng-vm) total-vm:525564kB, anon-rss:176556kB, file-rss:640kB, shmem-rss:0kB, UID:1000 pgtables:444kB oom_score_adj:1000

次に、memory.oom.groupを有効にしたcgroupで、子孫のcgroupに所属するプロセスすべてがまとめてkillされることも確認しておきましょう。

図1のようなツリーを作成します。

図1 memory.oom.groupで子孫のプロセスがkillれる動きを確認するためのツリー
memory.oom.groupで子孫のプロセスがkillれる動きを確認するためのツリー

そして、test011test012 cgroupそれぞれにシェルとsleepコマンドのプロセスを所属させ、親cgroupであるtest01 cgroupでmemory.oom.groupを1に設定します。

$ sudo mkdir -p /sys/fs/cgroup/test01/test01{1,2}
$ 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/memory.max 
192M
$ sudo swapoff -a
$ nohup sleep 7200 &
[1] 1117
$ nohup sleep 7200 &
[2] 1118
$ nohup sleep 7200 &
[3] 1119
$ echo 1117 | sudo tee /sys/fs/cgroup/test01/test011/cgroup.procs 
1117
$ echo 1118 | sudo tee /sys/fs/cgroup/test01/test012/cgroup.procs 
1118
$ echo 1119 | sudo tee /sys/fs/cgroup/test01/test012/cgroup.procs 
1119
$ echo $$ | sudo tee /sys/fs/cgroup/test01/test011/cgroup.procs 
(シェルをtest011 cgroupに所属させる)
1053

準備ができたところで、test011 cgroupに所属しているシェル上で、これまでと同様にstress-ngコマンドにより制限値を超えるメモリを消費させます。

$ stress-ng --vm 1 --vm-bytes 256M --vm-hang 0 -v
(実行していたシェルが kill される)

さきほどの例と同様にシェルがkillされます。その他、起動させていたプロセスを確認すると、test011test012 cgroupに所属しているプロセスはありませんし、プロセスも存在しません。

$ cat /sys/fs/cgroup/test01/test011/cgroup.procs 
(test011 cgroupに所属するプロセスはない)
$ cat /sys/fs/cgroup/test01/test012/cgroup.procs 
(test012 cgroupに所属するプロセスはない)
$ ps -p 1117,1118,1119,1053
    PID TTY          TIME CMD
(いずれのプロセスも存在しない)

dmesgで確認すると、まずstress-ngコマンドのプロセス(PID:1176)がkillされます。次に、test01 cgroup内で設定されているmemory.oom.groupが理由で、その他のtest01 cgroupの子孫に所属するプロセスがkillされていることがわかります。

[  937.581321] oom-kill:constraint=CONSTRAINT_MEMCG,nodemask=(null),cpuset=/,mems_allowed=0,oom_memcg=/test01,task_memcg=/test01/test011,task=stress-ng-vm,pid=1176,uid=1000
[  937.581335] Memory cgroup out of memory: Killed process 1176 (stress-ng-vm) total-vm:525564kB, anon-rss:177760kB, file-rss:512kB, shmem-rss:0kB, UID:1000 pgtables:452kB oom_score_adj:1000
[  937.581732] Tasks in /test01 are going to be killed due to memory.oom.group set
[  937.581739] Memory cgroup out of memory: Killed process 1117 (sleep) total-vm:5684kB, anon-rss:0kB, file-rss:2048kB, shmem-rss:0kB, UID:1000 pgtables:52kB oom_score_adj:0
[  937.581966] Memory cgroup out of memory: Killed process 1053 (bash) total-vm:8880kB, anon-rss:1792kB, file-rss:3840kB, shmem-rss:0kB, UID:1000 pgtables:64kB oom_score_adj:0
[  937.582191] Memory cgroup out of memory: Killed process 1174 (stress-ng) total-vm:263416kB, anon-rss:8960kB, file-rss:6016kB, shmem-rss:16512kB, UID:1000 pgtables:368kB oom_score_adj:0
[  937.582301] Memory cgroup out of memory: Killed process 1175 (stress-ng-vm) total-vm:263420kB, anon-rss:9052kB, file-rss:1280kB, shmem-rss:0kB, UID:1000 pgtables:120kB oom_score_adj:0
[  937.582510] Memory cgroup out of memory: Killed process 1176 (stress-ng-vm) total-vm:525564kB, anon-rss:177760kB, file-rss:512kB, shmem-rss:0kB, UID:1000 pgtables:452kB oom_score_adj:1000
[  937.582730] Memory cgroup out of memory: Killed process 1118 (sleep) total-vm:5684kB, anon-rss:0kB, file-rss:2048kB, shmem-rss:0kB, UID:1000 pgtables:52kB oom_score_adj:0
[  937.582994] Memory cgroup out of memory: Killed process 1119 (sleep) total-vm:5684kB, anon-rss:0kB, file-rss:2048kB, shmem-rss:0kB, UID:1000 pgtables:56kB oom_score_adj:0

上位のcgroupでmemory.oom.groupを設定すると、メモリ制限値の超過が起こったcgroupだけでなく、その子孫に所属するプロセスすべてがkillされることが確認できました。

まとめ

今回は4.19カーネルで実装されたmemory.oom.groupファイルについて紹介しました。

このファイルに設定を書き込むことで、cgroup v2のメモリコントローラによるOOM Killerの動きを変えられます。cgroup自身や子孫に所属するプロセスをすべてOOM Killerの対象にすることで、コンテナで実行するワークロードの整合性を保てるようになりました。

次回も引き続きメモリコントローラの機能について紹介する予定です。

おすすめ記事

記事・ニュース一覧