少し前にSNS上で、この連載に対する10周年おめでとうメッセージをいただきました。この連載は2014年5月に第1回が掲載され、今年でちょうど10年になります。このようなメッセージをいただくとは思っていなかったので、とてもうれしかったです。
最初の頃の記事を読むと、深い知識を知らないまま書いていることが、色々なところから感じられます。しかし、書き続けることで私自身の勉強になり、最初の頃よりはより充実した記事が書けるようになった気がしています。これも、記事を読んでいただいているたくさんの方のおかげだと思っています。ありがとうございます。
まだ書きたいことはありますので、連載は続きます。引き続きよろしくお願いいたします。
前回は、メモリコントローラがcgroup v1からv2でどのように変わったのかを紹介し、制限値を設定したときの動きを説明しました。制限値は、cgroup v1のときから設定できましたので、制限値を設定したときの動きは理解しやすかったのではないでしょうか。
cgroup v2からは、メモリの制限値だけでなくメモリの保証値を設定し、cgroupに対するメモリ保護が設定できるようになりました。この機能は、cgroup v2のメモリコントローラで、v1から一番大きく変わった機能であると言えるでしょう。
今回は、このメモリ保護を設定したときの動きを紹介します。
cgroup v1時代から設定されてきたように、特定のコンテナが大量にメモリを消費しないように制限値を設定すると、コンテナホストの安定稼働につながり、ひいてはコンテナの安定稼働につながります。
一方で、コンテナホスト全体でメモリの消費量が増加して、コンテナが使っているメモリが回収されると、コンテナの安定稼働に影響します。そこで、cgroup v2では、コンテナに最低限必要なメモリ量を設定し、必要以上にメモリが回収されないよう、メモリ保護の仕組みが実装されました。
まずは、前回からの復習を兼ねて、表1にcgroup v2のメモリコントローラで使用するインターフェースファイルを載せておきます。前回載せた表と同じです。
ファイル名 | 機能 | 操作 | デフォルト 値 |
---|---|---|---|
memory. |
cgroupとその子孫のcgroupが現在使っているメモリの総量 | 読み取り | ー |
memory. |
cgroupとその子孫のcgroupのメモリ消費が設定した値より少ない場合、cgroup内のプロセスのメモリは回収されない。回収可能なメモリがない場合はOOM Killerが呼ばれる | 読み書き | 0 |
memory. |
cgroupとその子孫cgroupのメモリ消費が設定した値より少ない場合、回収可能なメモリがない場合をのぞいては、cgroupのメモリは回収されない | 読み書き | 0 |
memory. |
cgroupとその子孫のcgroupのメモリ消費が設定値を超えた場合、メモリ回収の圧力がかかる。OOM Killerが呼ばれることはない | 読み書き | max |
memory. |
cgroupとその子孫のcgroupのメモリ消費が設定値を超えた場合で、減らせない場合はcgroupに対してOOM Killerが呼ばれる | 読み書き | max |
memory. |
スワップ使用量の制限値。cgroupとその子孫のcgroupのスワップ使用量が設定値を超えた場合、それ以上はスワップアウトしない | 読み書き | max |
memory. |
スワップ使用を絞る制限値。cgroupとその子孫のcgroupのスワップ使用量が設定値を超えた場合、スワップアウトを可能な限り絞る | 読み書き | max |
memory. |
この書き込んだバイト数分、メモリを回収する | 書き込み | ー |
memory. |
cgroup作成以降の自身とその子孫が使った最大のメモリ使用量 | 読み取り | ー |
memory. |
OOM Killerが呼ばれるとき、cgroup内と子孫のプロセスをまとめて扱うか、扱わないか | 読み書き | 0 |
memory. |
cgroupとその子孫のcgroupで起こったイベント数 | 読み取り | ー |
memory. |
現在のメモリ使用の状況をメモリタイプごとに表示 | 読み取り | ー |
それでは、メモリ保護について説明していきましょう。今回の記事の実行例は、Ubuntu 22.
メモリ保護
ここまでも説明した通り、cgroup v2からはメモリ保護が設定できるようになりました。
メモリ保護は、表1のとおりmemory.
とmemory.
で設定します。memory.
は、cgroup v2がstableになった4.memory.
は4.
この2つの違いは、memory.
とmemory.
の違いと同じで、OOM Killerが発動するかしないかです。
cgroup内のプロセスが消費するメモリは、memory.
とmemory.
で設定した値より少なければ回収されません。もし、memory.
が設定されている場合、回収できる保護されていないメモリがない場合はOOM Killerが呼ばれます。memory.
が設定されている場合は、OOM Killerは呼ばれません。
もし、memory.
やmemory.
で設定した値より多くメモリを消費している場合で、メモリ負荷が高まり、メモリを回収する圧力がかかると、超過している分に応じて回収されます。つまり、多く超過しているほど圧力がかかって回収され、少なく超過していると、その分、圧力は軽減されます[1]。
メモリ保護の設定値は、cgroupツリーの子孫にまで影響します。また、自分より上位のcgroupがある場合は、上位のcgroupに設定されている値や消費しているメモリ量によって制限を受けることに注意が必要です。
それでは、実際にメモリ保護が働くことを確認していきましょう。以下の実行例では、メモリを2GB搭載したホスト上で試しています。
設定しないとき
メモリ保護を設定する前に、何も設定しないときの動きを確認しましょう。cgroupに制限は設定せずに、表2のようにプログラムで消費するメモリを設定してstress-ng
を実行します。
cgroup | メモリ保護の設定 | プログラムの消費メモリ |
---|---|---|
test01 |
設定なし | 256M |
test02 |
設定なし | 1700M |
まずは1つ目のシェルでtest01
cgroupを作成し、自身をtest01
に追加します。
このシェル上で、メモリを256MB確保するようにstress-ng
コマンドを実行します。
$ sudo mkdir /sys/fs/cgroup/test01 (test01 cgroup作成) $ echo $$ | sudo tee /sys/fs/cgroup/test01/cgroup.procs 972 (プロセスをtest01 cgroupに登録) $ cat /proc/$$/cgroup 0::/test01 (test01に登録されたことを確認) $ stress-ng --vm 1 --vm-bytes 256M --vm-hang 0 -v (256MBメモリを消費するようにstress-ngを実行)
そして、test01
のcgroup.
の値を監視すると、大体指定したとおりの値のメモリを確保した状態になります。
$ while : ; do cat /sys/fs/cgroup/test01/memory.current ; sleep 1; done 0 0 0 266780672 276271104 276271104 276271104 276271104 :(略)
ここで、もう1つシェルを開き、こちらはtest01
とは別のcgroupへ所属するようにします。
この実行例で使っているホストはメモリを2GB持ったホストですので、多めのメモリを確保するように1700MBを確保するようにオプションを与えました。そして、なるべくメモリを使用するようにstress-ng
に--page-in
オプションを与えます。このオプションは、スワップに書き出したデータをメモリに書き戻すオプションです。
$ sudo mkdir /sys/fs/cgroup/test02 $ echo $$ | sudo tee /sys/fs/cgroup/test02/cgroup.procs 1028 $ cat /proc/$$/cgroup 0::/test02 $ stress-ng --vm 1 --vm-bytes 1700M --page-in --vm-hang 0 -v (1700MBメモリを消費し、スワップに書き出されたメモリを書き戻す指定でstress-ngを実行)
すると、test02
cgroupに所属するstress-ng
コマンドがメモリを消費しようとして、test01
cgroupに所属するプロセスが消費するメモリが減っていきます。
$ while : ; do cat /sys/fs/cgroup/test01/memory.current ; sleep 1; done :(略) 276332544 247586816 219856896 151453696 136445952 116088832 73453568 71491584 71491584 71491584 :(略)
この例では、test01
に所属するstress-ng
コマンドは70MBほどの消費に落ち着いています。確保できていない分はスワップへ書き出されます。
ここまでの動きを図で説明しましょう。
図1で、①のようにtest01
cgroupに所属するプロセスが起動しているところに、②のようにメモリを大量に消費するプログラムがtest01
cgroup以外で起動すると、test01
cgroupに所属するプロセスが消費しているメモリが回収され、スワップに書き出されます。
すると、③のようにtest01
cgroupに所属するプロセスが使えるメモリが少なくなり、パフォーマンスに影響が出ます。
このように、他のcgroupに所属するプロセスがメモリを消費すると、メモリが回収され、プロセスの実行に影響を与える可能性があります。このような場合、プロセスの実行に最低限必要なメモリをcgroupに設定しておくと、プロセスの安定稼働につながります。
そこで、memory.
やmemory.
を設定します。
memory.low
まずは、cgroup v2導入当初から存在していたmemory.
から動きを見ていきましょう。
メモリが回収できる場合
まずは、memory.
で設定したメモリ保護がきちんと効くことを確認してみましょう。
memory.low
の動きを確認するための設定とプログラムの消費メモリcgroup | memory. の値 |
プログラムの消費メモリ |
---|---|---|
test01 |
128M | 256M |
test02 |
設定なし | 1700M |
表3のように、cgroupで設定したmemory.
に設定した値より多めにプログラムにメモリを消費させます。その状態で、別にメモリ保護を設定していないcgroupに属しているプログラムにメモリを消費させます。
test01
cgroupを作成し、memory.
で設定した値より多くstress-ng
にメモリを消費させます。
$ sudo mkdir /sys/fs/cgroup/test01 $ echo $$ | sudo tee /sys/fs/cgroup/test01/cgroup.procs 996 $ echo 128M | sudo tee /sys/fs/cgroup/test01/memory.low 128M $ stress-ng --vm 1 --vm-bytes 256M --vm-hang 0 -v
ここで、別にtest02
cgroupを作成し、メモリ保護は設定しない状態で大量にメモリを消費するプロセスを起動します。先の実行例と同様に、メモリを使うように--page-in
オプションも与えます。
$ sudo mkdir /sys/fs/cgroup/test02 $ echo $$ | sudo tee /sys/fs/cgroup/test02/cgroup.procs 1038 $ stress-ng --vm 1 --vm-bytes 1700M --vm-hang 0 --page-in -v
test01
cgroupのmemory.
は、次のように変化します。
$ while : ; do cat /sys/fs/cgroup/test01/memory.current ; sleep 2; done :(略) 276557824 276557824 :(略) 152498176 150106112 144891904 :(略) 140783616 140783616 :(略)
他にメモリを消費するプログラムが起動したため、そちらにメモリが割りあたり、メモリが回収され、test01
に所属するプロセスのメモリ消費量が減っていることがわかります。
しかし、メモリは回収されたものの保証値を設定しているため、保証値以下になるほどにはメモリが回収されていないことがわかります。一方で、test02
cgroupのmemory.
は次のように変化しています。
$ while : ; do cat /sys/fs/cgroup/test02/memory.current ; sleep 2; done 1693544448 1701314560 1702375424 :(略) 1694265344 1694265344 :(略) 1556148224 1556148224 :(略)
メモリ保護がされていないcgroupに属するstress-ng
は、指定した1700MBのメモリに到達することはなく、メモリ消費が抑えられていることがわかります。
ここまでの動きを図2で説明します。
図2で、①のようにmemory.
を設定して、test01
cgroupに所属するプロセスを起動します。
その後、②でtest02
cgroupに所属する大量にメモリを消費するプロセスが起動しても、③のようにmemory.
で設定した値まではtest01
cgroupにメモリが確保された状態となります。
各cgroupに所属するプロセスが使用するメモリのうち、メモリが使えない分はスワップを使用します。
回収するメモリがない場合
次に、test01
だけでなくtest02
cgroupにもmemory.
を設定して、memory.
に設定した値より多くメモリを消費するプログラムの動きを見てみます。
memory.low
の動きを確認するための設定とプログラムの消費メモリcgroup | memory. の値 |
プログラムの消費メモリ |
---|---|---|
test01 |
128M | 256M |
test02 |
1700M | 1700M |
表4のように、片方のcgroupでは、memory.
に設定した値より多めにプログラムにメモリを消費させます。その状態で、別のcgroupでmemory.
に設定した値と同じだけ、プログラムにメモリを消費させます。
ここで、実行している環境は2GBしかメモリを搭載していないので、このように実行すると搭載以上のメモリを保護しなければならないことになります[2]。
test01
cgroupを作成し、memory.
で設定した値より多くstress-ng
にメモリを消費させます。また、これまでと同様になるべくメモリを使うように--page-in
オプションも与えます。
$ sudo mkdir /sys/fs/cgroup/test01 $ echo $$ | sudo tee /sys/fs/cgroup/test01/cgroup.procs 957 $ echo 128M | sudo tee /sys/fs/cgroup/test01/memory.low 128M $ stress-ng --vm 1 --vm-bytes 256M --vm-hang 0 --page-in -v
ここで、別のtest02
cgroupを作成し、memory.
に設定した値と同じ値をstress-ng
コマンドに設定します。こちらはstress-ng
コマンドに--page-in
オプションを与えます。
$ sudo mkdir /sys/fs/cgroup/test02 $ echo $$ | sudo tee /sys/fs/cgroup/test02/cgroup.procs 982 $ echo 1700M | sudo tee /sys/fs/cgroup/test02/memory.low 1700M $ stress-ng --vm 1 --vm-bytes 1700M --page-in --vm-hang 0 -v
test01
cgroupのmemory.
の値は、次のように変化していきます。
$ while : ; do cat /sys/fs/cgroup/test01/memory.current ; sleep 2; done 271757312 271757312 92090368 92090368 92098560 92098560 :(略)
最初はtest01
cgroupに所属するstress-ng
コマンドは、指定した256MB程度のメモリを消費しています。
その状態でtest02
cgroupに所属するプロセスがメモリを消費しはじめると、test01
cgroupに所属するプロセスは、memory.
に設定した値より多くメモリを消費していたので、メモリを回収する圧力がかかり、かなりのメモリがスワップに書き出されました。
最終的にはmemory.
へ設定した保証値以上に回収されてしまっています。
ここで、test02
cgroupのmemory.
の値は次のようになっていました。
$ while : ; do cat /sys/fs/cgroup/test02/memory.current ; sleep 2; done 1782550528 1782550528 1782550528 1782550528 1782550528 1782550528 :(略)
ここまでの動きを図3を使って説明しましょう。
図3で、①のようにtest01
cgroupに所属するプロセスがmemory.
設定値以上にメモリを消費していました。
そこに、test02
cgroupでmemory.
に設定された値以下しかメモリを消費していないプロセスを動かすと、test02
cgroupに所属するプロセスが消費するメモリは、memory.
に設定した通りに確保された状態になります。
memory.
に設定した値より多くメモリを消費していたtest01
cgroupにメモリ回収圧力がかかり、③のようにmemory.
設定値を満たさないレベルまでメモリが回収されたことがわかります。test01
cgroupではmemory.
に設定したメモリ保護が働いていません。
つまり、システム全体で回収するメモリがない場合は、memory.
を設定していても、memory.
は機能しないことがわかります。
このように、システム全体のメモリ負荷が高い場合で、システム全体で回収するメモリがない場合、memory.
に設定したメモリ保護は機能しません。回収可能なメモリがある場合のみ、メモリ保護が機能します。
実際に使用できるメモリより多い値をmemory.
として設定すると、OOM Killerは発動しないものの、メモリ保護自体効かない可能性があります。
メモリ保護に設定する値をオーバーコミットせず、適切な値を設定する必要があります。カーネルの付属文書にも
memory.min
ここまでで説明したように、memory.
は回収可能なメモリがある場合にのみ機能しました。
通常は、このようなメモリ保護で十分でしょう。しかし、memory.
のメモリ保護では役に立たないケースがあります。例えばスワップを持っていないシステムのようなケースです。
このようなケースにも対応できるように、4.memory.
が導入されました。
先に書いたように、memory.
とmemory.
の差は、ちょうどmemory.
とmemory.
の関係と似ています。そうです、memory.
はOOM Killerを使います。足りないメモリをOOM Killerを使って確保しようとします。
それでは、memory.
を設定して試してみましょう。
メモリが回収できる場合
まずは、memory.
を設定した状態で、回収するメモリが確保できる場合です。
memory.
の設定と、プログラムに与える値は表5のようにします。
memory.min
の動きを見るための設定とプログラムの消費メモリcgroup | memory. の値 |
プログラムの消費メモリ |
---|---|---|
test01 |
128M | 256M |
test02 |
設定なし | 1700M |
表5のように設定し、ここまでの実行例と同様にtest01
cgroupでstress-ng
コマンドを実行します。
$ sudo mkdir /sys/fs/cgroup/test01 $ echo $$ | sudo tee /sys/fs/cgroup/test01/cgroup.procs 961 $ echo 128M | sudo tee /sys/fs/cgroup/test01/memory.min 128M $ stress-ng --vm 1 --vm-bytes 256M --vm-hang 0 -v
この状態で、ここまでの実行例と同じように、test02
cgroupに属するプロセスで大量にメモリを消費させます。ここでもメモリを使用するようにstress-ng
コマンドには--page-in
オプションを与えます。
$ stress-ng --vm 1 --vm-bytes 1700M --page-in --vm-hang 0 -v
するとtest01
cgroupに所属するプロセスが消費するメモリは、次のように、さきほどのmemory.
を設定しなかったときのようにメモリ消費が少なくなることはなく、ある程度、設定値と近い値へ落ち着くようになります。
memory.
のときと同じような動きです。
$ while : ; do cat /sys/fs/cgroup/test01/memory.current ; sleep 1; done :(略) 276545536 276545536 274292736 273383424 254242816 223477760 :(略) 184053760 184053760 184053760 184053760 :(略)
このように回収するメモリがある場合は、図4のようにmemory.
のときの図2と同じになります。
つまり、回収するメモリが確保できる場合、memory.
とmemory.
で動きは変わりません。
回収するメモリがない場合
それでは、memory.
を設定した状態で、回収できるメモリがなくなった場合はどうなるのでしょう? それを見ていきましょう。
test01
、test02
という2つのcgroupを作成し、表6のようにそれぞれにmemory.
を設定します。
そして、搭載している量より大きなメモリを消費させるように、それぞれのcgroupに所属するstress-ng
プログラムに--page-in
オプションを与えて実行します。
memory.min
の動きを見るための設定とプログラムの消費メモリcgroup | memory. の値 |
プログラムの消費メモリ |
---|---|---|
test01 |
128M | 256M |
test02 |
1700M | 1700M |
test01
cgroupでは次のようにstress-ng
コマンドを実行します。ここまでと同様、メモリを使うように--page-in
オプションを与えます。
$ sudo mkdir /sys/fs/cgroup/test01 $ echo $$ | sudo tee /sys/fs/cgroup/test01/cgroup.procs 986 $ echo 128M | sudo tee /sys/fs/cgroup/test01/memory.min 128M $ stress-ng --vm 1 --vm-bytes 256M --vm-hang 0 --page-in -v
ここで、test02
cgroupでstress-ng
コマンドを実行すると、次のようにOOM Killerが発動します。
$ sudo mkdir /sys/fs/cgroup/test02 $ echo $$ | sudo tee /sys/fs/cgroup/test02/cgroup.procs 1043 $ echo 1700M | sudo tee /sys/fs/cgroup/test02/memory.min 1700M $ stress-ng --vm 1 --vm-bytes 1700M --vm-hang 0 -v stress-ng: debug: [3608] stress-ng-vm: child died: signal 9 'SIGKILL' (instance 0) stress-ng: debug: [3608] stress-ng-vm: assuming killed by OOM killer, restarting again (instance 0)
test01
cgroupでは、次のようにほぼmemory.
の設定通りにメモリが確保されています。
$ while : ; do cat /sys/fs/cgroup/test01/memory.current ; sleep 2; done :(略) 269770752 269770752 269770752 134221824 134217728 134217728 :(略)
一方で、test02
cgroupのほうは、OOM Killerによりメモリ消費が強制的に抑えられています。
$ while : ; do cat /sys/fs/cgroup/test02/memory.current ; sleep 2; done :(略) 1462009856 704868352 19591168 1182564352 78434304 :(略)
このとき、dmesg
でも次のようにOOM Killerが発動されたことが記録されています。
[ 216.682968] oom-kill:constraint=CONSTRAINT_NONE,nodemask=(null),cpuset=test02,mems_allowed=0,global_oom,task_memcg=/test02,task=stress-ng,pid=1236,uid=1001 [ 216.682979] Out of memory: Killed process 1236 (stress-ng) total-vm:1796844kB, anon-rss:1684292kB, file-rss:880kB, shmem-rss:44kB, UID:1001 pgtables:3488kB o om_score_adj:1000
ここまでで試した動きは、図5のようになります。
図5でも図4と同様に、①でmemory.
を設定して、test01
cgroupでプロセスを起動しました。
その後、②でtest02
cgroupにもmemory.
を設定し、memory.
で設定した値と同じ値でメモリを消費するプロセスを起動しました。
test01
cgroupに所属するプロセスはmemory.
以上にメモリを消費していましたので、test02
cgroupでプロセスを起動すると、memory.
以上のメモリは回収され、スワップに書き出されました。
しかし、それでもtest02
cgroupでmemory.
に設定されたメモリが確保できないため、③のようにOOM Killerが発動し、システム全体でメモリを確保しようとしました。
memory.
と違い、OOM Killerによりシステム全体のメモリ保護の設定を解決しようとするmemory.
の動きが確認できました。
dmesg
出力の1行目を見てみると、global_
という文字列があります。memory.
が起因となるOOM Killerは、cgroupに対するOOM Killerではなく、システム全体に対するOOM Kilerが発動していることがわかります。
このようにmemory.
で設定したメモリを保護できなくなったということは、システム全体でメモリが足りないということですので、システム全体のOOM Killerが発動します。dmesg
の出力から、そのことがわかります。
ちなみに、前回のmemory.
の実行例を見ると、dmesg
は次のようになっていました。
[ 1845.108346] oom-kill:constraint=CONSTRAINT_MEMCG,nodemask=(null),cpuset=test01,mems_allowed=0,oom_memcg=/test01,task_memcg=/test01,task=stress-ng,pid=4797,uid=1000
今回、OOM Killer時に出力されたログのglobal_
の部分に相当する文字列がoom_
となっていて、対象のcgroup名が記載されています。memory.
の場合はcgroup内のメモリ消費が設定値を超えたためにOOM Killerが発動しており、cgroup内のOOM Killerが発動していることがわかります。
ここでは、スワップがある環境を使って動きを見ましたが、スワップがないホスト上では、OOM Killerを使ってメモリ使用量を抑えるmemory.
の動きはもっと重要になるでしょう。
まとめ
今回は、cgroup v2で設定できるようになったメモリ保証の動きを紹介しました。
まとめると、メモリ負荷が高くなった場合、メモリ保護は、
- 回収するメモリがある場合は
memory.
とlow memory.
の動きは同じmin - 回収するメモリがない場合、
memory.
で設定したメモリ保護は無効になるlow - 回収するメモリがない場合、
memory.
で設定したメモリを保護するため、OOM Killerが発動するmin
という動きになります。
メモリ保護の設定における、memory.
とmemory.
の違いがご理解いただけたのではないでしょうか。
実際には、今回試したようにメモリ保護を設定するために、直接cgroupを操作して設定することはほとんどないでしょう。
しかし、コンテナランタイムなどでコンテナに対してメモリ保護を設定する場合でも、cgroupのメモリ保護の動きを理解した上で、コンテナの設計や見積もりをきっちりと行った上で保証値を設定しないと、OOM Killerが呼ばれてプロセスが安定して稼働できなかったり、保証値以下までメモリが回収されてしまったりして、コンテナの安定稼働に影響を及ぼすでしょう。
次回は、今回紹介できなかったメモリコントローラの階層構造を考慮した動きや、cgroup v2のマウントオプションのうち、メモリコントローラに関係するオプションを紹介する予定です。