为什么 Linux 的 OOM Killer 总是杀掉你不想杀的进程

本文深入讲解 Linux OOM Killer 的运作机制,分析为什么重要进程常被误杀,介绍 oom_score 与 oom_score_adj 的调整方法,并对比不同容错方案,给出生产环境下保护关键进程的实战建议。

一次再常见不过的“失手”

在 Linux 服务器上工作久了,总会有那么一次类似经历:某个凌晨,数据库节点突然失联,登上去看时,却发现系统刚刚经历了一次“内存耗竭”,进程被 OOM Killer 直接杀掉。更气人的是,被选中的不是那个疯狂吃内存的跑批任务,而是你反复叮嘱要保护的数据库主进程,或者一堆看起来占不了多少内存的微服务实例。

AI technology illustration

这几乎是每代运维都要被教育的问题:为什么 OOM Killer 总是杀掉你想保护的进程?它到底凭什么做选择?以及,有没有办法让内核在紧急时刻“懂事”一点。

先把结论放前面:OOM Killer 并不按你以为的“内存占用排行榜”来杀。它基于一套可调校的内核算法,而这套算法的默认权重,往往对长时间运行的守护进程并不友好。这就是“杀错”的根源。

OOM Killer 是怎么选对象的

当 Linux 的内存分配无法满足内核的紧急请求时,会触发 out_of_memory(),随后在系统进程列表中挑选一个进程进行释放。这个选择依赖于每个进程的 oom_score,也就是内核给每个进程打的“可牺牲分数”。分数越高,越容易被选中。通常这个分数与进程当前的内存占用、swap 使用、运行时长、是否特权进程、以及是否有大量子进程等因素有关。

但这里有一个关键误区:大多数人以为 oom_score 就是按 RSS(常驻内存集)排排坐。实际上,内核计算时用的是“估算的每个进程的内存占用量”,并叠加了诸多修正系数。例如,很多短时创建但分配了巨大虚拟内存的进程,或者占用大量页表内存的进程,都会获得意想不到的高分。

从 Linux 源码的 badness() 函数可以看出,打分主要考虑以下几个方面:

  • 进程当前使用的内存大小,包括 RSS、swap、以及页表等内核开销;
  • 进程的 nice 值,高优先级进程分数会降低;
  • 进程是否以 root 权限运行,root 进程会有轻微减分;
  • 进程的父进程/子进程也会影响最终分数,子进程的内存会被累加到父进程上;
  • 用户态可以通过 oom_score_adj 直接调整分数。

所以,一个由 root 启动的长寿进程,只要它占领了系统大部分内存,依然可能拿到全体最高分。尤其像 Java 应用这种占满堆内存、又 fork 出多个 worker 线程的,在 OOM 发生时往往首当其冲。

为什么被杀的总是“重要进程”

你以为的重要进程,内核并不认识。数据库或者 JVM 这类进程,通常由 root 启动、运行时间很长,而且会占有大量内存。按内核的“经验”,它们确实应该被保护,但实际计算时,内存占用始终是核心权重。一个长期占满系统内存大半的应用,即使运行时间再长,总分仍然很高。同时,内核的“内存占用”估算会比 RSS 更激进,共享内存、映射的文件页、甚至写时复制的潜在内存都可能被算进去。这在很多情况下会把 Java 的分数推到很高,最终被选为牺牲者。

另一个常见场景是容器内。当容器或 cgroup 的内存 limit 被触及,会触发一次 cgroup 级别的 OOM,此时内核只会从该 cgroup 中选择进程。如果容器内只有一个 Java 进程,那就别无选择。这时你看到的是“Java 被杀”,但日志显示的是 cgroup 内存超限,而不是系统整体内存不足。

还有一个容易被忽略的原因:内核并不是按瞬间的 RSS 来排,而是按“估算的内存占用”做“微积分”。这个估算会包含 mmap 文件、page cache 中与进程相关的页。所以某些执行了大规模文件映射的应用(比如日志搜索、数据分析)的 oom_score 会被高估,而它们实际上大部分内存是可回收的。这种“虚胖”也让很多按正常流量配置的服务被误杀。

先查再调:确认你的进程是怎么被选中的

拿到一台发生 OOM 的机器,第一件事不是重启,而是翻日志。内核会在系统日志里留下完整决策信息:

dmesg | grep -i oom

输出里会写明被杀进程的 PID、comm、score 以及当时的内存状态。例如:

Out of memory: Killed process 12345 (java), score 1250, total-vm: 8192MB

如果想实时查看某个进程当前的 oom_score 和调整值,可以直接读取 /proc:

cat /proc/<pid>/oom_score
cat /proc/<pid>/oom_score_adj

