Linux

内存明明够用却触发OOM Killer,如何确认是Page Cache、Slab还是cgroup限制导致的?

👤 为我痴狂 👁 6 阅读 ❤ 0 点赞 ➦ 0 分享 📅 2026-11-04
首页› 大数据› Linux› 正文
内存明明够用却触发OOM Killer,如何确认是Page Cache、Slab还是cgroup限制导致的?

摘要

生产环境中最令人困惑的一类故障,是监控面板上 free -g 仍显示大量可用内存,进程却被内核的 OOM Killer 击杀。本文以"可用内存"与"可回收内存"的语义分离为分析主线,把诱因收敛为三条可验证路径:Page Cache 在特定回收上下文下不可回收、Slab(尤其 dentry/inode 与不可回收 slab)持续膨胀、以及 cgroup v1/v2 内存控制器在层级限额下先于全局内存耗尽触发局部 OOM。文章给出从 /proc/meminfo、/proc/slabinfo、memory.stat 到内核 tracepoint 的完整判定链路,并讨论误判边界与治理策略。

一、问题定义:为什么"内存够用"是一个伪命题

1.1 可用内存的三种语义

讨论 OOM 之前必须先统一口径。Linux 上至少存在三种被口语化为"可用内存"的量:MemFree(完全未分配的物理页)、MemAvailable(内核估算的、在不触发交换的前提下可供新分配使用的内存,包含大部分可回收的 page cache 与可回收 slab)、以及特定 cgroup 的 memory.current 与 memory.max 之差。运维面板常展示 MemAvailable,而 OOM Killer 的触发条件是"在给定内存域(zone)与给定 cgroup 层级上,经过直接回收仍无法满足分配请求"。两者口径不同,于是出现"面板够用、内核却杀进程"的观感冲突。

本文评述:把 MemAvailable 当作 OOM 的预测指标是一种系统性误用。MemAvailable 是启发式估算,其计算依赖内核版本与 zone 的可回收性判断,并不承诺"任何分配都能被满足"。更严谨的做法是把"内存是否够用"拆成两个独立问题:全局物理内存是否耗尽,以及分配上下文所属的 cgroup 是否触顶。二者对应完全不同的排查工具链。

1.2 OOM Killer 的真实触发点

内核在 __alloc_pages_slowpath 路径中,先尝试轻量回收与直接回收(direct reclaim),若仍失败则进入 out_of_memory()。在 cgroup 场景下,若分配受 memcg 约束且该层级已达 memory.max,则走 memcg OOM 路径,选择并击杀该 cgroup 内 oom_score 最高的任务。关键点在于:触发条件是"分配失败",而不是"内存使用率高"。一个长期维持 95% 内存占用但回收顺畅的系统可以稳定运行;一个占用仅 60% 却因碎片、不可回收页或 memcg 限额而分配失败的系统会立刻 OOM。

笔者认为,理解这一点能直接排除大量误诊:OOM 日志中的 total-vm、rss 数值只描述被杀进程的地址空间,不描述系统状态;真正有价值的字段是 Node X Normal free、slab_reclaimable、slab_unreclaimable,以及是否出现 oom_memcg 字样。

1.3 本文的分析主线

全文围绕一条主线展开:"可回收性"是连接内存充足表象与 OOM 事实的唯一桥梁。Page Cache 的问题在于"看起来可回收、实际在特定上下文回收不动";Slab 的问题在于"部分可回收、部分永久驻留";cgroup 的问题在于"全局可回收、局部不可用"。三条路径共享同一套判定逻辑——先确认内存域,再确认回收能力,最后确认限额层级。后续章节按此顺序展开。

二、三条主因的机制拆解:Page Cache、Slab 与 cgroup

2.1 Page Cache:可回收的前提条件

Page Cache 是文件页在内存中的缓存,由 address_space 的 radix tree / xarray 组织。它被计入 Cached,在多数情况下可被 kswapd 或直接回收释放。但"可回收"有严格前提:页未被锁定(PG_locked)、未处于写回中(PG_writeback)、不属于被 mlock 的映射、且所在 address_space 允许释放。以下场景会让 Page Cache 名义上存在、实际上回收失败:

  • 大量脏页堆积,回写带宽成为瓶颈,回收线程在等待 IO 时分配方已超时;
  • 文件被 mmap(MAP_LOCKED) 或 mlock() 锁定;
  • 内存碎片导致即使释放了页也无法拼出高阶连续页,分配仍失败;
  • cgroup 内 page cache 受 memory.high 节流,回收速度被主动限制。

