Linux kernel のバグを追おう: memcg 編(1)
今回から、シリーズものとして Linux kernel で実際にあったバグを追うとともに、コードリーディングをしていこうと思います。
まずは、memcg 関連のバグを追っていきます!
この話を理解するには、cgroup の説明が必要になってきます。 まあ簡単に言うと、cgroup はプロセスをグループ化し、CPU やメモリ、I/O などのリソースを管理する仕組みです。例えば、「このグループが使えるメモリは 80 MB まで!それを超えたらメモリの回収を試みて、それでも無理ならプロセスを kill しちゃうぞ☆」というように使えます。
今回追っていく memcg は、memory cgroup の略です。cgroup が持つ機能のうち、プロセスが使用するメモリの量を記録したり、上限を設定したりする部分を担当します。cgroup については、気が向いたら別の記事にするかもしれません。cgroup には v1 と v2 があり、Docker や Kubernetes でも使われているなど、かなり奥が深い仕組みです。(まとめたい……!)
さて、本題のバグについてです。もともと、container host のように cgroup の作成と削除を繰り返す環境では、kernel が使用するメモリ量が増え続ける問題がありました。原因を調べると、memcg ごとに確保される per-CPU の統計領域が数多く残り、kernel memory を消費していました。
ここで少し寄り道です。per-CPU とは、kernel が CPU ごとに専用のデータ領域を持つ仕組みです。今回問題になっている memcg の統計情報も、per-CPU の領域へ保存されます。CPU ごとに専用の領域を持つことで、それぞれの CPU が自分の領域へ統計を記録できます。複数の CPU が同じデータを更新する場合と比べて、ロックの競合を避けられるのが利点です。per-CPU の仕組みについては、こちらの記事が分かりやすく説明しています。
per-CPU はロックの競合を避けられる一方、CPU の数だけデータ領域が必要になります。例えば論理 CPU が 4 個ある場合、1 個の memcg に対して、CPU ごとの統計領域も 4 個用意されます。
memcg A
├─ CPU 0 用の統計領域
├─ CPU 1 用の統計領域
├─ CPU 2 用の統計領域
└─ CPU 3 用の統計領域
そのため container の作成などに伴って cgroup が作られるたびに、memcg 用の統計領域も CPU ごとに用意されます。図にすると以下のようになります。
memcg A ─ CPU 0〜3 用の統計領域
memcg B ─ CPU 0〜3 用の統計領域
memcg C ─ CPU 0〜3 用の統計領域
...
memcg X ─ CPU 0〜3 用の統計領域
つまり、必要なメモリはおおよそ「残っている memcg の数 × CPU の数」に応じて増えます。CPU 数の多い container host ほど、この問題の影響が大きくなるわけです。
まあでも、cgroup を削除したときに memcg と per-CPU の統計領域も一緒に解放すれば、メモリ使用量は増えないじゃん!と思うかもしれません。そのとおりです。ところが実際には、cgroup を削除しても memcg が解放されず、dying 状態のまま残ることがありました。その結果、memcg が持つ per-CPU の統計領域も解放されず、cgroup の作成と削除を繰り返すたびに蓄積していきます。
では、なぜ memcg が dying 状態で残るのでしょうか。端的に言うと、page cache に残った page が memcg への参照を持ち続けているからです。
詳しく言いますね。memcg に所属するプロセスがファイルを読み、読み込んだ部分がまだ page cache にない場合、kernel はその内容を page cache に載せます。このとき kernel は、memcg に対して「この page 分のメモリを使いました!」とメモリ使用量を計上します。この動作を charge といいます。
今回扱っている修正前の実装では、page を memcg に charge すると、page から memcg への参照も作られます。これによって memcg の参照カウントが増えます。 そのため、cgroup を削除しても page cache に page が残っていると、memcg の参照カウントは減らず、memcg 自体を解放できません。この問題は、2021 年の patch seriesでも報告されています。
そこで 2022 年に、memcg 自体は解放できなくても、per-CPU の統計領域だけは先に解放するパッチが提案されました。ただし、このパッチは増え続けるメモリ使用量を抑えるための緩和策です。page から memcg への参照は残ったままなので、根本の問題は解決していません。
2023 年にも、共有メモリによって削除後の memcg が残り続ける「zombie memcg」が議論されていました。簡単には倒せないゾンビだったようです。
このシリーズでは、次回にまず 2022 年の緩和策を読み、その次に Linux 7.1 で入った根本修正を追っていきます。簡単には倒せなかったゾンビは、どのように退治されたのでしょうか。乞うご期待!