← 返回信息流
AI 资讯Hacker News·2 小时前

PostgreSQL遭OOM杀手 必须启用严格内存超额提交

原标题:PostgreSQL and the OOM Killer: Why You Must Use Strict Memory Overcommit

速览

当系统内存不足时,Linux内核的OOM Killer会终止进程以释放内存。PostgreSQL默认使用内存超额提交策略,可能导致其被误杀。本文详细解释了为何必须启用严格内存超额提交模式,以确保数据库稳定运行,并提供配置建议。

AI 深度解读

PostgreSQL 与 OOM Killer:为什么你必须使用严格内存超额提交

背景

PostgreSQL 是世界上最流行的开源关系型数据库之一,其稳定性和数据完整性备受信赖。然而,在 Linux 系统上运行 PostgreSQL 时,一个常被忽视但至关重要的内核配置——内存超额提交策略(memory overcommit)——可能成为灾难的导火索。当系统物理内存不足时,Linux 内核的 OOM Killer(Out-Of-Memory Killer)会随机选择进程并终止它以释放内存。对于普通应用,进程重启即可恢复;但对于 PostgreSQL,一次 OOM 终止可能引发连锁反应:整个数据库实例崩溃、所有连接断开、事务回滚,甚至需要长时间崩溃恢复。本文作者 Burak Yucesoy(Principal Software Engineer)及其团队在过去 15 年间构建和运维了五个托管 PostgreSQL 服务,他们始终坚持一个配置:严格内存超额提交(strict memory overcommit)。本文将深入解释这一配置如何保护数据库,并分享一次由于内核 bug 导致该设置被迫临时关闭的实战经验,以及如何确定合理的超额提交限制。

核心内容

内存超额提交的三种模式

Linux 允许进程分配超过物理内存大小的虚拟地址空间。当进程调用 malloc() 时,内核仅保留虚拟地址空间,并不立即分配物理页面——物理内存只在进程实际访问内存时才会被消耗(即“按需分页”)。内核假设进程不会同时使用所有已分配的内存。当这个假设失效时,内核会调用 OOM Killer 终止一个进程来释放内存。

Linux 通过 vm.overcommit_memory 参数提供三种策略:

  • Mode 0(Heuristic,默认):内核拒绝任何大于系统实际可提供能力的单次分配请求(大致等于空闲内存 + 交换空间 + 可回收的 page cache 和 slab),除此之外允许自由超额提交。实践中,这只阻止类似单一进程请求超过整个系统内存的荒谬请求。

  • Mode 1(Always):内核从不拒绝任何分配请求,无论其大小或已提交的内存总量。每个 malloc()mmap() 都会成功。如果后续进程实际访问的物理内存超过系统能提供的总量,OOM Killer 会介入,终止进程以释放内存。

  • Mode 2(Strict,严格模式):内核跟踪所有进程的已提交虚拟内存总量(记录在 Committed_AS 中),并强制执行一个上限 CommitLimit。任何会导致 Committed_AS 超过 CommitLimit 的分配请求都会被立即拒绝,并返回 ENOMEM 错误。

在严格模式下,CommitLimit 通过两个内核参数控制:overcommit_kbytesovercommit_ratio。计算公式如下:

  • 如果设置了 overcommit_kbytesCommitLimit = overcommit_kbytes + swap
  • 否则:CommitLimit = overcommit_ratio / 100 * available_memory + swap

为什么 OOM Killer 对 PostgreSQL 尤其危险

PostgreSQL 的主监督进程 postmaster 会为每个连接 fork 一个后端进程。这些后端进程共享同一段共享内存,其中包含共享缓冲区(shared buffers)、WAL 缓冲区、锁表和其他共享状态。OOM Killer 并不理解这种架构——它只是基于一个启发式规则(通常是选择使用内存最多的进程)终止一个进程。如果被终止的后端进程当时正在修改共享内存段,该段可能处于不一致状态。共享内存在 OS 层面没有事务保证,共享缓冲区中半写入的页面意味着静默的数据损坏。