本文评述:把 Page Cache 视为"随时可让出的缓冲"是传统运维经验的惯性,但在写回密集与容器化场景下,这一假设频繁失效。判断 Page Cache 是否为元凶,不能只看 Cached 大小,而要看 Dirty、Writeback 与 pgscan_direct 的增速,三者共同刻画"回收是否跟得上分配"。

2.2 Slab:被低估的驻留者

Slab 分配器为内核对象提供缓存,分为可回收(SReclaimable,如 dentry、inode cache)与不可回收(SUnreclaim,如 task_struct、socket buffer 相关结构)两部分。dentry/inode 在内存压力下可由 shrinker 回收,但回收速率受限于引用计数与并发访问;而 kmalloc 系列与网络栈缓存一旦泄漏或持续增长,会直接消耗不可回收内存。

Slab 导致 OOM 的典型模式是"缓慢泄漏 + 突发分配"。缓慢泄漏使 SUnreclaim 单调上升,系统长期处于高水位;突发分配在某个时刻无法通过回收腾出足够空间,直接触发 OOM。由于 Slab 不计入进程 RSS,常规进程级监控完全看不到它。笔者认为,Slab 类故障的排查价值最高,因为它的证据链最清晰:/proc/slabinfo 中按 object 数排序即可定位异常增长项,配合 slabtop 与内核 tracepoint 可进一步归因到具体子系统。

2.3 cgroup:局部限额先于全局耗尽

cgroup v2 的 memory 控制器以 memory.current、memory.max、memory.high 三个核心接口管理内存。当某 cgroup 的 current 达到 max 且回收失败,内核在该层级内触发 memcg OOM,击杀该 cgroup 内任务,而不影响其他 cgroup。这解释了大量"宿主机内存充足、容器内进程被杀"的现象。

更隐蔽的是层级效应:子 cgroup 的限额之和可能超过父 cgroup 限额,父级先触顶时,子级任务同样会被击杀,但日志归属容易误读。memory.high 则提供软限流,超过后任务被节流并强制回收,若回收不及仍可能升级为 OOM。本文评述:cgroup 类故障的判定核心不是"用了多少内存",而是"在哪个层级触顶、该层级的回收是否被 high 节流、以及 memory.stat 中哪个字段在增长"。只看 memory.current 会漏掉 anon 与 file 的结构差异,而 anon 占比过高意味着回收空间本就有限。

2.4 三者的耦合关系

三条路径并非互斥。典型复合故障是:容器内应用大量读写文件,page cache 在 memcg 内累积并逼近 memory.high,触发节流与回收;回收压力使 dentry/inode slab 增长;最终在某次突发分配中 memcg 触顶触发 OOM,而宿主机 free 仍显示充裕。识别这类故障要求排查顺序固定:先看 OOM 日志归属(全局还是 memcg),再看该域内的结构分布,最后看回收速率。顺序颠倒会导致在错误层级上反复取证。

三、判定路径:从指标到证据的分层排查法

3.1 第一步:读 OOM 日志,确定内存域

内核 OOM 日志(dmesg 或 journal)是唯一权威的现场记录。判定要点:出现 oom_memcg=/xxx 表示 memcg 路径;出现 Node 0 Normal free:...min:... 表示全局 zone 路径;slab_reclaimable 与 slab_unreclaimable 的比值直接提示 Slab 嫌疑度。若日志中 free 远高于 min 水位却仍 OOM,则高度指向碎片、不可回收页或 memcg 限额。

3.2 第二步:区分 Page Cache 与 Slab

在全局路径下,用 /proc/meminfo 与 /proc/vmstat 交叉验证。若 Dirty 与 Writeback 高企、nr_dirty_threshold 被突破,指向 Page Cache 回写瓶颈;若 SUnreclaim 持续增长且 slabtop 显示某缓存对象数异常,指向 Slab。两者可同时存在,此时按增长斜率排序,斜率更陡者为主因。

# 全局结构快照
grep -E 'MemFree|MemAvailable|Cached|Dirty|Writeback|SReclaimable|SUnreclaim' /proc/meminfo
# Slab 对象排序(按 object 数)
slabtop -o -s c | head -20
# 回收速率
grep -E 'pgscan_direct|pgsteal_direct|allocstall' /proc/vmstat

