一台内存只有 8GB 的服务器,Linux 内核咬定它已经用掉了 651GB。这不是打错的数字。

Ubicloud 在排查托管 PostgreSQL 的内存不足报错时,撞见了这个读数——同一台机器上,所有进程实际能被记账的内存加起来只有 2.43GB。中间 648GB 的差额,来自 Linux 6.5 内核里一次改错的条件判断,只差一个字符。

Ubicloud 做云托管数据库将近十五年,一直坚持给 PostgreSQL 开启严格内存超订(strict memory overcommit):内存快满时,宁可让内核直接拒绝新分配,也不让操作系统自己挑一个进程杀掉。这次天文数字级的内存占用,逼着他们暂时关掉这个设置,也牵出一段内核 bug 的排查过程。

PostgreSQL 为什么怕被 OOM killer 点名

Linux 默认允许进程申请超过物理内存的地址空间,这叫内存超订。多数时候没事。内存真吃紧到临界点,内核会启动 OOM killer,挑一个进程杀掉腾地方。

普通进程被杀,重启重连就行。PostgreSQL 不一样。

它的主进程 postmaster 给每个连接 fork 一个后端,这些后端共享同一段内存,装着 shared buffers、WAL 缓冲、锁表。OOM killer 不懂这套结构,逮住哪个进程占内存多就杀哪个。

如果被杀的后端正写在共享内存里,这段内存可能陷入不一致状态——共享内存在操作系统层面没有事务保证。postmaster 一旦发现有子进程被杀,就认定整段共享内存已经不可信,于是终止所有后端、断开全部连接,重启后走一遍 crash recovery。这是 PostgreSQL 在保护数据,不是软件脆弱。

代价是,一次 OOM kill 打掉的不是一个连接,是整台机器上的所有连接。写量大时重放 WAL,能拖出很长的宕机。

两种内存失败路径 默认 overcommit Strict overcommit(mode 2) 持续超订内存 物理内存耗尽 OOM killer 杀死某后端 全库重启 + crash recovery 分配将超 CommitLimit 内核直接返回 ENOMEM 该事务取消 服务继续,其余连接不受影响

严格内存超订对应 vm.overcommit_memory=2:内核记着全局已提交内存 Committed_AS,设一条上限 CommitLimit。谁的分配请求会把总量顶过这条线,内核直接返回 ENOMEM,一分都不给。

PostgreSQL 收到这个错误很淡定:取消当前事务,报个错给客户端,postmaster 照样在线,别的连接不受影响。一次事故性宕机,换成了一次日常报错。

这笔交易只在一种场景下划算:机器专门跑 PostgreSQL,外加少数几个认识的 sidecar 进程,commit 的内存曲线可预测,上限才敢往紧了设。混部机器上,别人家的进程随时可能吃掉 commit 额度,PostgreSQL 自己啥都没多干,也可能无辜挨一记 ENOMEM。

651GB 是怎么冒出来的

启用严格超订没几周,Ubicloud 的部分数据库开始报内存不足,可物理内存明明还有富余。查 /proc/meminfo 才发现,那台 8GB 机器的 Committed_AS 折合 651GB,同规格健康机器只有约 2.6GB。

他们先怀疑是 shared_buffers 用了 huge pages 导致重复计账,逐一核对 VMA 标记后排除。把全机器所有可记账内存段加起来,实际只有 2.43GB。648GB 的差额,只能是内核的计数器本身在漏记,越攒越多。

8GB机器的内存账本 651GB Committed_AS 显示值 2.43GB 实际accountable内存 268x 虚增倍数

对比全部机队,规律很清楚:

内核版本Committed_AS/MemTotal 均值最大值比值超过1的机器占比
Linux 6.524.97340523%
Linux 6.80.321.86<1%

统计上,6.5 机器出现内存虚增的概率是 6.8 机器的 52 倍。开机越久涨得越多,每周复合涨幅约 4.7%。6.8 基本不受这类膨胀影响。

顺着代码 history 查下去,问题出在 Linux 6.5 的一次重构:commit 408579c 把 do_vmi_align_munmap() 的返回值约定从"0/1 表示成功、负数表示失败"改成"只有 0 表示成功",但 mm/mremap.c 里 move_vma() 的判断条件,从 < 0 被错改成了 !

本该只在 unmap 失败时才把内存计数补回去的分支,变成了每次 unmap 成功都补一次。一个字符的方向错了,内存账本就跟着每次 mremap 悄悄膨胀,从不清零。Linus Torvalds 后来亲自定位并修复,一行代码,把 ! 改回 < 0

对数据库工程师和技术管理者意味着什么

这次事故对两类人是不同的信号。

数据库与云基础设施工程师:先查自己机器跑的内核版本。6.5 系列存在这个已知问题,如果开着严格超订又跑在 6.5 上,Committed_AS 这个指标已经不可信,不能拿它当扩容或告警依据。能升级到 6.8 或更新的稳定分支,比自己写 workaround 划算。

关注 PostgreSQL 可靠性的技术管理者:这次事故说明严格超订不是配完就能忘的开关,它依赖内核计数器长期准确。如果团队的数据库机器是共享的、多租户的,或者跑着来路不明的常驻进程,严格超订省下的宕机时间,很可能被无谓的 ENOMEM 报错吃回去,这笔账要重新算。

接下来值得盯两件事:Ubicloud 有没有在验证清楚后重新打开严格超订,以及 Linux 上游会不会针对这类 mremap 计数路径补一次专项测试,防止同类问题在其他内核版本复现。


可靠性是把失败提前,不是消灭失败。strict overcommit 的价值判断很清楚:与其等数据库在最坏的时候被判死刑,不如让它提前、频繁地遇到一堆无害的小拒绝。

但这次的内核 bug 提醒了一件更朴素的事:strict overcommit 靠的是一本准确的账。账本本身出错,这套机制立刻反噬——机器明明有富余内存,数据库却被内核拦在门外,报错的原因从"真的没内存"变成"内核算错了"。

凡事预则立,不预则废。strict overcommit 能兜底的前提,是运维方能预判、能观测这台机器上到底跑着谁的内存。机器专用、进程已知、内存行为可预测,三条都满足,这套机制才划算。少了任何一条,它省下的宕机时间,很可能被无谓的报错吃回去。