PostgreSQLとOOM Killer:なぜ厳格なメモリオーバーコミットが不可欠なのか

PostgreSQLとOOM Killer:なぜ厳格なメモリオーバーコミットが不可欠なのか

PostgreSQLのLinux OOM Killerに対する脆弱性

単一のOOM (out of memory) killは、PostgreSQLクラスター全体をクラッシュさせ、データベースの完全な再起動とクラッシュリカバリを強いる可能性があります。 これは、PostgreSQLの共有メモリアーキテクチャに起因します。メインの監視プロセス (postmaster) は、各接続に対してバックエンドプロセスをフォークし、これらのバックエンドは共有バッファ、WALバッファ、およびロックテーブルのための重要なメモリセグメントを共有しています。

LinuxのOOM killerがメモリを解放するためにバックエンドプロセスを終了させると、これらの共有依存関係を理解せずに実行されます。バックエンドが共有メモリセグメントを修正している最中に終了させられた場合、そのセグメントは不整合な状態になる可能性があります。サイレントなデータ破損を防ぐために、postmasterは子プロセスの消失を検知し、直ちに他のすべての残存するバックエンドを終了させ、すべての有効な接続を切り、実行中のすべてのトランザクションを中断します。その後のクラッシュリカバリプロセスは、特に書き込み量が多い場合に、大幅なダウンタイムを引き起こす可能性があります。

厳格なメモリオーバーコミット:破滅的な事態を日常的なエラーへと変換する

厳格なメモリオーバーコミット (vm.overcommit_memory=2) は、メモリ割り当てを早期かつ優雅に失敗させることで、システムが過剰に割り当てて後でクラッシュするのを防ぎ、PostgreSQLを保護します。

Linuxは3つのオーバーコミット・ポリシーを提供しています:

  • Mode 0 (Heuristic): デフォルト。カーネルは、単一のリクエストが法外に大きい場合を除き、ほとんどの割り当てを許可します。
  • Mode 1 (Always): カーネルは割り当てを拒否しません。物理メモリが不足した場合、OOM killerがプロセスを終了させます。
  • Mode 2 (Strict): カーネルはコミットされた合計仮想メモリ (Committed_AS) を追跡し、CommitLimit を適用します。この制限を超えるいかなる割り当ては、直ちに ENOMEM エラーで拒否されます。

PostgreSQLにとって、ENOMEM は日常的なエラーです。メモリを割り当てられないバックエンドは、クライアントにエラーを報告しトランザクションをキャンセルしますが、postmasterと他のすべての接続は影響を受けません。これにより、システム全体の失敗を、局所的で管理可能なエラーへと変換できます。

ケーススタディ: 「Phantom Memory」 カーネルバグ

Linux 6.5 カーネルにおける微妙なバグにより、Committed_AS がリークし、物理メモリが豊富にある場合でも誤ったOOMエラーが発生するようになりました。

Ubicloudは、一部の8 GBサーバーが650 GBを超えるコミット済みメモリを報告していることを発見しました。分析の結果、カーネル 6.5.0 を実行しているサーバーは、6.8.0 を実行しているものよりも、この膨張が起こる可能性が52倍高く、リークは毎週約4.7%の複利で増加していました。

ルート原因

このバグはコミット 408579c で導入され、mm/mremap.c 内の move_vma() 関数における1文字の変更に起因していました。do_vmi_munmap() に対するエラーチェックが < 0 (エラー時に実行) から ! (成功時に実行) に変更されていました。

条件が反転していたため、カーネルは失敗時のみではなく、メモリの再マップが成功するたびに、コミット済みメモリカウンターを再インクリメントしていました。これにより、Committed_AS が時間の経過とともに単調増加していました。このバグは、デフォルトのヒューリスティック・オーバーコミットの下では隠れていました。なぜなら、カーネルは Mode 0 では割り当てを制御するために Committed_AS を使用しないためです。このバグは、厳格なメモリオーバーコミット (Mode 2) を有効にしている場合にのみ、失敗を引き起こします。

Commit Limit を設定するためのヒューリスティック

厳格なメモリオーバーコミットを安全に実装するためには、commit limit はカーネルのオーバーヘッドとサイドカープロセスの両方を考慮する必要があります。

Ubicloudは、overcommit_kbytes を計算するために以下の式を使用しています:

overcommit_kbytes = (total_memory_kb * 0.75 * 0.8) + 2 * 1048576

計算の明細

  • 80% ルール: 利用可能なメモリの80%がユーザースペースにコミットされます。残りの20%はカーネルのデータ構造 (page tables, slab caches, network buffers) のために予約されています。この20%は無駄ではありません。page cache は回収可能であり、Committed_AS にはカウントされないため、page cache のために引き続き使用されます。これにより PostgreSQL の読み取り性能が向上します。
  • 2 GB バッファ: サイドカープロセス (e.g., Prometheus, node_exporter, wal-g) を収容するために、固定の 2 GB オフセットが追加されます。これらの多くは Go で書かれており、事前に大きな仮想メモリ領域を予約します。Ubicloud のデータによれば、サーバーのサイドカーの 96% がコミット済みメモリを 1 GB 未満に消費しているため、2 GB は安全で寛大な上限となります。
  • Hugepage 調整: 提供された実装では、total memory は、hugepages 用に予約された 25% のメモリを考慮して、最初に 0.75 を掛けます。hugepages は個別の会計方式で処理され、commit limit にはカウントされません。

実装の要約

本番環境の PostgreSQL デプロイメントにおいて、壊滅的な OOM kill を避けるために vm.overcommit_memory=2 を有効にすることが推奨されます。ただし、これは、CommitLimit が頻繁で不必要な ENOMEM エラーを誘発するほど低く設定されないよう、ワークロードのメモリ特性を監視した後にのみ行うべきです。

Sources