cgroup v2のmemory.highとmemory.max: スロットリングとOOMの境界

cgroup v2のmemory.highは回収圧力とスロットリング、memory.maxはOOMにつながるハードリミットです。memory.eventsとmemory.oom.groupも含め、公式ドキュメントに沿って整理します。

Linuxcgroup v2メモリOOM運用

コンテナやサービスにメモリ上限を設定するとき、cgroup v2には memory.high と memory.max という2つのファイルがあります。どちらも「これ以上使わない」という意味に見えますが、超えたときにカーネルがすることは違います。一方はプロセスを遅くし、もう一方はプロセスを終了させることがあります。

この記事では、Linuxカーネルの公式ドキュメント“Control Group v2”(docs.kernel.orgのLinux 7.3.0ドキュメント、2026-10-20確認)だけを根拠に、2つの境界の違いと、その結果を確認するための memory.events、memory.events.local、memory.oom.group を整理します。システム全体のメモリオーバーコミットについてはLinuxのメモリオーバーコミットで別に扱っています。

まずmemory.current

メモリのインターフェースファイルの中で最も基本になるのが memory.current です。ルート以外のcgroupに存在する読み取り専用ファイルで、ドキュメントは「cgroupとその子孫が現在使用しているメモリの総量」と定義しています。子cgroupの使用量は親の値に含まれます。

以下のファイルに共通する前提も押さえておきます。

  • メモリ量はすべてバイト単位です。PAGE_SIZEに揃っていない値を書き込むと、読み戻したときに最も近いPAGE_SIZEの倍数へ切り上げられることがあります。
  • memory.high と memory.max のデフォルトはどちらも max で、上限なしを意味します。
  • これらのファイルはルートcgroupにはありません。ドキュメントの規約では、ルートcgroupはリソース制御の対象外です。
  • 追跡対象はユーザー空間のメモリ(ページキャッシュと匿名メモリ)、dentryやinodeなどのカーネルデータ構造、TCPソケットバッファです。ドキュメント自身が、この追跡は「完全に漏れがないわけではない」と書いています。

memory.high: 超えると遅くなる

ドキュメントは memory.high を「メモリ使用量のスロットル制限」と呼びます。使用量がこの境界を超えると、そのcgroupのプロセスはスロットリングされ、強い回収圧力を受けます。Controller Issues and Remediesの節はもう少し具体的で、割り当てを直接回収(direct reclaim)に回して超過分を解消させる仕組みだと説明しています。

要点は次の2文です。

  1. high制限を超えてもOOM killerは決して呼ばれません。
  2. 極端な条件では、この制限を超えることがあります。

この「極端な条件」について、ドキュメントは多数の同時割り当てとグループ内の回収の進行が完全に行き詰まった場合と説明しています。そのときでも、グループを終了させるより他のグループやシステムの余裕から割り当てを満たすほうがたいてい良い、というのが設計の考え方で、こうしたはみ出しを抑える役割は memory.max に任されています。

そのため memory.high を低くしすぎても、プロセスは終了せず、性能が徐々に落ちていきます。ドキュメントは、ユーザーがそれを観察しながら、許容できる性能を保てる最小のメモリ量が見つかるまで調整できると書いています。

memory.max: 減らせなければOOM

memory.max は「メモリ使用量のハードリミット」で、ドキュメントの言葉ではcgroupのメモリ使用量を制限する主要な仕組みです。使用量がこの値に達して減らせない場合、そのcgroupの中でOOM killerが呼ばれます。

ハードリミットであっても、ドキュメントには次の注意書きがあります。

  • 状況によっては、使用量が一時的に制限を超えることがあります。
  • デフォルト設定では、通常のorder-0割り当ては、OOM killerが現在のタスクを犠牲に選ばない限り常に成功します。
  • OOM killerを呼ばない種類の割り当てもあります。呼び出し側は別の方法で再試行したり、ユーザー空間に -ENOMEM を返したり、ディスクのreadaheadのように黙って無視したりします。

稼働中のcgroupで memory.max を変更するときに特に重要な記述もあります。memory.max に書き込むと、カーネルはまず新しい制限を設定して新たなchargeを止め、その後、新しい制限を満たすまで回収とOOM killを進めます。あるいは memory.max に書き込んだタスク自身が終了させられることもあります。現在の使用量より低い値を書くと、その場でOOM killが起きうるということです。

2つのファイルの比較

項目 memory.high memory.max
ドキュメント上の位置づけ スロットル制限 ハードリミット
超えたとき スロットリングと強い回収圧力 減らせなければcgroup内でOOM killerを呼ぶ
OOM killer 決して呼ばない 呼ぶ(呼ばない割り当ての種類もある)
超過の可能性 極端な条件で超えうる 状況により一時的に超えうる
デフォルト max max
関連イベント memory.events の high memory.events の max、oom、oom_kill

