メモリcgroup(cgroup v2)によるリソース制御の内部
コンテナのメモリ制限が「なぜ効くのか」が腑に落ちます。課金・リクレイム・low/high/maxの階層制限・PSIまで原理から押さえ、OOMやスロットルの挙動を予測できるようになります。
- 1.メモリコントローラはページを最初に触れたcgroupへ課金し、charge/uncharge の単位で使用量を集計する。階層では子の使用が親へ積み上がる。
- 2.制御は4つのつまみで効く。low は回収から守る保護下限、high は超過でリクレイムをかける軟性スロットル、max は突破不可の硬性上限で超えれば OOM、min は決して回収されない保証下限。
- 3.PSI(Pressure Stall Information)はメモリ不足で実行が止まった時間の割合を some/full で公開し、しきい値ではなく「待たされ具合」で逼迫を検知できる。
なぜ「使った瞬間」に課金が決まるのか
メモリコントローラの出発点は**課金(charge)**です。プロセスがページを確保しても、その時点ではまだ物理メモリは消費されません。実際にページへ書き込み、ページフォルトで物理ページが割り当てられた瞬間に、カーネルは「このページは誰の負担か」を1つの cgroup に紐付けます。これが charge で、解放時の uncharge と対で使用量を増減させます。
紐付け先を決める規則は first-touch——確保したプロセスではなく、その物理ページに最初に触れたタスクが属する cgroup に課金されます。共有ライブラリやページキャッシュのように複数 cgroup から使われるページも、所有者は最初に触れた1つに固定されるため、後から触れた側はタダ乗りに見えることがあります。これがコンテナのメモリ会計が直感とずれる典型例です。
課金対象はユーザーページ(匿名メモリとファイルページ)だけではありません。memory.stat を見ると anon・file・slab・kernel_stack・sock などに分かれており、ソケットバッファやカーネルが確保する slab までが同じ予算に乗ります。v1 で別管理だったカーネルメモリが既定で統合されたのが cgroup v2 の重要な設計変更で、これにより「ユーザー空間は制限内なのにカーネル割当で逼迫する」抜け穴が塞がれました。
charge を1ページごとに行うとアトミック操作のコストが無視できません。実装は per-CPU のストックにまとめて先取りし、そこから小分けに引き当てます。memory.current が割当と完全に同期せず多少遅れて見えるのはこのバッチ化のためで、設計上の正常な挙動です。
階層をどう積み上げるか
cgroup v2 は**単一階層(unified hierarchy)**を持ち、すべてのコントローラが同じツリーを共有します。メモリの使用量はこのツリーに沿って積み上がります。あるリーフ cgroup が 100MB 使えば、その値は親、さらに祖父へと再帰的に合算されます。つまり子の合計は親の制限に従属し、親の max を子たちの合計が超えることはできません。
root
└── service.slice max=2G current=1.6G ← 配下の合算
├── app-A max=1G current=0.9G
└── app-B max=1G current=0.7G (A+B=1.6G ≤ 2G)
この階層性のおかげで、上位で大枠の予算を切り、下位で個別配分する入れ子の予算管理が成り立ちます。名前空間と cgroups でコンテナの基盤として触れた cgroups の、メモリ面の中核がこの積み上げ構造です。制御の効き方は次の4つのつまみで決まります。
| つまみ | 性質 | 超過・逼迫時の挙動 |
|---|---|---|
| memory.min | 硬性の保護下限(保証) | この量までは決してリクレイムされない。親の保護を超える要求は切り詰められる |
| memory.low | 軟性の保護下限 | 原則回収対象外だが、他に回収先が無ければ食い込む(ベストエフォート保護) |
| memory.high | 軟性の上限(スロットル) | 超えると積極リクレイムが走り、割当側が待たされる。突破は可能だが減速する |
| memory.max | 硬性の上限 | 突破不可。回収しきれなければ cgroup 内 OOM killer が発動する |
要点は high と max の役割分担です。max だけだと「上限に達した瞬間に OOM で殺す」しかなく、一時的なスパイクでもプロセスが落ちます。high を max より低く置くと、high 到達でまずリクレイムをかけて減速させ、ピークをならしてから本当に駄目なときだけ max で OOM——という二段構えになります。high は割当側のレイテンシを犠牲に安定性を買うバックプレッシャとして働きます。
high 超過時のリクレイムが割当ペースに追いつかないと、プロセスは OOM で殺されもせず、リクレイムのために延々と待たされ続けるライブロック状態に陥ります。memory.events の high カウンタが増え続けるのにスループットが出ない場合がこれで、max を適切に設定して「いずれ OOM で決着する」逃げ道を残すのが安全策です。
リクレイムは何を、どの順で追い出すか
high 超過や max 逼迫が起きると、その cgroup を対象にメモリリクレイム(回収)が走ります。これはシステム全体ではなく、課金がその cgroup(とその配下)に閉じたターゲットリクレイムである点が重要です。あるコンテナが逼迫しても、回収はそのコンテナのページから始まり、無関係なコンテナのページキャッシュを巻き込みません。
回収アルゴリズムの土台は LRU 近似です(ページ置換アルゴリズム)。各 cgroup は active/inactive の2リストを匿名・ファイルそれぞれに持ち、参照されたページを active 側へ、参照の薄れたページを inactive 側へ流し、inactive の末尾から追い出します。ファイルページはストレージへ書き戻すか単に捨てれば回収でき、匿名ページはスワップへ退避できる場合のみ回収できます。スワップが無効なら匿名ページは回収不能で、逼迫の出口は OOM だけになります。
memory.high 超過
→ 対象 cgroup の inactive LRU から回収を試みる
file: ダーティなら writeback、クリーンなら即破棄
anon: スワップ有効なら退避、無効なら回収不可
→ 目標まで回収できれば割当継続(ただし割当側は減速)
→ 回収しきれず max も突破 → cgroup OOM killer
ファイルページの回収はページキャッシュとライトバックと密接です。ダーティページは書き戻し(writeback)を待たないと捨てられないため、I/O が詰まっていると回収が進まず、結果としてメモリ逼迫が I/O 逼迫へ波及します。匿名とファイルのどちらを優先的に潰すかは swappiness で傾けられます。cgroup v1 には memory.swappiness という per-cgroup の専用ファイルがありましたが、cgroup v2 にはそれが無く、原則として全体の vm.swappiness が適用されます(プロアクティブ回収を促す memory.reclaim には、その回収限りの swappiness を引数で渡せます)。
max 突破で発動する cgroup OOM killer は、システム全体ではなくその cgroup のサブツリー内からプロセスを選びます。選定は oom_score に基づき、原則メモリ使用量の大きいプロセスが標的です。memory.oom.group を有効にすると、1プロセスだけ殺すのではなくそのグループ全体を巻き込んで停止でき、半端に生き残ったプロセスによる不整合を避けられます。
PSI:しきい値ではなく「待たされ具合」で測る
memory.current が max に近いかどうかは、逼迫の良い指標になりません。リクレイムが効率よく回って実害が無いこともあれば、まだ上限に余裕があるのに回収のため CPU が空転していることもあるからです。そこで cgroup v2 は PSI(Pressure Stall Information) を提供します。PSI が測るのは使用量ではなく、リソース待ちで実行が止まっていた時間の割合です。
memory.pressure には some と full の2行が現れます。some は「少なくとも1つのタスクがメモリ待ちで stall した時間の割合」、full は「すべての実行可能タスクが同時に stall し、CPU が前進していなかった時間の割合」です。full が高いということは、その間 cgroup は実質的に仕事をしておらず、リクレイムやリフォルトに丸ごと食われていたことを意味します。
| 指標 | 意味 | 読み方 |
|---|---|---|
| some(avg10/60/300) | 誰か1人でもメモリ待ちで止まった割合 | 0でなければ何らかの圧力が始まっている早期サイン |
| full(avg10/60/300) | 全タスクが同時に止まり前進ゼロだった割合 | 高いほど実害大。スループット低下に直結する |
| total(累積マイクロ秒) | stall時間の積算値 | 差分を取れば任意区間の圧力を算出できる |
PSI の本質は、メモリ・CPU・I/O を同じ「stall 時間の割合」という単位で横並びに測れることです。使用量という絶対量では機種やワークロードで意味が変わりますが、「待たされた割合」は機種非依存で比較できます。スケジューラが実行を止めて待たせている時間という観点で、CFS/後継スケジューラの実行可能タスク管理とも接続します。
PSI は読むだけでなく memory.pressure への書き込みでしきい値トリガを登録できます。「直近10秒のうち full が50ms を超えたら通知」のような条件で poll() 待ちでき、OOM が起きてから慌てるのではなく、圧力が一定を超えた段階でユーザー空間が先回りして対処できます。systemd-oomd はこの PSI トリガを使い、カーネル OOM より早く・賢くプロセスを選んで停止する仕組みです。
まとめ
- メモリコントローラは first-touch で課金先 cgroup を固定し、charge/uncharge で使用量を集計する。匿名・ファイルに加えカーネルメモリ(slab・socket)も v2 では同じ予算に統合される。
- 制御は単一階層に沿って積み上がり、4つのつまみ——保護の min/low、減速の high、突破不可の max——で効く。
highでならし、maxで最終的に OOM、という二段構えが安定運用の鍵。 - リクレイムは cgroup に閉じたターゲット回収で、active/inactive LRU を匿名・ファイル別に回し、ダーティページは writeback を待ってから捨てる。スワップ無効なら匿名ページの出口は OOM だけ。
- PSI は使用量ではなく stall した時間の割合(some/full)で逼迫を測り、しきい値トリガで OOM 前に先回りできる。使用量監視より実害に直結した指標になる。
OS Article
メモリcgroup(cgroup v2)によるリソース制御の内部を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
cgroup
比較で見る軸
難易度: advanced / カテゴリ: OS / タグ数: 6
導入後に効く点
制御は4つのつまみで効く。low は回収から守る保護下限、high は超過でリクレイムをかける軟性スロットル、max は突破不可の硬性上限で超えれば OOM、min は決して回収されない保証下限。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- OS
- タグ数
- 6
判断チェックリスト
- 自社の用途が「cgroup / メモリ」に近いか確認する。
- 強みである「メモリコントローラはページを最初に触れたcgroupへ課金し、charge/uncharge の単位で使用量を集計する。階層では子の使用が親へ積み上がる。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。