3.3 第三步:确认 cgroup 层级与结构

若日志指向 memcg,进入对应 cgroup 目录读取 memory.current、memory.max、memory.high、memory.stat。重点看 anon、file、slab、pgscan、pgsteal 与 oom 计数。若 anon 占比高,说明可回收空间小;若 file 高但 pgsteal 低,说明回收被节流或受阻。同时沿父目录逐级检查限额,排除父级先触顶。

# cgroup v2 现场取证
cat /sys/fs/cgroup/<path>/memory.current
cat /sys/fs/cgroup/<path>/memory.max
cat /sys/fs/cgroup/<path>/memory.high
grep -E '^(anon|file|slab|pgscan|pgsteal|oom)' /sys/fs/cgroup/<path>/memory.stat
# 沿层级向上确认限额
systemd-cgtop -m

3.4 第四步:用 tracepoint 固化证据

静态指标只能提供嫌疑,动态 trace 才能定性。内核提供 vmscan:mm_vmscan_direct_reclaim_begin/end、kmem:kmem_cache_alloc、oom:oom_score_adj_update 等 tracepoint,配合 perf 或 bpftrace 可观测回收耗时与分配热点。本文评述:在生产环境长期开启全量 trace 成本过高,建议采用"短窗口采样 + 故障复现"策略,仅在疑似时段开启 30–60 秒,避免观测本身引入抖动。

3.5 判定矩阵

证据特征 Page Cache 主因 Slab 主因 cgroup 主因
OOM 日志归属 全局 zone 全局 zone oom_memcg 出现
关键指标 Dirty/Writeback 高 SUnreclaim 单调增 current≈max,anon 占比高
回收行为 pgscan 高但 pgsteal 低 shrinker 调用频繁但释放少 memory.high 节流明显
宿主机 free 低 低 可能很高

四、案例与数据:三类故障的现场还原

4.1 案例一:写回瓶颈下的 Page Cache 假象

某日志聚合节点在夜间批量写入时 OOM。宿主机 64 GiB 内存,free -g 显示 available 约 12 GiB。dmesg 显示全局 zone 触发,slab_reclaimable 正常,但 Dirty 达 3.8 GiB、Writeback 持续 2 GiB 以上。根因是底层存储 IOPS 不足,脏页回写跟不上写入速率,直接回收在等待 IO 时无法及时释放页。本文评述:该案例中 available 的估算包含了大量脏页,而脏页在回写完成前不可释放,正是"估算口径"与"实际可回收性"脱节的典型。治理手段是降低 vm.dirty_ratio 与 vm.dirty_background_ratio,并提升存储带宽。

4.2 案例二:dentry 缓存膨胀

某构建集群在遍历海量小文件后 OOM。slabtop 显示 dentry 对象数达千万级,SReclaimable 占 Slab 总量九成以上。理论上 dentry 可回收,但 shrinker 在并发遍历下回收效率低,且部分 dentry 被引用计数钉住。最终在某次大文件拷贝的突发分配中触发 OOM。笔者认为,这类故障的教训是"可回收"不等于"能及时回收":回收速率与分配速率不匹配时,可回收页同样会演变为事实上的不可用。缓解方式包括控制并发遍历、调整 vfs_cache_pressure,以及在容器内设置合理的内存上限。

4.3 案例三:父 cgroup 限额先触顶

某 Kubernetes 节点上多个 Pod 同时被 OOM,但节点 MemAvailable 仍有 20 GiB。排查发现所有 Pod 位于同一父 cgroup(如 system.slice 或 kubelet 管理的 runtime slice),父级 memory.max 被设置为节点内存的 80%,子级限额之和远超父级。父级触顶后,内核在父级内选择 oom_score 最高者击杀,日志中的 oom_memcg 指向父级路径。本文评述:容器编排系统普遍存在"子限额之和大于父限额"的配置,这是设计上的超卖,但故障时极易误判为单个 Pod 内存泄漏。排查必须沿 cgroup 树自底向上核对每一级 max 与 current 的关系。

4.4 数据说明

上述案例中的数值为笔者在测试环境中复现与整合所得(模拟数据),用于说明指标形态与判定逻辑,不代表特定生产系统。内核行为描述基于 Linux 5.15 与 6.1 主线代码路径,读者应以所用发行版内核版本为准。相关内核文档与回收机制说明可参考 内核内存管理文档 与 cgroup v2 文档。

五、对比、局限与前沿演进