memory.highはkill switchではない

memory.high だけを設定して「これで超えないだろう」と期待するのは、ドキュメントの意図とずれています。ドキュメントは、high制限は「外部プロセスが制限されたcgroupを監視し、強い回収圧力を和らげるシナリオ」で使うべきだと明記しています。

Usage Guidelinesの節も同じ方向です。high制限を超えるとOOM killerではなくスロットリングが起きるので、管理エージェントには監視して対処する余地が十分にあり、たとえばメモリを追加で割り当てたりワークロードを終了させたりできる、と説明しています。同じ節は memory.high をメモリ使用量を制御する主要な仕組みとして挙げ、high制限の合計が利用可能なメモリを上回るオーバーコミットも有効な戦略だとしています。ハードな天井は memory.max、日常的な使用量の調整は memory.high と役割が分かれていると言えます。

ただし、使用量の数字だけでは判断しにくいこともドキュメントは指摘しています。ネットワークから受け取ったデータをファイルに書き込むワークロードは、利用可能なメモリをすべて使うこともあれば、少ないメモリで同じように動くこともあります。メモリが足りているかどうかは、メモリ不足でワークロードがどれだけ影響を受けているか、つまり圧力の度合いで見る必要があります。ドキュメントのメモリインターフェース一覧には、メモリのpressure stall informationを示す memory.pressure ファイルがあります。PSIそのものはLinux Pressure Stall Informationで解説しています。

memory.eventsとmemory.events.local

境界に達したかどうかは memory.events で確認します。ルート以外のcgroupにある読み取り専用のflat-keyedファイルで、値が変わるとfile modifiedイベントが発生します。今回保存した版(7.3.0)で定義されているフィールドは次のとおりです。

フィールド ドキュメント上の意味
low 使用量がlow境界より下なのに、高いメモリ圧力のために回収された回数。通常はlow境界がオーバーコミットされている兆候
high high境界を超えたためにプロセスがスロットリングされ、直接回収に回された回数。グローバルな圧力ではなくhigh制限で使用量が抑えられているcgroupなら、発生するのが想定どおり
max 使用量がmax境界を超えそうになった回数。直接回収で下げられなければcgroupはOOM状態になる
oom 使用量が制限に達し、割り当てが失敗しそうになった回数。失敗したhigh-order割り当てや呼び出し側が再試行を望まない場合など、OOM killerが選択肢にないときは増えない
oom_kill このcgroupに属するプロセスが、種類を問わずOOM killerによって終了させられた数
oom_group_kill グループOOMが発生した回数
sock_throttled このcgroupに関連付けられたネットワークソケットがスロットリングされた回数

high、max、oom、oom_kill はそれぞれ別の段階を数えています。high が着実に増えているのは、memory.high が役目を果たしているだけかもしれません。max はハードリミットにぶつかった試み、oom は割り当てが失敗の手前まで行ったケース、oom_kill は実際に終了したプロセスの数です。oom_kill の定義が「種類を問わずOOM killerによって」となっている点にも注意してください。このcgroupの memory.max 以外が原因のOOMで終了したプロセスも含まれうるということです。

memory.events のフィールドはすべて階層的です。子孫cgroupで起きたイベントも祖先の値に反映され、階層の下で起きたイベントによって祖先のファイルにfile modifiedイベントが発生することもあります。そのcgroup自身で起きたイベントだけを見たい場合は memory.events.local を読みます。形式は同じですが、フィールドは階層的ではなくローカルで、このファイルのfile modifiedイベントもローカルのイベントだけを反映します。

たとえば親 P の下に子 A と B があり、A だけでOOM killが起きたとします。P/memory.events の oom_kill は増えますが、P/memory.events.local は変わりません。どの子で問題が起きたかを探すときは、親の memory.events で範囲を絞り、各子の memory.events.local で場所を特定する、という使い方ができます。

例外が1つあります。cgroup2を memory_localevents マウントオプション付きでマウントすると、memory.events にもサブツリーを含まない現在のcgroupのデータだけが入ります。ドキュメントはこれをレガシーな動作と呼び、オプションなしのデフォルトではサブツリーのカウントを含みます。このオプションはシステム全体に効き、initネームスペースからのマウント時またはリマウント時にしか変更できません。

memory.oom.group: 全部かゼロか、ただし例外あり

memory.oom.group はルート以外のcgroupにある読み書き可能なファイルで、デフォルトは 0 です。設定すると、OOM killerはそのcgroupを分割できない1つのワークロードとして扱います。cgroupに属するタスク(リーフでなければ子孫のタスクも含む)を、まとめて終了させるか、1つも終了させないかのどちらかになります。一部のプロセスだけが終了してワークロードが中途半端な状態で残るのを避けるための機能です。

