想象你经营着一家装满货物的仓库,货架空间就是物理内存,每一箱货物就是一个正在运行的进程。正常情况下,仓管员老周(内核)会定期把空纸箱(页缓存)回炉、把不常用的货搬到楼外的临时棚(swap 交换区)。可当订单突然暴增、老板又提前签了超出仓库容量的空头支票(overcommit)时,货架瞬间被塞满,连纸箱都没得扔、外棚也堆不下。眼看仓库就要爆仓,老周只能咬咬牙做一件残酷的事:挑几箱“最不划算”的货直接丢掉,保住整间仓库——这就是 OOM Killer(Out Of Memory Killer)的由来。它丢谁、怎么丢、能不能提前避免,就是我们要聊的管理机制。
对 AI 从业者而言,模型训练和推理服务常因内存失控被内核“误杀”,理解 OOM 的触发与打分机制,是写出稳定、可运维的 AI 服务的必备基本功。
可视化演示
展示Linux OOM Killer的进程打分策略和内存监控预警,包含oom_score排名和内存使用趋势与OOM阈值线。
本节配有交互动效(进入视口后自动加载,约 3KB)。
OOM进程打分 OOM Killer按oom_score对进程排序 红色标记分数最高即将被杀的进程 oom_score = f(mem, cpu, time) 代价最小策略选择终止影响最小的进程
OOM触发机制
仓库的货架旁立着一块电子秤,上面显示的不是“剩余面积”而是“预估可用面积”——因为有些空间虽然空着,但旁边堆着随时可能倒下来的备用货架,不能算作真正可用。内核里这个读数叫 MemAvailable,它不只看空闲页,还会把“当下能立刻回收的页缓存”也算进去,同时扣掉一层“保命水位”(low watermark,内核为自己预留的最低水位线)。当这个读数归零,分配内存的请求又迟迟得不到满足,才轮到 OOM Killer 出场。
技术上的触发路径是这样的:进程申请内存时调用 __alloc_pages_nodemask,内核先去空闲链表找页,找不到就唤醒回收线程尝试压缩和回收,回收仍然颗粒无收,最终走进 out_of_memory() 分支。值得警惕的是 overcommit(超额承诺):内核默认允许进程申请超过物理内存的虚拟空间,就像老板先开空头支票,等客户真的来提货时仓库才会暴露缺口。
当电子秤读数跌破预警,老周会先做两轮“温和处理”:扔纸箱、搬外棚。如果扔完搬完还是不够,他才去翻那本“优先丢弃清单”——OOM Killer 并不是一言不合就动手的暴脾气,而是穷尽所有常规手段后的最后裁决。
- 空闲货架
MemFree是当前完全空闲的物理内存页,相当于仓库里没有放任何货物的空货架面积。 - 可回炉纸箱
reclaimable_page_cache是可以被回收的页缓存,比如只读文件的缓存页,扔掉后下次需要时再读磁盘就行。 - 保命水位
low_watermark是内核为紧急分配预留的不可动用余量,相当于每个货架底部画着红线,红线以下谁都不能碰。
内存回收与OOM
老周的第一反应永远是“能回炉就回炉”:把干净的页缓存(读过的文件、程序镜像)直接扔掉,脏页写回磁盘后再扔,匿名页则整页搬到楼外临时棚(swap)。搬走的代价是时间——换入换出都要读写磁盘,像从外棚搬货回货架要花几趟人工一样。可有些货是焊死在货架上的:被内核锁定的页面(比如 mlock 锁定的内存)、内核核心代码驻留页,这些连老周也挪不动,称为“不可回收页面”。
如果可回收的纸箱已经扔光、外棚也堆满了,还是无法满足一个新订单,老周才走向那个放着红按钮的角落。内核的判断逻辑很直接:先按优先级遍历各个内存节点执行回收,回收数量不达标才执行 out_of_memory()。所谓“回收失败”,不一定是可回收内存为 0,也可能是回收速度跟不上分配速度,或者系统处于无法睡眠等待的原子分配上下文,根本等不起磁盘写回。
回收不是无限度的:内存压力越大,老周刷盘越频,系统越卡;外棚(swap)本身也有上限。当 reclaimable 趋于 0,而新的内存请求仍在排队,内核的“最后通牒”才会下达——启动 OOM Killer,进入下一个章节的残酷打分。
- 纸箱回炉
page_cache是文件缓存页,干净页直接丢,脏页先写回磁盘再丢。 - 匿名页换出
swapable_anon_pages是进程堆栈等匿名内存页,它们没有磁盘文件做后盾,只能整页搬到 swap 分区。
OOM打分策略
既然必须丢货,丢哪箱?老周掏出计算器,给每箱货物称重打分,分数最高的那箱就要被丢出去。称重公式非常朴素:看这箱货现在占了多少货架(RSS 常驻内存)、占了多少外棚(swap 用量)、以及它自己的“标牌”占多大(页表体积)。三者相加后除以仓库总容量,再放大一千倍,得到一个 0 到 1000 之间的“坏品评分”。
内核实现里这个函数叫 oom_badness(),分数越高代表“清掉它能释放的空间越多、对系统运行影响越小”。特权进程会被打一点折:拥有 CAP_SYS_ADMIN 能力的进程(多为 root 启动的系统服务)得分会减去四分之一,算是老周给“自己人”的一点情面,但只是轻描淡写地减分,绝不豁免。最终 select_bad_process() 遍历所有进程,挑出坏品评分最高者作为牺牲品。
打分看起来绝对公平,但“公平”未必等于“合你心意”。如果你的数据库进程恰好占内存最多,它就会在下一轮被选中。所以内核又给了一把“手动天平”——调整各进程的权重,让真正重要的货永远留在货架上。
- 实际占位
RSS + swap + pagetable计算进程真正消耗的物理内存,包括常驻内存、换出到 swap 的部分、以及页表本身的大小。 - 换算成比例除以
total_memory得到该进程占全仓库的比例。 - 放大千倍乘以 1000 并向下取整,把比例变成直观的千分制分数,方便与后面要讲的调节值直接相加。
oom_score_adj调节
什么叫“重要的货”?仓库里没有客观标准,只能靠人贴标签。/proc/<pid>/oom_score_adj 就是给每个进程贴的标签:取值范围是 -1000 到 1000 的整数。负号代表“尽量别扔我”,正数代表“清仓时优先扔我”。贴到 -1000 被称为 OOM_DISABLE,内核直接跳过该进程,即使全仓库只剩它一个活物也不碰它——这是真正的传家宝待遇。
最终判决分数就是坏品评分加上这张标签:
实际部署时,Nginx 这类反向代理一旦被杀,所有在线请求都会断,运维通常会把它设为 -500 甚至更低;而 Spark 的离线 Executor 这类“丢了可以重跑的活”,可以设为正值,让它在内存危机时优先出局。容器的场景也适用:容器内进程的 oom_score_adj 可以在 cgroup 维度统一设置,Kubernetes 里还会根据 QoS 等级自动赋予不同权重。标签贴得好,OOM 来临时损失的只是边缘服务,核心业务毫发无损。
- 基础坏品分上节的
badness,由进程实占内存决定,是客观称重结果。 - 人工标签
oom_score_adj是管理员手动写入的偏移量,正数让进程更容易被杀,负数更安全。
监控预警方法
老周再会挑货,事后也得有账可查。Linux 把每次 OOM 判决都写进内核环形缓冲区,用一条命令就能翻出来:
典型日志长这样:
这段日志就是老周在记录本上写的判决书:谁被杀、它的虚拟内存和物理内存各占多少。除了翻旧账,还要看实时读数:/proc/<pid>/oom_score 显示每个进程的当前总分,/proc/<pid>/oom_score_adj 显示人工标签,/proc/meminfo 里的 MemAvailable 则是全仓库的“余量电子秤”。把这几项汇聚到监控系统,就能给爆仓装上倒计时警报:
预警之后做什么?先看 ps aux --sort=-rss 揪出内存大户,再顺着 oom_score_adj 检查关键服务有没有贴好“传家宝”标签。监控不是买保险,而是让“爆仓事件”从意外事故变成可预判、可演练的日常操作。
- 剩余比例
MemAvailable除以MemTotal得到当前可用内存的占比,是比 MemFree 更真实的余量指标。 - 预警阈值占比小于
0.1时触发预警,给运维留出至少几分钟的处置窗口。
dmesg | grep -i oomOut of memory: Killed process 1234 (python3) total-vm:4567890kB, anon-rss:123456kB, file-rss:890kB内存规划调优
最好的 OOM 是永远不会发生的 OOM。要让它不发生,得从源头规划。第一件事是收紧老板的空头支票——/proc/sys/vm/overcommit_memory 设为 2(严格模式),内核会用下面这个公式算出“承诺上限”,超额的内存申请直接拒绝:
第二件事是 swap 和外棚规划:内存越大,swap 的价值越偏向“兜底”而非“扩容”,机械盘上的 swap 甚至会让系统卡到怀疑人生。第三件事是容器限额设计:给每个 Pod 设定 resources.limits.memory,等于给每个包间画好天花板,包间爆了自己清场,不连累整栋楼。最后,用 oom_score_adj 给数据库、网关这类“传家宝”贴上 -1000 的免死金牌。全套做下来,即使某天流量突变,你失去的也只是最不重要的那几箱货。
- 物理内存份额
total_ram乘以overcommit_ratio再除以 100,得到内核愿意为普通进程担保的物理内存部分。 - 换出空间兜底加上
total_swap,因为这部分内存即使超额兑现,也能换到外棚临时存放。
AI 应用场景
大模型推理服务容器 OOM 排查
阶段:模型部署与在线服务运维(MLOps)
解决的问题:大模型推理时 KV Cache 膨胀、并发请求突增,常让 K8s Pod 内存超限被 OOMKilled。传统排查靠人工翻日志,难以快速定位“到底是谁吃光了内存”。结合本文的机制,可通过 dmesg 日志与 cgroup 内存统计快速锁定超限进程。
具体案例:某公司部署 7B 参数模型推理服务,设置 memory.limit=8Gi,高峰时段 Pod 反复 OOMKilled。运维在 worker 节点执行 dmesg | grep -i oom,发现是被 vLLM 引擎内部缓存页挤爆,随后调大 limit 并给引擎进程设置 oom_score_adj=-800,彻底告别半夜重启。
AI训练任务内存预算保护
阶段:训练资源配置与任务交付
解决的问题:PyTorch DataLoader 多进程预取会开大量 Buffer,加上 CUDA 固定内存,物理内存瞬时见顶,OOM Killer 可能误杀 GPU 上的训练主进程,导致几天的训练白跑。通过给主训练进程贴负标签、给预取进程贴正标签,把牺牲目标引向可重启的辅助进程。
具体案例:某团队训练大模型时,节点上的 torch.multiprocessing 共享内存预取进程占满内存,训练进程被内核“处决”。调整后训练主进程 oom_score_adj=-600,数据加载 worker 设为 +200,内存危机时只丢可重建的缓存进程,训练照常进行。
AIOps 智能内存告警平台
阶段:可观测性与智能运维(AIOps)
解决的问题:人工盯 MemAvailable 容易漏报,且无法预判“再涨多少会触发 OOM”。可将 dmesg 中的历史 OOM 记录、/proc/meminfo 指标、各进程 oom_score_adj 汇聚成特征,训练一个“爆仓倒计时”预测模型,提前 15 分钟告警。
具体案例:某互联网公司监控平台每分钟采集 MemAvailable、swap 用量、top10 oom_score 进程,叠加时序预测模型,在可用内存跌破 15% 时自动通知业务方回滚版本或降级缓存,成功将月度 OOM 事故从 12 起降到 1 起。
Python 演示
#!/usr/bin/env python3"""OOM Killer 选择过程模拟:按 badness 打分挑选"代价最小"的进程终止""" class Proc: def __init__(self, name, rss, swap, adj=0): self.name = name self.rss = rss self.swap = swap self.adj = adj def mem(self): return self.rss + self.swap def badness(self, total): return self.mem() * 1000 // total def score(self, total): return self.badness(total) + self.adj TOTAL_MB = 6000MIN_FREE_MB = 512 def available_mb(procs): used = sum(p.mem() for p in procs) return TOTAL_MB - used def oom_kill(procs): victim = max(procs, key=lambda p: p.score(TOTAL_MB)) procs.remove(victim) return victim def main(): procs = [ Proc("java", 2800, 500, adj=-200), Proc("mysql", 1800, 300, adj=0), Proc("python", 600, 100, adj=300), Proc("nginx", 150, 0, adj=-500), Proc("redis", 700, 0, adj=-100), ] round_no = 0 while available_mb(procs) < MIN_FREE_MB and len(procs) > 1: round_no += 1 victim = oom_kill(procs) print("Round #%d: kill %s (score=%d, free %dMB)" % ( round_no, victim.name, victim.score(TOTAL_MB), victim.mem())) print("Available: %dMB, survivors: %s" % ( available_mb(procs), ", ".join(p.name for p in procs))) if __name__ == "__main__": main()
def badness(self, total):实现rss+swap占总内存的比例并放大千倍,对应正文的坏品评分。def score(self, total):把badness和人工标签oom_score_adj相加,得到最终判决分数。victim = max(procs, key=lambda p: p.score(TOTAL_MB))内核select_bad_process()的简化版,挑出分数最高的进程。procs.remove(victim)模拟 OOM Killer 终止进程,内存随之释放。while available_mb(procs) < MIN_FREE_MB and len(procs) > 1:只要可用内存低于保命水位就一直“杀”,直到内存充裕或只剩一个进程。
常见误区
误区我只要把 swap 分区开得足够大,系统就永远不会 OOM 了。
正解swap 只是把内存压力转移到了磁盘,外棚也有堆满的时候。当匿名页大量换出、swap 本身也被占满,OOM Killer 照样会登场;更糟的是,Swap 频繁读写会让系统慢到像卡死。swap 的正确定位是“缓冲垫”而非“扩容仓”。
为何常见:大家直观地认为“内存不够就加硬盘”,忽略了 swap 只是延迟爆仓,而不是消灭爆仓。
误区系统内存一紧张就随机干掉一个进程,完全没有规律,跟抽签差不多。
正解内核选择谁牺牲有明确算法——oom_badness() 按 RSS、swap、页表折算基础分,再叠加 oom_score_adj,分数最高的进程被选中。你觉得随机,是因为没去翻 dmesg 日志,也没意识到 oom_score 会随内存使用实时波动。
为何常见:OOM 发生时进程往往瞬间消失,用户来不及看日志,误以为是“运气不好”。
误区我给关键进程设置了 `oom_score_adj = -500`,它就永远不会被杀。
正解负数只表示“扣分”,不代表免疫。该进程依然参与评分排序,只是排名靠后。只有写入 -1000(OOM_DISABLE)才会被内核完全跳过。在极端内存压力下,低分进程也可能成为唯一可牺牲的对象。
为何常见:人们容易把“权重降低”误解为“直接免死”,忽略了 -1000 才是硬豁免线。
误区宿主机还有一半空闲内存,容器里的进程怎么会被 OOM 杀掉?
正解容器有独立的 cgroup 内存限额,内核会为每个 cgroup 单独执行 OOM 判决。即使宿主机的 MemAvailable 还很充裕,只要容器内用量超过 memory.limit,该容器内的进程同样会被内核处决——包间炸了照样清场,跟整栋楼满不满没关系。
为何常见:大家默认 OOM 只发生在整机层面,忽略了 cgroup 维度也有一套独立的“爆仓”判定。
面试题
Q1. 什么情况下会触发 Linux 的 OOM Killer?
- A.只要可用内存低于 50% 时就会触发
- B.内存耗尽且常规回收仍无法获得足够内存时← 正确答案
- C.用户直接键入 kill -9 命令时
- D.每次系统开机时必定触发一次
答案:B。OOM Killer 是最后手段,内核会先回收页缓存、换出 swap,只有回收失败才进入 out_of_memory()。选项 A 的阈值不存在,C 是用户主动杀进程,D 更是无稽之谈。
Q2. oom_score_adj 的合法取值范围是?
- A.-100 到 100
- B.-1000 到 1000← 正确答案
- C.-999 到 999
- D.0 到 1000
答案:B。内核规定该参数范围是 [-1000, 1000],其中 -1000 表示 OOM_DISABLE(完全免疫)。其他选项边界都不对,记住“负一千到正一千”这个区间即可。
Q3. 设置哪个值可以让进程完全豁免 OOM Killer?
- A.oom_score_adj = 0
- B.oom_score_adj = -100
- C.oom_score_adj = -1000← 正确答案
- D.oom_score_adj = 1000
答案:C。只有 -1000 才对应内核的 OOM_DISABLE 状态,进程会被直接跳过;-100 只是扣分,0 没有影响,1000 反而让它优先被丢。
Q4. 进程的 badness 原始评分包含下列哪些内存?
- A.常驻内存 RSS← 正确答案
- B.占用的 swap 空间← 正确答案
- C.进程页表占用的内存← 正确答案
- D.虚拟地址空间总大小
答案:ABC。oom_badness() 统计的是进程“真金白银”消耗的物理内存:RSS、swap、页表。虚拟地址空间只是承诺的纸面数字,未实际占用货架,不参与计分。
Q5. 容器内的进程被 OOM 杀掉,一定是因为宿主机物理内存不足吗?
- A.一定是,容器没有独立内存限制
- B.不一定,可能是 cgroup 内存限额被突破← 正确答案
- C.一定不是,容器不会触发任何 OOM
- D.与进程数量的多少有关
答案:B。容器有独立 cgroup 限额,内核按 cgroup 维度执行 OOM 判决。即使宿主机内存富余,容器内超限同样会引发 OOM。这就像包间爆了会清场,跟整栋楼满不满没有直接关系。
Q6. 在 oom_badness 评分中,拥有 CAP_SYS_ADMIN 能力的进程会如何被处理?
- A.总分乘以 10
- B.直接跳过评分
- C.得分约打 75 折(points -= points >> 2)← 正确答案
- D.得分固定为 0
答案:C。内核给特权进程留了一点情面,points -= (points >> 2) 相当于乘 0.75,只是微降分,绝不豁免。选项 A 方向相反,B、D 都是无法获得的“免死金牌”。
Q7. 下列哪些页面在内存压力下难以被回收,可能促使 OOM 触发?
- A.内核 mlock 锁定的用户页面← 正确答案
- B.干净的文件映射缓存页
- C.内核核心代码驻留页← 正确答案
- D.已被换出到 swap 的匿名页
答案:AC。mlock 锁定页和内核核心驻留页被焊死在货架上,不可回收;干净缓存页随时能丢,已换出的匿名页早就不占物理内存了。回收不动、又腾不出空间,才一步步走向 OOM。
Q8. 下列哪些命令或文件可用于排查进程的 OOM 情况?
- A.dmesg | grep -i oom← 正确答案
- B.cat /proc/pid/oom_score← 正确答案
- C.cat /proc/pid/oom_score_adj← 正确答案
- D.ls /etc/cron.d
答案:ABC。dmesg 看历史判决,/proc 下的 oom_score 与 oom_score_adj 查看实时打分和标签。选项 D 与内存无关。这几件套是 OOM 排查的“三板斧”。
Q9. 一台 16GB 内存、2GB swap 的机器,overcommit_memory=2 且 overcommit_ratio=50,CommitLimit 是多少?
- A.8GB
- B.10GB← 正确答案
- C.16GB
- D.18GB
答案:B。CommitLimit = total_ram × ratio / 100 + total_swap = 16 × 50 / 100 + 2 = 10GB。严格模式下内核最多承诺这个额度,超过即拒绝大额申请。
Q10. 用户程序 malloc(1GB) 成功,但写入数据时进程却被 OOM 杀掉,最可能的原因是什么?
- A.物理内存瞬间被 DMA 占满
- B.overcommit 允诺超额的内存,写入触发缺页时才真正占用物理内存导致爆仓← 正确答案
- C.内核 bug 导致误判
- D.swap 分区损坏
答案:B。malloc 只分配了虚拟地址承诺,写入时才发生缺页、真正占用物理内存。overcommit 模式下内核允许纸面超额承诺,等大量写入同时发生,物理内存瞬间见底,OOM Killer 只能“撕票”。这是“内存看着没用完却被杀”的经典原因。