5.1 三类主因的治理成本对比

Page Cache 类问题通常通过参数调优与 IO 扩容解决,成本低但需持续观察;Slab 类问题往往涉及内核对象生命周期,定位难、修复周期长,部分场景需升级内核或打补丁;cgroup 类问题多为配置与编排策略问题,修复快但容易复发。笔者认为,治理优先级应遵循"先配置、后参数、再内核"的顺序,避免在配置错误的前提下做深度内核调优。

5.2 方法局限

本文给出的判定路径依赖 OOM 日志与运行时指标,存在两类局限:其一,容器环境中日志可能被裁剪或丢失,需提前配置内核日志持久化;其二,memory.stat 的字段语义随内核版本变化,跨版本比对需谨慎。此外,内存碎片导致的分配失败难以用单一指标刻画,需结合 /proc/buddyinfo 与 /proc/pagetypeinfo 综合判断。

5.3 前沿演进

近年内核社区在内存回收与观测上的演进值得关注:MGLRU(多代 LRU)在 6.1 后逐步默认启用,改善了 page cache 回收的公平性与延迟;PSI(Pressure Stall Information)提供了比利用率更贴近"分配受阻"的指标,已成为容器内存压力的重要信号;cgroup v2 的 memory.peak、memory.events 让限额触顶更易观测。本文评述:这些机制并未改变 OOM 的触发本质,但显著降低了"证据获取"的成本,工程上应优先接入 PSI 与 memory.events,而非继续依赖 MemAvailable 做告警。延伸阅读可参考 PSI 文档 与 MGLRU 相关讨论。

六、结论与治理展望

"内存够用却 OOM"的本质,是把"内存占用"误当作"内存可用性"。本文以可回收性为主线,将诱因收敛为 Page Cache 回收受阻、Slab 驻留膨胀与 cgroup 局部限额三条路径,并给出"读日志定域—看结构定因—沿层级核对—用 trace 固化"的四步判定法。工程实践中,建议把 PSI、memory.events 与 Slab 对象数纳入常规监控,替代单一的可用内存告警。

展望来看,随着 MGLRU 普及与 cgroup v2 成为默认,回收行为的可观测性将持续改善,但超卖配置与碎片问题不会自动消失。笔者认为,未来的治理重心会从"事后定位"转向"事前容量建模":在编排层显式约束父子限额关系,在应用层控制脏页与元数据缓存的增长速率,才是降低此类故障复发的根本路径。

主要参考文献

  1. Linux Kernel Documentation. Memory Management Guide. kernel.org, 2024.
  2. Linux Kernel Documentation. Control Group v2. kernel.org, 2024.
  3. Linux Kernel Documentation. Pressure Stall Information. kernel.org, 2023.
  4. LWN.net. Multi-generational LRU 系列讨论. 2022–2023.
  5. Brendan Gregg. Systems Performance, 2nd Edition. Pearson, 2020.
  6. Linux 内核源码 mm/vmscan.c、mm/oom_kill.c、mm/memcontrol.c(6.1 主线)。
  7. Red Hat. Memory Management Guide, RHEL 9 文档, 2023.
  8. Google. cgroup v2 与容器内存治理实践白皮书, 2023.

内容仅供学习参考。如需引用,请以原始文献为准。全文约 6800 字 | 参考文献 62 篇(主要 8 篇)。

声明:本文内容仅为作者学习、思考、经验、笔记的总结,仅供技术交流与参考。文中观点仅代表笔者个人思辨,不构成任何学术建议、商业建议或专业建议。所有数据来源已标注,引用时请以原始文献为准。

🔒 复制本站文章内容需登录并达到 L3。当前:未登录

分享到

💬
微信
📷
朋友圈
🐧
QQ好友
🌐
QQ空间
👁
微博
📌
钉钉
🔗
复制链接
📑
复制图文

微信扫一扫分享

打开微信「扫一扫」,扫描二维码后在微信中分享给好友或朋友圈。

💬 评论 (0)

评论功能已关闭

⏸️ 本站暂未开放评论功能,不能进行评论,此为规划的后续开发预留
首页| 关于本网| 网站声明| 联系我们| 网站纠错| 服务| 网站地图
黔ICP备19010680号-1  |  邮箱:six528528@163.com
贵公网安备 52010302001819号
Copyright 2019-2026 http://www.databrush.com/ All rights reserved.
QQ
QQ扫一扫
Logo
DBN数据刷