oom_score_adj 的默认值是 0,取值范围 [-1000, 1000]。设置 -1000 表示完全禁杀,1000 表示强制优先被杀。注意,即使一个进程被设成 -1000,也不意味着它不会因为极端内存压力而挂掉,只是它绝不会是 OOM Killer 主动选择的那个。

如何避免“杀错”:方案对比

面对这个问题,几乎所有应急方案都围绕一个目标:让 OOM Killer 在关键时刻知道哪些进程应该保留。具体怎么做,取决于你的环境。下面这张表列出了几种常见思路的差异。

保护方案 操作难度 适用场景 局限性
设置 oom_score_adj 系统级核心进程,如数据库、配置中心 需要覆盖所有相关子进程,无法根治内存不足
调整 vm.overcommit_memory 允许拒绝潜在超大内存申请的系统 可能改变应用行为,需要稳定性评估
增加 swap 或压缩内存 内存波动较大的业务 治标不治本,swap 会导致 IO 延迟上升
配置 cgroup 内存限制 容器化、微服务架构 需要精确估算内存上限,否则容易频繁 OOM
优化应用内存模型 大内存应用,如 JVM、缓存服务 需要改代码/配置,周期长

从投入产出比看,第一项( oom_score_adj )是最推荐的启动动作,因为它的成本低、效果直接。但要注意,只设置 main 进程不够。如果你的服务用 supervisor 或 systemd 拉起了多个子进程,必须对所有子进程同样设置。更稳妥的做法是在 systemd service 里全局配置:

[Service]
OOMScoreAdjust=-800

这样所有由 systemd 生成的进程都会继承这个分数调整。

几个常见的坑和判断

这里想多说几个实践中容易踩的坑。第一个坑是“一设就完事”。有些团队为了省事,把核心进程的 oom_score_adj 直接改成 -1000,觉得这样就能永垂不朽。可内存申请失败并不等于进程被杀。当系统内存耗尽,进程在分配内存时阻塞或者触发内核的直接 reclaim,可能长时间无法响应,最终被健康检查判定为不健康而重启。所以 -1000 是最后的保护,不是不死的金钟罩。

第二个坑是“只调分、不查因”。你可能把数据库保护得很稳,但系统内存依然会耗尽,OOM Killer 会挑选其他软件来杀。如果被杀的是消息队列或者监控代理,同样会影响业务。所以分数调整只是让牺牲品从“核心”变为“边缘”,不等于不牺牲。只有把内存预算分配到每台机器,并设定合理的告警,才能降低整体风险。

第三个坑是“拿物理内存容量来推算”。很多团队买 128GB 内存的机器,就以为能同时跑 80GB 数据库和 40GB 缓存。这里忽略了 page cache 和内核本身对内存的占用,也忽略了内存碎片和不可回收页。当物理内存未满、但 available 很少时,也可能触发 OOM。检查内存是否紧张,应该看 /proc/meminfo 里的 MemAvailable,而不是 MemFree。

怎么落地:一个务实的检查清单

如果想在团队里把这件事做成规范化操作,不妨从这几条开始:

  • 确认关键服务进程的 oom_score_adj,把它设定在 -400 到 -800 范围内,既避免直接禁杀,也远离被选中。
  • 给可以牺牲的批处理任务、临时采集进程设置正值,让内核优先选择它们。例如统一通过 systemd 设置 OOMScoreAdjust=300。
  • 在监控系统里采集所有主机的 MemAvailable 和 oom_score,设置至少两个告警阈值,比如 15% 和 5%,而不是只监控内存使用率。
  • cgroup 环境里设置 memory.oom.group=1,这样 OOM 时会杀掉同组所有进程,避免留下一个半残死循环,反而更难排查。
  • 定期复盘 OOM 事件,检查当日是否有突发的内存申请,或者应用版本升级导致的内存膨胀,把每次 OOM 当成一次容量分析。

这些条目做起来并不难,难的是坚持形成习惯。但和“半夜被叫起来重启数据库”相比,这点投入非常划算。

小结

回到标题的问题:OOM Killer 并不是故意和你作对,它只是在一个极端情况下,基于一套不完全符合业务认知的评分规则做选择。要想让它“懂事”,你得主动告诉它哪些进程更重要。这个“告诉”就是 oom_score_adj,以及配合系统的内存规划一起使用。

最后说一句实在话:任何 OOM 调整都只是兜底手段。只要你的服务还在频繁经历内存耗尽,就说明预算出了问题。与其一直研究怎么选受害者,不如把这个问题写进容量规划和架构设计里,让系统尽量少走到那一步。

原创文章,作者:fudengji,如若转载,请注明出处:https://fudengji.cn/article/564/

(0)
上一篇 1小时前
下一篇 45分钟前

相关推荐