Linux 内存 OOM-Killer 打分机制与生产级白名单保护

封面信息图

在 Linux 操作系统与 Kubernetes 生产集群的运维实践中,Out of Memory: Kill process(OOM-Killer,内存溢出杀手) 是内核用来防止操作系统彻底死锁崩溃的最后一道防御底线。

然而,在很多真实的线上生产事故现场,OOM-Killer 的行为却经常表现出令人绝望的“误杀与盲目性”:
当宿主机上的某个业务容器发生内存泄漏将整机内存挤爆时,内核并没有优先杀死那个罪魁祸首容器,而是手起刀落杀死了宿主机上的 sshd 进程,导致值班 SRE 彻底无法通过 SSH 登录服务器抢救
甚至在更惨烈的情况下,内核直接杀死了 kubeletcontainerd 守护进程,导致整台计算节点瞬间与 Kubernetes APIServer 失联脱管,引发大规模 Pod 级联驱逐风暴!

为什么内核会杀错“自己人”?
要彻底驯服 OOM-Killer 这把双刃剑,必须深入理解 Linux 内核的 oom_badness() 评分算法,并建立起一套**“系统核心组件绝对免疫白名单 + 业务工作负载分级打分策略”**。

深入物理底层:oom_badness() 算法评分公式

当系统内存耗尽且无法通过 Page Cache 页面回收释放出空闲内存时,Linux 内核会遍历系统中的所有活动进程,调用 mm/oom_kill.c 中的 oom_badness() 函数,为每个进程计算出一个介于 0 到 1000 之间的坏分数(oom_score):

$$\text{Points} = \frac{\text{进程占用的物理内存 (RSS) + 交换分区 (Swap)}}{\text{系统总物理可用内存}} \times 1000$$

随后,内核会读取该进程的打分偏置参数(oom_score_adj,取值范围为 $[-1000, 1000]$) 进行最终加权修正:

$$\text{Final oom_score} = \max\left(0, \min\left(1000, \text{Points} + \text{oom_score_adj}\right)\right)$$

内核会严格挑选 Final oom_score 得分最高的那个进程下达死刑判决(发送 SIGKILL 信号强制终止)

[ 系统内存耗尽,触发内核 OOM-Killer ]
                  │
                  ▼
┌─────────────────────────────────────────────────────────────┐
│ 内核遍历进程并计算 oom_score (0 ~ 1000)                      │
├─────────────────────────────────────────────────────────────┤
│ 1. 进程 A: SSHD 守护进程 (配置 oom_score_adj = -1000)        │
│    - 计算得分: 0 + (-1000) ──► 最终得分: 0 【绝对免疫豁免】   │
│                                                             │
│ 2. 进程 B: 核心 MySQL 主进程 (配置 oom_score_adj = -900)     │
│    - 计算得分: 600 + (-900) ──► 最终得分: 0 【最高生存权】   │
│                                                             │
│ 3. 进程 C: 某个发生内存泄露的测试容器 (oom_score_adj = +800) │
│    - 计算得分: 200 + 800 ──► 最终得分: 1000 【首选受死靶子】│
└─────────────────────────────┬───────────────────────────────┘
                              │
                              ▼
                [ 核心系统组件毫发无损,精准击杀流氓进程 ]

为什么默认配置下内核会杀错进程?

在默认情况下,所有普通进程的 oom_score_adj 都为 0

  • 如果某个业务进程本身只占用了 2GB 内存,但它启动了 50 个工作子进程(每个子进程占 40MB);
  • 而宿主机上的 sshd 进程由于历史原因常驻占用了 100MB 内存;
  • 内核在单进程维度扫描时,发现 sshd 单个 PID 的占用比那 50 个 40MB 的小进程都要大,直接误判 sshd 是全场最大的进程,将其当场击杀!

生产级防御配置:构筑系统组件免死金牌(White-Listing)

为了确保在任何极端物理内存挤兑场景下,操作系统的管理通道与集群控制面永不宕机,必须对关键守护进程注入免死金牌(oom_score_adj = -1000

1. 保护 SSH 远程登录服务(sshd

编辑 /etc/systemd/system/sshd.service.d/override.conf

[Service]
# 设置为 -1000 代表完全豁免 OOM 查杀 (OOM-Score 将永远为 0)
OOMScoreAdjust=-1000

执行 systemctl daemon-reload && systemctl restart sshd

2. 保护 Kubelet 与容器运行时(Containerd / Docker)

编辑 /etc/systemd/system/kubelet.service.d/10-kubeadm.confcontainerd.service

[Service]
# 保证 Kubelet 和 Containerd 在任何内存挤兑下绝不崩溃
OOMScoreAdjust=-999
3. Kubernetes QoS 体系与 oom_score_adj 的内在映射关系

Kubernetes 官方在设计 QoS 等级时,就是通过底层动态修改容器 cgroup 的 oom_score_adj 来实现优先级淘汰的:

  • Guaranteed 等级(requests == limits
    Kubelet 会自动为其计算注入 oom_score_adj: -997。在整个宿主机中拥有仅次于系统组件的极高生存优先权。
  • Burstable 等级(requests < limits
    Kubelet 会根据其 requests.memory 占节点内存的比例,动态赋予一个 $[2, 999]$ 之间的中间打分偏置。申请越少、超用越多的 Pod,得分越高。
  • BestEffort 等级(未配置任何 requests 与 limits)
    Kubelet 直接将其 oom_score_adj 固定设为最大值 1000
    一旦宿主机发生内存紧张,BestEffort 容器在毫秒级时间内充当“第一批受死肉盾”,优先被内核全部消灭,用它们的生命为核心 Guaranteed 容器争取宝贵的安全生存空间。

现场排查与审计实战

# 1. 实时查看当前系统所有进程的 OOM 打分排名 (Top 10 最危险进程)
printf 'PID\tOOM_SCORE\tOOM_SCORE_ADJ\tNAME\n'
for pid in $(ls /proc | grep -E '^[0-9]+$'); do
    if [ -f /proc/$pid/oom_score ]; then
        score=$(cat /proc/$pid/oom_score)
        adj=$(cat /proc/$pid/oom_score_adj)
        comm=$(cat /proc/$pid/comm 2>/dev/null)
        printf "%s\t%s\t%s\t%s\n" "$pid" "$score" "$adj" "$comm"
    fi
done | sort -k2 -rn | head -n 10

# 2. 审计历史内核 OOM 击杀日志
dmesg -T | grep -E -i "oom_reaper|killed process"

总结

Linux OOM-Killer 不是洪水猛兽,而是一个可以通过数学规则精确掌控的系统稳定器。
通过为 SSHD、Kubelet、核心数据库注入 -1000 免死白名单,并推行严格的 Kubernetes QoS 分级机制,我们让操作系统具备了在极端极限压力下“弃卒保车、精准除瘤、永不失联”的工业级自愈与生存能力。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