第58回では、メモリコントローラでメモリに対する制限や保護を設定した場合に、階層構造が考慮される動作を見ました。
上位のcgroupでメモリに対する制限値や保証値を設定した場合、その子孫に対しても設定が有効になるので、サブツリー全体に対してメモリの制限をかけたり、サブツリー全体を保護したりできます。
その中で、ツリー構造を考慮したメモリ保護の動作に違和感を持った方は多いのではないでしょうか。上位cgroupで設定したメモリ保護が、下位cgroupに影響しない場合があったためです。このような動作は直感的ではなく、cgroupで制限を設定した場合のツリー構造を考慮した動作とは異なりましたし、実際の使用でも問題がありました。
このため、5.
機能を説明するにあたって、図1のようにroot cgroup直下に
「システム関係タスク」
ここで、メモリコントローラを使い、
それぞれのサブツリーに適切な制限や保護を設定することで、ホストの運用に必要なタスクにも、サービス提供に必要なワークロードにも悪影響が出ないようにできます。このように、cgroupを適切に設定し、ホストの安定運用を実現することは一般的なことでしょう。
サブツリー全体に対するメモリ制限
まずは、サブツリー全体に制限をかけて、他のサブツリーに属するタスクの実行に影響が出ないようにすることを考えてみます。
ロギングなどのシステム運用に必要なタスクが必要以上にメモリを消費して、
図2のように、サブツリー上位の
図2の例では、
すると、
図2では、
サブツリー全体のメモリ消費を制限するには、このように上位のcgroupで制限を設定し、それ以下のcgroupはデフォルト値のままで、必要なcgroupにのみ制限値を設定するというような運用が考えられます。
サブツリー配下のcgroupすべてに制限値を設定するということはまれでしょう。
カーネルデフォルトのメモリ保護の動作
サブツリー全体に対するメモリ保護を考える前に、メモリ保護におけるカーネルデフォルトの動作をおさらいしておきましょう。
メモリ保護に関係するファイルのデフォルト値は、第57回で説明したとおり"0"でした。つまり、デフォルトではcgroupに対してはメモリ保護が働かない状態です。
階層構造を考えた場合に適用されるメモリ保証値は、第58回で説明したとおり、歴史的な経緯があり、また実装が容易であったため、ツリーをroot cgroupまでたどったときの最小値が適用されるように実装されていました。
図3のように、サブツリー上位のcgroupでメモリ保証値が設定されていても、それ以下のcgroupでメモリ保証値を明示的に設定しない場合、デフォルト値である"0"が設定されているため、メモリ保護が効かないことになります。
これが、第58回で確認したメモリ保護におけるカーネルデフォルトの動作でした。
サブツリー全体に対するメモリ保護
このようなメモリ保護に対するカーネルデフォルトの動作を踏まえて、サブツリー全体を保護するために、メモリ保護を設定することを考えてみましょう。
メモリ制限で設定したように、上位のcgroupに保証値を設定してサブツリー全体を保護したいところです。しかし、図3のように、サブツリー最上位の
その他のcgroupは、メモリ保証値を設定しない場合はデフォルト値ですから、カーネルデフォルトのメモリ保護の動作で説明したようにメモリは保護されません。
サブツリー全体を保護したい場合、図4のようにサブツリーすべてのcgroupに保証値を設定しなければいけません。
そこで例えば、図4のように、すべてのcgroupにサブツリー最上位のcgroupと同じ値を設定して、ツリー全体を保護することを考えてみましょう。
このように設定すれば、サブツリー全体が保護できるかもしれません。しかし、サブツリー全体を16GBで保護したいということと、サブツリー内のcgroupすべてに16GBを設定することはかなり意味が異なるでしょう。
では、適切に設定するために、サブツリー全体でcgroupそれぞれに適切なメモリ保護値を見積もった上で、設定すればよいかというとそんなことはないでしょう。すべてのcgroupで適切な設定値を見積もるのは非常に大変な作業になるでしょうし、適切に見積もれないケースも多いはずです。サブツリー全体のcgroupすべてに適切な値を設定することは、非現実的だと言えます。
またLinuxホストでは通常、cgroupfsツリー/sys/以下)
そもそも、やりたいことは
さらに、すべてのcgroupに値を設定したとしても、各ワークロードやタスクが必要とするメモリは、時間とともに変化する可能性があります。すべてのcgroupに固定値でメモリ保証値を設定することで、逆にリソース分配が非効率になる可能性があります。
くわえて、すべてのcgroupに値を設定する場合、図5のように、下位のcgroupに設定する保護値の見積もりができずに、上位のcgroupで不当に大きなメモリ保護を設定するようなケースが考えられます。
図5のように設定している場合、負荷が高くなり、メモリが回収されているようなケースでは次のような動作になります。
- 「コンテナA」
では10GBのメモリが保護される - 「コンテナB」
では5GBのメモリが保護される - 「ワークロード」
で設定したメモリ保護のうち、10+5=15GBのメモリは保護されるが、残りの50-15=35GBのメモリは保護には使われない
このように、無駄に高く設定した保証値が、実際はメモリ保護には使用されないままの状態になります。
memory_recursiveprotオプション
ここまでで説明したとおり、メモリコントローラでメモリ保護を設定した場合の動作は、他のcgroupで制限値を設定した場合のように、上位の設定値が下位に分配される動作とは異なっており、不自然でわかりづらく、実際にメモリ保護を設定しようにも、現実には設定が困難でした。
上位のcgroupで指定した保護値を下位に分配するのであれば、cgroupの制限値を設定した場合の動作とも合っており、期待されるあるべき動作だと言えるでしょう。現実的にメモリ保護も設定しやすくなります。
このようなメモリ保護でのあるべき姿を実現するために、5.memory_オプションが導入されました。cgroup v2をマウントする時に、マウントオプションで指定します。
memory_オプションを指定してcgroup v2をマウントした場合、図6のように、上位のcgroupで設定した保証値で、ツリー全体が保護されます。
memory_recursiveprotオプションを指定した場合
図6のように、
ツリー配下のcgroupにメモリ保証値を設定していなくても、各cgroupには、
また、memory_オプションを設定すると、図5で説明したような
memory_recursiveprotオプションでメモリが分配される様子
図7のように、メモリ保証値として
- 「コンテナA」
では10GBのメモリが保護される - 「コンテナB」
では5GBのメモリが保護される - 「ワークロード」
で設定したメモリ保護のうち、残りの50-15=35GBのメモリは、 「コンテナA」 「コンテナB」 のメモリ使用状況に応じて分配される
つまり上位で設定したメモリ保護は、下位で明示的に指定された保護で使われない分も、下位の使用状況に応じて分配されますので無駄がありません。メモリが有効に使えるため、ホストの安定稼働に役立つでしょう。
もちろん、上位のcgroupで設定した保証値より大きな値を下位のcgroupに設定しても、上位の保証値を超えてメモリが保護されることはありません。
memory_recursiveprotオプションの動作
それでは、memory_オプションの動作を実際に見ていきましょう。
まずは、memory_オプションが有効であることを確認します。Ubuntu 24.
shell_01$ grep memory_recursiveprot /proc/self/mountinfo 34 25 0:29 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime shared:9 - cgroup2 cgroup2 rw,nsdelegate,memory_recursiveprot
このようにmemory_オプションを指定してcgroup v2をマウントしています。
cgroup v2がマウントされているものの、memory_オプションが指定されていない場合は、次のように再マウントします。
$ sudo mount -t cgroup2 -o remount,memory_recursiveprot,(その他のオプション) cgroup2 /sys/fs/cgroup
また、cgroup v2がマウントされておらず、cgroup v2が使えない状態の場合は次のようにマウントします。
$ sudo mount -t cgroup2 -o memory_recursiveprot cgroup2 /sys/fs/cgroup/
cgroup v2でmemory_オプションを指定してマウントした状態になったところで、下位のcgroupで保証値を設定しなくても、上位のcgroupで設定した保証値が下位のcgroupに分配され、上位のcgroupで設定したメモリ保護が下位のcgroupでも有効になっていることを確認してみましょう。
ここで、第58回の
動作がわかりやすいように、test01 cgroupのmemory.は、第58回の設定値とは異なる128MBに設定しました。
| cgroup | memory.の値 |
プログラムの消費メモリ |
|---|---|---|
test01 |
128M | - |
test011 |
0 | 256M |
test02 |
0 | 1700M |
shell_01$ sudo mkdir -p /sys/fs/cgroup/test01/test011 (cgroupを作成) shell_01$ echo "+memory" | sudo tee /sys/fs/cgroup/cgroup.subtree_control (子cgroupでmemoryコントローラーを使えるようにroot cgroupを設定) +memory shell_01$ echo "+memory" | sudo tee /sys/fs/cgroup/test01/cgroup.subtree_control (子cgroupでmemoryコントローラーを使えるようにtest01 cgroupを設定) +memory shell_01$ echo "128M" | sudo tee /sys/fs/cgroup/test01/memory.min (test01 cgroupで128MBのメモリ保護値を設定) 128M shell_01$ cat /sys/fs/cgroup/test01/memory.min 134217728 shell_01$ cat /sys/fs/cgroup/test01/test011/memory.min (test011 cgroupではメモリ保護値を設定していないのでデフォルトの0) 0 $ echo $$ | sudo tee /sys/fs/cgroup/test01/test011/cgroup.procs (シェルのPIDをtest011 cgroupに登録) 1021
表1のように設定し、メモリを256MB消費するプログラムをtest011 cgroupに所属するシェルから実行します。
shell_01$ stress-ng --vm 1 --vm-bytes 256M --vm-hang 0 -v :(略)
そして、test02 cgroupに所属するシェルから、大量にメモリを消費させるようにプログラムを実行します。
shell_02$ sudo mkdir /sys/fs/cgroup/test02 shell_02$ echo $$ | sudo tee /sys/fs/cgroup/test02/cgroup.procs 1071 shell_02$ stress-ng --vm 1 --vm-bytes 1700M --page-in --vm-hang 0 -v :(略)
test011 cgroupでのメモリ使用量を確認すると、次のようになりました。
$ while :; do sleep 2; cat /sys/fs/cgroup/test01/test011/memory.current ; done (test011のメモリ使用量を確認) :(略) 297463808 297463808 216137728 134082560 134111232 134111232 134111232 :(略)
最初はstress-ngプログラムへの指定どおり256MB程度メモリを消費していました。test02 cgroupで大量にメモリが消費されるとメモリが回収されました。しかし、第58回のように大量にメモリが回収されることはなく、test01 cgroupで指定した128MB程度でメモリ消費の減少は止まっています。
つまり、プロセスが所属するtest011 cgroupでメモリ保証値を設定しなくても、その上位のtest01 cgroupで設定したメモリ保証値が効いていることがわかります。第58回の実行例と比較してみてください。
memory_recursiveprotオプションとsystemd
先に述べたように、memory_オプションを付与すると、メモリ保護の動作はcgroup v2でリソース制限を設定した場合の動作とも一致し、あるべき姿になったと言えるでしょう。
あるべき姿ではあるものの、memory_オプションはカーネルデフォルトでは有効ではありません。なぜ、カーネルデフォルトの動作は従来のままでmemory_オプションが新設されたのでしょう。
これは、カーネルの進化ではよくあることで、それまでの動作との互換性を保つためです。仮にmemory_オプションが実装される前のメモリ保護の動作を期待して、cgroup v2が設定されて動作しているシステムがあった場合、急にカーネルの動作が変わってしまうと不具合が発生する可能性があるためです。
このためカーネルでは、明示的にmemory_オプションを指定したときだけ、機能が有効になるようになりました。
しかしsystemdでは、オプションを指定したときの動作があるべき姿であるという理由で、247以降では、memory_オプションを指定してcgroup v2をマウントするようになりました[1]。
今では、稼働しているLinuxホストのほとんどがsystemdを採用しているので、古いバージョンのsystemdが動いている環境でなければ、memory_を指定した状態でcgroup v2が動作していると言ってもよいでしょう。
まとめ
今回は、memory_オプションを指定すると、どのようにメモリ保護が動作するのかについて説明し、実際の動作を確認しました。
第58回や今回の最初で紹介した、階層構造を考慮したメモリ保護におけるカーネルデフォルトの動作は少し不自然でした。
そこで、5.memory_オプションが導入され、systemdではデフォルトでmemory_オプションを指定してcgroup v2をマウントするようになり、今ではmemory_を指定した動作が事実上のデフォルトになりました。
cgroup v2の目玉の1つは、メモリ保護が設定できることでした。memory_オプションが実装されたことにより、メモリ保護の動作があるべき姿になり、柔軟でわかりやすく設定できるようになりました。
次回は、引き続きメモリコントローラが持つ機能について紹介する予定です。
