PostgreSQL 与 OOM Killer:为什么严格的内存超量分配(Strict Memory Overcommit)至关重要
PostgreSQL 与 OOM Killer:为什么严格的内存超量分配(Strict Memory Overcommit)至关重要
PostgreSQL 对 Linux OOM Killer 的脆弱性
单次 OOM (out of memory) kill 可能会导致整个 PostgreSQL 集群崩溃,迫使数据库进行完整的重启和崩溃恢复。 这发生是因为 PostgreSQL 的共享内存架构。主监督进程 (postmaster) 为每个连接 fork 出后端进程,而这些后端进程共享用于 shared buffers、WAL buffers 和 lock tables 的关键内存段。
当 Linux OOM killer 终止一个后端进程以释放内存时,它并不理解这些共享依赖关系。如果一个后端进程在修改共享内存段时被 kill,该段可能会处于不一致的状态。为了防止静默数据损坏,postmaster 会检测到子进程的丢失并立即终止所有其他剩余的后端进程,从而断开所有活动连接并中止所有进行中的事务。随后的崩溃恢复过程可能会导致显著的停机时间,尤其是在高写入量的情况下。
严格的内存超量分配:将灾难转化为常规错误
严格的内存超量分配 (vm.overcommit_memory=2) 通过在早期以优雅的方式让内存分配失败,而不是允许系统过度分配并在稍后崩溃,从而保护 PostgreSQL。
Linux 提供三种超量分配策略:
- 模式 0 (Heuristic): 默认模式。除非单个请求大得离谱,否则内核允许大多数分配。
- 模式 1 (Always): 内核从不拒绝分配。如果物理内存耗尽,OOM killer 会终止进程。
- 模式 2 (Strict): 内核跟踪总的已提交虚拟内存 (
Committed_AS) 并强制执行CommitLimit。任何超过此限制的分配都会立即以ENOMEM错误被拒绝。
对于 PostgreSQL,ENOMEM 是一个常规错误。无法分配内存的后端进程会向客户端报告错误并取消事务,但 postmaster 和所有其他连接都保持不受影响。这通过将系统范围的故障转化为局部、可管理的错误,实现了系统的保护。
案例研究:"phantom memory" 内核漏洞
**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.c 中 move_vma() 函数内的一个字符变更。do_vmi_munmap() 的错误检查从 < 0 (在错误时运行) 变为了 ! (在成功时运行)。
由于条件被反转了,内核在每次成功的内存重映射 (remap) 时都会重新增加已提交内存计数器,而不是仅在失败时。这导致 Committed_AS 随时间单调递增。由于默认的启发式超量分配模式下,内核不使用 Committed_AS 来限制分配,因此该漏洞在模式 0 下是隐藏的;它仅在启用严格超量分配 (模式 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 偏移量,以容 Accommodate 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 killer 终止。然而,这应该只在监控了工作负载的内存特征后进行,以确保 CommitLimit 设置得不会太低,从而触发频繁且不必要的 ENOMEM 错误。