ドキュメントには限界も2つ明記されています。

  • OOM保護のかかったタスク(oom_score_adj が -1000)は例外で、決して終了させられません。「全部」からはこうしたタスクが外れます。
  • あるcgroupでOOM killerが呼ばれた場合、祖先cgroupの memory.oom.group の値にかかわらず、そのcgroupの外のタスクは終了させません。

memory.oom.group が決めるのは、OOMが起きたときに何をまとめて終了させるかであり、OOMそのものを防ぐものではありません。グループOOMが実際に起きたかどうかは、memory.events の oom_group_kill で確認します。

読み取りだけで行う点検手順

以下のコマンドはファイルを読むだけです。<pid>、<cgroup2-mount>、<cgroup-path> はプレースホルダーなので、環境に合わせて置き換えてください。

# 1. プロセスが属するcgroupのパスを確認(v2のエントリは "0::<cgroup-path>" の形式)
cat /proc/<pid>/cgroup

# 2. cgroup2のマウントポイントとパスをつなげる
CG="<cgroup2-mount><cgroup-path>"

# 3. 現在の使用量と2つの境界
cat "$CG/memory.current"
cat "$CG/memory.high"
cat "$CG/memory.max"

# 4. 階層イベントとローカルイベントの比較
cat "$CG/memory.events"
cat "$CG/memory.events.local"

# 5. グループOOMの設定と圧力情報
cat "$CG/memory.oom.group"
cat "$CG/memory.pressure"

レガシーなcgroupを併用していると /proc/<pid>/cgroup に複数行が出ることがありますが、cgroup v2のエントリは常に 0:: の後にパスが続く形式です。そのcgroup配下に生きているプロセスがあるかどうかは、cgroup.events の populated フィールド(あれば1、なければ0)で確認できます。

memory.peak を読むと、cgroupとその子孫で記録された最大使用量がわかります。ただし、このファイルに空でない文字列を書き込むと、同じファイルディスクリプタからの以降の読み取り値が現在の使用量にリセットされます。点検中は読むだけにしておくのが安全です。

書き込みは実験環境で

次の例は、使い捨てのテスト用VMのような実験環境を前提にしています。本番のcgroupにはそのまま適用しないでください。前述のとおり、現在の使用量より低い memory.max を書き込むと、すぐにOOM killが起きることがあります。

# 実験環境のみ: memoryコントローラが有効になっているテスト用の親の下で実行
cat "<lab-parent>/cgroup.subtree_control"   # 出力に "memory" があるか確認
mkdir "<lab-parent>/memtest"
echo 268435456 > "<lab-parent>/memtest/memory.high"   # 256 MiB、バイト単位
echo 536870912 > "<lab-parent>/memtest/memory.max"    # 512 MiB
echo <test-pid> > "<lab-parent>/memtest/cgroup.procs"
cat "<lab-parent>/memtest/memory.events"

権限と委譲についてドキュメントが定めているルールも確認しておきます。

  • コントローラは親の cgroup.subtree_control で有効になっていなければ子で使えません。また、ルート以外のcgroupは、自分自身のプロセスを持たないときにだけ子へドメインリソースを分配できます。
  • あるディレクトリのリソース制御ファイルは親のリソース分配を制御します。そのため委譲を受けた側は、委譲ポイントにあるそうしたファイルに書き込めないようにします。権限の低いユーザーへ委譲する場合は、ディレクトリと cgroup.procs、cgroup.threads、cgroup.subtree_control への書き込み権限だけを与えます。
  • 非rootのeuidでプロセスを移動するには、移動先の cgroup.procs と、移動元と移動先の共通祖先の cgroup.procs の両方に書き込み権限が必要です。
  • メモリのように状態を持つリソースは、プロセスを移動しても一緒には移りません。ドキュメントは、リソース制限を変えるためにプロセスを頻繁に移動するやり方を推奨していません。

制限事項とバージョンについて

この記事は2026-10-20に、docs.kernel.orgのLinux 7.3.0「Control Group v2」ドキュメントと照らし合わせて確認しました。見えるファイルやフィールドは、カーネルのバージョン、ビルドオプション、マウントオプションによって変わることがあります。サービスマネージャーやコンテナランタイムがこのインターフェースをラップして別の形で公開したり、代わりに管理したりすることもあります。自分のシステムでは、そのカーネルバージョンのドキュメントと使っているツールのドキュメントもあわせて確認してください。この記事では、各機能がどのカーネルバージョンで導入されたかは扱いません。

出典

  • Linux kernel documentation (7.3.0)、“Control Group v2”、2026-10-20確認。参照した節: Mounting、Processes、Controlling Controllers、Delegation、Guidelines、Interface Files (Conventions)、Core Interface Files、Memory Interface Files、Usage Guidelines、Controller Issues and Remedies。

ブログへ