OOM Killerとメモリ過剰コミットの設計
Linuxはメモリを実体より多く約束し、足りなくなるとプロセスを殺す。なぜそんな設計なのか、誰が犠牲になるのかを押さえれば、本番で突然プロセスが消える事故の原因と対策が腑に落ちます。
- 1.Linuxはmalloc等の確保要求に対し、実際の物理メモリ+スワップより多くを約束する過剰コミットを既定とする。多くのプロセスが確保した分を使い切らない統計に賭けることで、メモリ利用率を高めている。
- 2.overcommit_memory(0=ヒューリスティック/1=常に許可/2=厳格会計)で約束の方針が変わる。2にすると確保時点で上限を超える要求を弾けるが、実メモリより少ない約束しかできず空きを遊ばせやすい。
- 3.実メモリが本当に枯渇するとOOM Killerが起動し、oom_scoreが最大のプロセスを殺してメモリを回収する。スコアはメモリ使用量を基準にoom_score_adjで調整でき、cgroup v2のmemory.maxはプロセス群単位で逼迫を局所化する。
なぜメモリを「過剰に約束」するのか
malloc や mmap が成功して返っても、その時点で物理メモリが確保されたわけではありません。Linux はアドレス空間に 仮想的な予約 を与えるだけで、実際の物理ページは最初にそのアドレスへ書き込んだ瞬間のページフォルトで初めて割り当てられます。この遅延割り当ての仕組みは デマンドページングとページフォルト処理 の通りです。
ここで設計上の判断が生じます。確保要求の合計が物理メモリ+スワップを超えてもよいか という問題です。Linux は既定で「超えてよい」とします。これが メモリオーバーコミット(過剰コミット) です。
プロセスは確保した仮想メモリを 全部は使い切らない ことが多い。fork 直後の子は親のページを コピーオンライト で共有し書き込むまで実体を持たない。疎な配列、予約だけして埋めないバッファ、大きく確保して一部しか触れないヒープ。これらの「約束したが触らないページ」を見越して、実体より多く約束する方が物理メモリの利用率が上がる、という賭けが過剰コミットの根拠です。
賭けである以上、外れることがあります。多くのプロセスが約束した分を一斉に書き込み始めると、遅延割り当てが連鎖し、物理メモリとスワップが本当に底をつきます。確保(約束)の時点では足りていたのに、書き込み(実体化)の時点で足りなくなる ——この時間差こそが過剰コミットの核心であり、後述の OOM Killer が必要になる理由です。
overcommit_memory:約束の会計方針
どこまで約束を許すかは vm.overcommit_memory で切り替えます。これは「確保要求を会計上どう扱うか」の方針であって、実メモリの逼迫時の挙動(OOM Killer)とは別レイヤである点に注意してください。
| 値 | モード | 確保要求の扱い | 性質 |
|---|---|---|---|
| 0 | ヒューリスティック(既定) | 明らかに無茶な要求だけ即座に拒否し、それ以外は許可 | 実用上ほぼ通る。逼迫はOOMで後処理 |
| 1 | 常に許可 | 会計を一切せず全要求を成功させる | 確保は絶対に失敗しない。逼迫は完全にOOM任せ |
| 2 | 厳格会計(never overcommit) | コミット総量に上限を設け、超える要求を確保時点で拒否 | mallocがNULLを返しうる。OOMを避けやすい |
値 2 の上限は CommitLimit = スワップ総量 + 物理メモリ × overcommit_ratio / 100(あるいは overcommit_kbytes の絶対値指定)で決まります。現在の約束済み総量は /proc/meminfo の Committed_AS で確認できます。
2 にすると「確保が通った=書き込んでも OOM にならない」を保証でき、突然プロセスが殺される事故を避けやすくなります。代償は 保守性の高さ です。overcommit_ratio が控えめだと、物理メモリより少ない総量しか約束できず、空き物理メモリを抱えたまま malloc が失敗します。さらに fork する大規模プロセス(巨大ヒープを持つ親が小さな子を起こす)が、コピーオンライト前提なのに「親と同量のコミット」を会計上要求して弾かれる、という現実的な不便も起きます。多くの汎用システムが既定の 0 のままなのはこのためです。
メモリ逼迫時のリクレイム経路
物理メモリが減ってくると、OS はいきなりプロセスを殺すのではなく、まず リクレイム(回収) で空きを作ろうとします。OOM Killer はこの回収が破綻したときの最終手段です。経路を順に追います。
メモリ確保要求(ページフォルトやカーネル確保):
空きページがウォーターマーク以上 → そのまま割り当て
空きが low ウォーターマーク未満:
1. 非同期回収: kswapd を起こし、背後で回収させる
2. 同期回収(direct reclaim): 確保しようとした側自身が回収に従事
- クリーンなファイルキャッシュページ → 即破棄して回収
- ダーティページ → 書き戻してから回収
- 無名ページ(ヒープ等) → スワップへ追い出して回収
3. 回収しても min ウォーターマークを割る → OOM Killer 起動
回収対象の選別、すなわち「どのページを追い出すか」は ページ置換アルゴリズム(LRU・Clock・WSClock) のロジックそのものです。Linux は active/inactive の LRU リストでホット/コールドを判定します。ここで効くのが ページの素性 です。ファイル由来のクリーンなページは捨てるだけで回収できますが、書き換え済みのダーティページは ページキャッシュとライトバックの仕組み の通り書き戻しが先に必要で、回収コストが高くなります。
無名ページ(ヒープやスタックの実体)は スワップがあって初めて追い出せます。スワップを無効にした構成(多くのコンテナ/Kubernetes ノードが該当)では、回収できるのは事実上クリーンなファイルキャッシュだけになります。無名ページが膨らんだ状態でキャッシュを回収し尽くすと、回収手段が尽きて即 OOM Killer に到達します。「スワップを切ると速い」は、逼迫時の逃げ道を捨てているのと表裏一体です。
OOM Killer:誰を犠牲にするか
回収が破綻し、確保要求を満たせないと判断されると OOM Killer が起動します。その仕事は「最小の犠牲で最大のメモリを取り戻す」プロセス選定です。判断基準が OOMスコア(oom_score) です。
スコアの本質は単純で、そのプロセスがどれだけメモリを使っているか(おおむね物理メモリ+スワップの使用量に比例)が土台になります。たくさん使っているプロセスを殺すほど多く回収でき、かつ「暴走して食い潰した犯人」である確率が高い、という発想です。各プロセスの現在値は /proc/<pid>/oom_score で読めます。
この生スコアを人間/運用の意図で上下させるのが oom_score_adj です。
| 設定 | oom_score_adj の値 | 効果 |
|---|---|---|
| 絶対に殺さない | -1000 | 実質的に選定対象から除外(保護) |
| 殺されにくくする | 負の値(例 -500) | 生スコアから減算され順位が下がる |
| 標準 | 0 | メモリ使用量どおりに評価 |
| 真っ先に殺させる | 正の値(最大 1000) | 生スコアに加算され最優先の犠牲に |
選定ロジックは「oom_score が最大のプロセスを殺す」が基本ですが、いくつか重要な補正があります。
oom_score_adj = -1000は完全保護:監視デーモンや SSH など「これが死ぬと復旧不能」なプロセスを守るのに使います。ただし保護されたプロセスばかりが残ると、回収しきれず最終的に カーネルパニック に至る場合があります。- 犠牲の波及:選ばれたプロセスを殺すとき、同じスレッドグループは巻き込まれます。狙いは「メモリを丸ごと解放できる単位」を殺すことだからです。
- init(PID 1)等は対象外:これを殺すとシステムが終了するため、選定から除かれます。
スコアはメモリ使用量主体なので、正常に大量のメモリを使う本命プロセス(DB やアプリ本体)が、暴走した小さなプロセスより先に殺されることがあります。守りたいプロセスには oom_score_adj を負に、生贄にしてよい補助プロセスには正に振る、という明示的な意思表示が運用上は要ります。何もしなければ「一番太ったプロセス」が機械的に選ばれます。
cgroup によるOOMの局所化
ここまではシステム全体(グローバル)の逼迫でしたが、現代のサーバーでは cgroup v2 によってメモリ逼迫を プロセス群単位 に閉じ込めるのが実務の主流です。
memory.max を設定したグループは、その上限を超えてメモリを使えません。グループ内の合計使用量が memory.max に達すると、まず そのグループ内だけで リクレイムが走り、回収しきれなければ そのグループ内のプロセスだけを対象に OOM Killer(cgroup OOM)が起動します。システム全体にはまだ空きがあっても、上限を超えたグループの中で犠牲が選ばれる——これが 局所化 の利点です。
memory.max は ハードリミット:到達したら回収、回収できなければグループ内 OOM。memory.high は ソフトリミット:超えると確保を行うプロセスを意図的に遅延(スロットリング)させ、回収の圧力をかけるが、それ自体では OOM を起こしません。memory.high で「太りすぎる前に減速」させ、memory.max を最後の安全弁にする、という二段構えが定石です。Kubernetes のメモリ limit はこの memory.max に対応します。
cgroup OOM の挙動は、グローバル OOM と同じく「グループ内で oom_score が最大のプロセスを殺す」です。つまり過剰コミット・リクレイム・OOM 選定という三層の仕組みが、グローバルとグループの両スコープで相似形に働きます。コンテナで「ノード全体には余裕があるのに特定 Pod だけ繰り返し OOMKilled される」のは、まさにこの局所化が正しく働いている証拠です。
(1)malloc 成功は約束であり物理割り当てではない。実体化は最初の書き込み時のページフォルト。(2)過剰コミットは「全部は使わない」統計への賭けで、overcommit_memory の 0/1/2 が会計方針を決める。(3)逼迫時はまず回収(kswapd・direct reclaim)、破綻して初めて OOM Killer。(4)OOMスコアはメモリ使用量主体、oom_score_adj(-1000で完全保護〜+1000で最優先犠牲)で調整。(5)cgroup v2 の memory.max はOOMをグループ内に局所化する。この5点が頻出です。
まとめ
- 過剰コミット は、確保(約束)と書き込み(実体化)の時間差を利用し、物理メモリ+スワップより多くを約束してメモリ利用率を上げる設計。
vm.overcommit_memoryの 0(ヒューリスティック)/1(常に許可)/2(厳格会計)で方針が変わり、2 は安全だが空きを遊ばせやすい。 - 実メモリが逼迫すると、いきなり殺さず リクレイム で回収を試みる。クリーンなファイルキャッシュは破棄、ダーティページは書き戻し、無名ページはスワップへ。スワップ無効環境では逃げ道が狭く OOM に直結しやすい。
- 回収が破綻すると OOM Killer が
oom_score(メモリ使用量主体)最大のプロセスを殺す。oom_score_adjで保護(-1000)や生贄化(+1000)を明示できる。 - cgroup v2 の
memory.maxは逼迫とOOMをプロセス群単位に局所化し、memory.highのスロットリングと二段構えで暴走を封じ込める。
土台となる遅延割り当ては デマンドページングとページフォルト処理 と 仮想記憶(ページング)、回収側の選別ロジックは ページ置換アルゴリズム(LRU・Clock・WSClock) も合わせて読むと、メモリ逼迫時の挙動が一本の線で繋がります。
OS Article
OOM Killerとメモリ過剰コミットの設計を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
OOM Killer
比較で見る軸
難易度: advanced / カテゴリ: OS / タグ数: 6
導入後に効く点
overcommit_memory(0=ヒューリスティック/1=常に許可/2=厳格会計)で約束の方針が変わる。2にすると確保時点で上限を超える要求を弾けるが、実メモリより少ない約束しかできず空きを遊ばせやすい。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- OS
- タグ数
- 6
判断チェックリスト
- 自社の用途が「OOM Killer / オーバーコミット」に近いか確認する。
- 強みである「Linuxはmalloc等の確保要求に対し、実際の物理メモリ+スワップより多くを約束する過剰コミットを既定とする。多くのプロセスが確保した分を使い切らない統計に賭けることで、メモリ利用率を高めている。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。