cgroup v2の階層モデルとコントローラ
コンテナの資源制御がなぜv2で一本化されたのかが腑に落ちます。統一階層・委譲・no internal processの制約を設計判断から押さえ、CPU/IO/memoryの効き方を予測できるようになります。
- 1.v1はコントローラごとに別階層を持てたため同一プロセスの所属が分裂し、CPUとメモリの整合した制御が困難だった。v2は全コントローラが共有する単一階層へ統一した。
- 2.親はsubtree_controlで子に渡すコントローラを選び、所有権を移せばサブツリーを委譲できる。委譲先は内側で自由に分割できるが、有効化は親が許したコントローラに限られる。
- 3.no internal process制約により、コントローラ有効時はプロセスを葉にしか置けない。資源競合を内部ノードとリーフの間で起こさないための設計判断である。
なぜ複数階層をやめたのか
cgroup v1 の最大の特徴は、コントローラごとに独立した階層を組めたことでした。CPU 用のツリー、メモリ用のツリー、blkio 用のツリーを別々にマウントし、それぞれで好きな粒度のグループ分けができます。柔軟に見えますが、これは深刻な構造問題を抱えていました。
問題は、あるプロセスがコントローラごとに別のグループへ所属できてしまう点です。プロセス P が CPU 階層では groupA、メモリ階層では groupX に属していると、「P が属するグループ」という概念そのものが一意に定まりません。すると、メモリ逼迫でページキャッシュをライトバックする際に「この I/O はどの CPU 配分・どの I/O 帯域に従うべきか」をカーネルが決められません。メモリ回収が生む I/O を正しい blkio グループへ紐付けられず、コントローラ間で協調が必要な制御が成立しないのです。
cgroup v2 はこの根を断つため、**単一階層(unified hierarchy)**へ移行しました。ツリーは1本だけで、すべてのコントローラがそれを共有します。プロセスの所属は常に1つのノードに定まり、「このプロセスの CPU・メモリ・I/O はすべてこのノードの制約に従う」と一意に言えます。柔軟性を一段手放す代わりに、コントローラ横断の整合性という上位の利得を取った設計判断です。
| 観点 | cgroup v1 | cgroup v2 |
|---|---|---|
| 階層の数 | コントローラごとに別階層を持てる | 全コントローラが単一階層を共有 |
| プロセスの所属 | コントローラごとに異なりうる | 常に1ノードに一意 |
| 横断制御 | メモリ回収I/Oの紐付けが困難 | writeback等をI/O側と協調可能 |
| プロセス配置 | 内部ノードにも置ける | no internal process制約あり |
サブツリー制御の委譲
単一階層では、すべての資源を root 直下で握ると硬直します。そこで v2 は**コントローラの委譲(delegation)**を中心に据えます。鍵となるのが2つのファイルです。あるノードの cgroup.controllers は「ここで利用可能なコントローラ」を示し、cgroup.subtree_control は「子に渡すコントローラ」を示します。
重要なのは、コントローラはトップダウンに親が許した分しか有効化できない点です。親が subtree_control に +memory を書いて初めて、子の cgroup.controllers に memory が現れ、子はさらにその孫へ渡せます。逆に親が渡さなければ、子は配下でメモリ制御を一切有効化できません。この一方向の伝播が、誰がどの資源を再分配できるかを階層に沿って厳密に決めます。
root
cgroup.subtree_control = "cpu memory" ← 子に cpu と memory を委譲
└── workload.slice
cgroup.controllers = "cpu memory" ← 親から渡された
cgroup.subtree_control = "cpu" ← 孫には cpu だけ渡す
├── app-A (cpu は制御可、memory はこのノードで制御)
└── app-B
所有権の観点では、サブツリーの root ディレクトリと cgroup.procs・cgroup.threads・cgroup.subtree_control の所有者を委譲先ユーザーに移せば、非特権ユーザーへサブツリーを委譲できます。委譲先は内側を自由に切り分けられますが、有効化できるコントローラは親が許した範囲に閉じ込められます。systemd の slice/scope や、rootless コンテナがユーザー権限で資源制御を組めるのは、この委譲モデルが土台です。コンテナ基盤としての位置づけは名前空間と cgroupsで扱った隔離と制限の「制限」側の中核にあたります。
委譲は権限の穴になりかねないため、nsdelegate マウントオプションを使うと cgroup 名前空間の境界を委譲境界として扱えます。委譲されたサブツリーの内側からは、cgroup.procs への書き込みでプロセスを境界外(祖先側)へ移すことができず、委譲先が割り当てを抜け出すのを防ぎます。
コントローラはどう協調するか
単一階層の真価は、コントローラ同士が同じノードを参照して協調できる点に現れます。代表的な4つを押さえます。
| コントローラ | 主なインターフェース | 制御するもの |
|---|---|---|
| cpu | cpu.weight / cpu.max | 実行時間の比例配分と帯域上限(クォータ) |
| io | io.weight / io.max | ブロックI/Oの帯域とIOPS。デバイス別に指定 |
| memory | memory.low / high / max | 物理メモリの保護下限・スロットル・硬性上限 |
| pids | pids.max | サブツリーで生成できるプロセス数の上限 |
協調が効く典型がメモリ回収と I/O の連動です。あるノードで memory.high を超えると、そのノード配下でターゲットなメモリ回収が走り、ダーティページは書き戻しを伴います。v2 ではこの writeback が生む I/O が、同じノードの io コントローラの帯域に課金されます。v1 では所属が分裂して不可能だった「メモリ逼迫が生む I/O を、その原因グループの I/O 予算で律する」が成立するわけです。
cpu コントローラは2系統で効きます。cpu.weight は比例配分——競合時に重みの比でCPU時間を割り振る軟性の制御で、競合が無ければ上限なく使えます。cpu.max は QUOTA PERIOD 形式のハードクォータで、競合の有無に関わらず周期あたりの実行時間を絶対的に頭打ちします。weight は実体的な配分をCFS の vruntime 管理へ写像し、重みの大きいノードほど vruntime がゆっくり進んで長くCPUを得ます。pids は最も単純で、fork 爆弾のような暴走をプロセス数という直交した軸で止める安全装置です。
no internal process 制約
v2 を運用すると必ず突き当たるのが no internal process 制約です。あるノードで subtree_control を通じてコントローラを有効化している間は、そのノードに直接プロセスを置けない(cgroup.procs が空でなければならない)というルールです。プロセスを持てるのは、子へコントローラを配っていない葉ノードだけになります。例外は root だけです。
なぜこの制約が要るのか。理由は資源の競合相手を揃えるためです。もし内部ノード N が直接プロセス P を抱えつつ、子ノード C へもコントローラを配っていたら、N の資源を「P という個別プロセス」と「C というサブツリー」が奪い合うことになります。プロセスとサブツリーは粒度が違い、cpu.weight のような比例配分の比較対象として等価に扱えません。「P の重み10」と「サブツリー C の重みは内部でどう積み上がるのか」を整合的に比べる定義が存在しないのです。
v1 感覚で「あるグループにプロセスを入れ、その下にさらに子グループを作って制御を足す」とすると、子で subtree_control を有効化した瞬間に親へのプロセス追加が EBUSY で弾かれます。対処は定石で、プロセスを必ず専用の葉(例:group/leaf や systemd の .scope)へ退避させ、内部ノードは分配点として空に保つことです。
この制約により、各ノードでの競合は**常に同種の単位どうし(子ノードの集合)**に限られ、weight による比例配分や max による上限が曖昧さなく定義できます。柔軟性を削ってでも制御モデルを単純に保つという、v2 全体を貫く設計思想がここに凝縮されています。
まとめ
- v1 の複数階層はプロセスの所属をコントローラごとに分裂させ、メモリ回収 I/O の紐付けなどコントローラ横断の制御を不能にしていた。v2 は単一階層で所属を一意化し、この問題を根絶した。
- 委譲は
subtree_controlによるトップダウンの伝播で決まり、子は親が許したコントローラしか有効化できない。所有権の移譲で非特権ユーザーへサブツリーを安全に委ねられる。 - cpu(weight/max)・io・memory・pids は単一ノードを共有して協調する。メモリの
high超過が生む writeback I/O を同ノードの io 予算へ課金できるのが象徴的な利得。 - no internal process 制約は、内部ノードでプロセスとサブツリーが資源を奪い合う曖昧さを排し、競合を同種の単位に揃える設計判断。プロセスは葉に置く。
OS Article
cgroup v2の階層モデルとコントローラを実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
cgroup
比較で見る軸
難易度: advanced / カテゴリ: OS / タグ数: 5
導入後に効く点
親はsubtree_controlで子に渡すコントローラを選び、所有権を移せばサブツリーを委譲できる。委譲先は内側で自由に分割できるが、有効化は親が許したコントローラに限られる。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- OS
- タグ数
- 5
判断チェックリスト
- 自社の用途が「cgroup / コンテナ」に近いか確認する。
- 強みである「v1はコントローラごとに別階層を持てたため同一プロセスの所属が分裂し、CPUとメモリの整合した制御が困難だった。v2は全コントローラが共有する単一階層へ統一した。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。