PostgreSQL 與 OOM Killer:為什麼嚴格的記憶體超額分配(Strict Memory Overcommit)至關重要

PostgreSQL 與 OOM Killer:為什麼嚴格的記憶體超額分配(Strict Memory Overcommit)至關重要

PostgreSQL 對 Linux OOM Killer 的脆弱性

單次 OOM (out of memory) kill 可能導致整個 PostgreSQL 集群崩潰,迫使資料庫進行完整的重啟與崩潰恢復(crash recovery)。 這發生是因為 PostgreSQL 的共享記憶體架構。主監督程序 (postmaster) 會為每個連線派生 (fork) 後端程序,而這些後端程序會共享用於 shared buffers、WAL buffers 和 lock tables 的關鍵記憶體段。

當 Linux OOM killer 終止一個後端程序以釋放記憶體時,它並未理解這些共享依賴關係。如果一個後端程序在修改共享記憶體段時被殺掉,該段記憶體可能會處於不一致的狀態。為了防止靜默資料損壞,postmaster 會偵測到子程序的遺失,並立即終止所有其餘的後端程序,導致所有活動連線中斷並中止所有進行中的交易。隨後的崩潰恢復過程可能會導致顯著的停機時間,特別是在高寫入量的情況下。

嚴格的記憶體超額分配:將災難轉化為常規錯誤

嚴格的記憶體超額分配 (vm.overcommit_memory=2) 透過在早期且優雅地讓記憶體分配失敗,而非允許系統超額分配並在稍後崩潰,來保護 PostgreSQL。

Linux 提供三種超額分配策略:

  • 模式 0 (Heuristic): 預設值。除非單一請求大得離譜,否則核心會允許大多數分配。
  • 模式 1 (Always): 核心從不拒絕分配。如果實體記憶體耗盡,OOM killer 會終止程序。
  • 模式 2 (Strict): 核心會追蹤總共已承諾的虛擬記憶體 (Committed_AS) 並強制執行 CommitLimit。任何超過此限制的分配都會立即以 ENOMEM 錯誤被拒絕。

對於 PostgreSQL,ENOMEM 是一個常規錯誤。無法分配記憶體的後端程序會向用戶端報告錯誤並取消交易,但 postmaster 和所有其他連線仍保持不受影響。這將系統級別的故障轉化為局部、可控的錯誤。

個案研究: 「幻影記憶體」核心錯誤

**Linux 6.5 核心中的一個微妙錯誤導致了 Committed_AS 洩漏,即使在實體記憶體充足的情況下也會導致錯誤的 OOM 錯誤。

Ubicloud 發現某些 8 GB 的伺服器報告了超過 650 GB 的已承諾記憶體。分析顯示,運行 kernel 6.5.0 的伺服器發生此類膨脹的可能性比運行 6.8.0 的伺服器高出 52 倍,且洩漏量每週以約 4.7% 的複利增長。

根本原因

該錯誤是在 commit 408579c 中引入的,核心在 mm/mremap.cmove_vma() 函數中進行了一個單一字符的更改。對於 do_vmi_munmap() 的錯誤檢查從 < 0 (發生錯誤時執行) 變成了 ! (成功時執行)。

由於條件被反轉了,核心在每次 成功 的記憶體重新映射 (remap) 時都會重新增加已承諾的記憶體計數器,而不是僅在失敗時增加。這導致 Committed_AS 隨著時間推移而單調遞增。這個錯誤在預設的啟發式超額分配模式下是隱藏的,因為核心在模式 0 下不使用 Committed_AS 來限制分配;它僅在啟用嚴格超額分配 (模式 2) 時才會導致失敗。

設定 Commit Limit 的啟發式方法

**為了安全地實施嚴格的超額分配,commit limit 必須同時考慮核心開銷與 sidecar 程序。

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,這能提高 PostgreSQL 的讀取性能,因為 page cache 是可回收的,且不計入 Committed_AS
  • 2 GB 緩衝區: 增加一個固定的 2 GB 偏移量以容納 sidecar 程序(例如 Prometheus、node_exporter、wal-g)。其中許多是用 Go 語言編寫的,Go 會預先保留大量的虛擬記憶體區域。Ubicloud 的數據顯示,96% 的他們的伺服器 sidecar 消耗的已承諾記憶體少於 1 GB,因此 2 GB 是一個安全且寬裕的上限。
  • 大頁面 (Hugepage) 調整: 在提供的實作中,總記憶體首先乘以 0.75,以考慮 25% 的記憶體被預留給 hugepages,而 hugepages 由獨立的計數進行處理,不計入 commit limit。

實作總結

對於生產環境的 PostgreSQL 部署,建議啟用 vm.overcommit_memory=2 以避免災難性的 OOM kills。然而,這應該僅在監控了工作負載的記憶體特性後才進行,以確保 CommitLimit 沒有被設定得太低,以至於觸發頻繁且不必要的 ENOMEM 錯誤。

Sources