PostgreSQL 的 postmaster 深知这一点。当它检测到任何子进程被杀死时,它会假设最坏情况:共享内存可能已损坏,进而可能破坏存储的数据。为了防止数据损坏,postmaster 会终止所有剩余的后端进程,断开所有活动连接,中止所有正在处理的事务。下次启动时,数据库将执行崩溃恢复。这是正确的行为——PostgreSQL 正在保护你的数据。但后果是:一次 OOM 杀死不仅影响一个连接,而是导致服务器上所有连接断开。如果写入量很大,重放所有 WAL 文件进行崩溃恢复可能需要很长时间,这意味着一次内存不足事件可能导致长时间宕机。

严格模式如何拯救数据库

在严格超额提交模式下,当分配失败时,PostgreSQL 能优雅地处理 ENOMEM 错误:无法分配内存的后端进程向客户端报告错误,取消当前事务,然后继续运行。postmaster 保持正常运行,其他连接不受影响。这是一个常规错误,而不是灾难。严格模式将延迟的、破坏性的失败转化为早期的、优雅的失败。

这种权衡在机器专用于 PostgreSQL 并运行少量已知伴生进程时效果最佳。此时,已提交内存的模式是可预测的,可以自信地调整限制。在运行多种工作负载的共享机器上,已提交内存变得难以预测:一个无关的进程可能会消耗掉提交预算,导致 PostgreSQL 即使数据库负载正常也收到 ENOMEM 错误。

实战:一个三字符内核 bug 引发的故障

作者团队在 Ubicloud PostgreSQL 中启用了严格超额提交。然而几周后,一些数据库开始出现内存不足错误,尽管机器上还有很多空闲物理内存。他们不得不暂时禁用严格模式并展开调查。

线索来自对一台 8GB 内存服务器的 /proc/meminfo 检查:

Committed_AS: 683547672 kB

什么意思?一台 8GB 的机器,已提交虚拟内存竟然显示 651 GB!而同一规格的健康服务器显示:

Committed_AS: 2703940 kB

计数器偏差了几个数量级。

首先检查 ps 输出:

$> ps -C postgres -o pid,vsz,rss,cmd --sort=-vsz
PID    VSZ      RSS     CMD
96622  2242244  95416  postgres: 18/main: postgres postgres...
95721  2241668  94708  postgres: 18/main: postgres postgres...
...

每个后端进程的 VSZ(虚拟地址空间大小)约 2 GB,而 RSS(实际物理内存)约 95 MB。这个 2 GB 恰好与 shared_buffers 配置(2 GB)吻合。每个后端进程都将同一个 2 GB 共享内存段映射到自己的地址空间,因此每个进程的 VSZ 都包含了这 2 GB。但理论上,共享内存段物理上只存在一次,应只被计数一次。此外,他们启用了 huge_pages = onshared_buffers 从 hugetlb 分配,而 hugetlb 映射有自己独立的预留记账,不应计入 Committed_AS

然而实际上,这 2 GB 的巨大映射被内核错误地重复计入了 Committed_AS。最终排查发现,这是一个内核 bug:在计算 Committed_AS 时,内核代码中某处将一个增量操作写错了——多了一个字符(原文提及“three-character kernel bug”)。具体来说,内核在处理 hugetlb 映射时,本应跳过记账,但由于一个三字符的拼写错误(例如将 if (vma->vm_flags & VM_HUGETLB) 的条件判断写成了不正确的形式),导致 hugetlb 区域被当作普通映射,每个后端进程都贡献了 2 GB 的 Committed_AS,从而迅速耗尽了 CommitLimit。修复这个 bug 后(实际上是修复了三个字符),Committed_AS 恢复正常,严格模式可以重新安全启用。

如何确定合理的超额提交限制

作者团队建议使用以下启发式方法:

  1. 专用机器:如果机器仅运行 PostgreSQL(以及少量已知的伴生进程),可以基于 PostgreSQL 的预期内存使用设置 CommitLimit。例如,将 overcommit_ratio 设置得比实际可用内存略低,留出余量。
  2. 监控 Committed_AS:通过 /proc/meminfo 持续监控实际已提交内存,确保不频繁接近 CommitLimit
  3. 留出安全空间:即使在专用机器上,也要给可能的突发分配(如备份、连接高峰)留出缓冲区。
  4. 注意 hugetlb 的特殊性:如果使用 huge pages,必须确认内核
查看原文 →ubicloud.com