服务器运维

OOM killer 是什么?进程被杀怎么排查与预防

OOM killer 是 Linux 内核在内存彻底耗尽、怎么回收都腾不出空间时的最后手段:给每个进程打分,向分数最高的那个发 SIGKILL,让系统活下来。进程莫名消失,先用 dmesg 查有没有 Killed process;确认是 OOM 后,短期用 oom_score_adj 保护关键进程,长期要把各服务的内存上限加起来算清楚,再配适量 swap。

作者 顶联产品团队发布于 6 分钟阅读

OOM killer(Out Of Memory killer)是 Linux 内核的内存耗尽处理机制:物理内存和 swap 都用完、内核回收缓存和换页之后仍然分配不出内存时,它给每个进程算一个分数,杀掉分数最高的那个,释放内存让系统继续运行。被选中的进程收到的是 SIGKILL,不能捕获也不能拒绝,表现就是“进程突然没了”,程序自己的日志里没有任何退出信息。所以服务莫名其妙地消失时,第一件事是看内核日志里有没有 Out of memory 和 Killed process;有,再弄清楚是谁把内存吃光的、为什么偏偏杀了它。

OOM killer 什么时候会触发

Linux 默认允许程序申请超过实际可用量的内存(overcommit),申请时只登记虚拟地址,真正写入时才分配物理页。所以 OOM 不是发生在 malloc 那一刻,而是发生在某个进程触碰新内存、内核拿不出空闲页的时候:内核先回收页缓存、把匿名页换到 swap、整理内存碎片,这些都失败后才调用 OOM killer。按触发范围分四种:

触发场景 条件 在哪个范围里挑进程 日志特征
整机内存耗尽 物理内存和 swap 都用完,回收不出可用页 全系统 oom-kill: 行带 global_oom,constraint 为 CONSTRAINT_NONE
cgroup 内存上限 容器或 systemd 服务的 memory.max(cgroup v1 为 memory.limit_in_bytes)到顶 只在该 cgroup 内 Memory cgroup out of memory,constraint 为 CONSTRAINT_MEMCG
NUMA 节点或 cpuset 限制 进程被限定只能用部分内存节点,这些节点耗尽 受限的进程 constraint 为 CONSTRAINT_CPUSET 或 CONSTRAINT_MEMORY_POLICY
手动触发 通过 SysRq 的 f 键 全系统 与整机 OOM 类似,invoked oom-killer 行里 order=-1

下面两种情况看起来像 OOM,其实不是 OOM killer 干的,排查时要分开:

  • Java 报 java.lang.OutOfMemoryError:这是 JVM 的堆或元空间到了 -Xmx 等参数设定的上限,由 JVM 自己抛出,进程不一定退出,内核日志里也没有记录。
  • 程序报 Cannot allocate memory:申请内存当场失败,常见于 vm.overcommit_memory=2(严格模式)或进程触及 ulimit 限制,内核没有杀任何进程。

三个相关的内核参数,先看当前值:

sysctl vm.overcommit_memory vm.panic_on_oom vm.oom_kill_allocating_task
参数 默认 含义
vm.overcommit_memory 0 0 按启发式允许超额申请;1 总是允许;2 严格限制,承诺总量不超过 CommitLimit(swap + 内存 × overcommit_ratio%,ratio 默认 50)
vm.panic_on_oom 0 0 杀进程;1 整机 OOM 时内核崩溃,受 cpuset、内存策略或 cgroup 限制的 OOM 仍只杀进程;2 任何 OOM 都崩溃
vm.oom_kill_allocating_task 0 0 按分数挑;非 0 直接杀掉正在申请内存的那个进程,省去扫描

OOM killer 怎么挑进程:打分规则与算例

内核给每个进程算的“坏分”(badness)由两部分组成:

  1. 内存占用:常驻内存页(匿名页、文件映射页、共享内存页)+ 已被换到 swap 的页 + 页表占用的页,单位是页。
  2. 人为调整:oom_score_adj × 可用总页数 ÷ 1000。oom_score_adj 取值 -1000 到 1000,相当于按总内存的千分之几加分或减分。

整机 OOM 时“可用总页数”是物理内存加 swap。几类进程不参与评选:oom_score_adj 为 -1000 的、内核线程和 init(PID 1)。如果上一个被选中的进程还在退出、没释放完内存,内核会先等它,而不是马上再杀一个。

用一个算例看 oom_score_adj 的分量。假设一台服务器有 16 GiB 内存和 2 GiB swap,总共 18 GiB,按 4 KiB 一页是 4,718,592 页:

进程 内存占用 oom_score_adj 调整分 总分
mysqld 9 GiB = 2,359,296 页 0 0 2,359,296
报表任务(java) 3 GiB = 786,432 页 500 500 × 4,718,592 ÷ 1000 = 2,359,296 3,145,728
php-fpm 单个子进程 60 MiB = 15,360 页 0 0 15,360

报表任务只占 3 GiB,却因为 +500 的调整排在 mysqld 前面先被杀掉。反过来,如果什么都不调,被杀的就是占用最大的 mysqld。由此有两个结论:

  • 分数只看占用,不看谁在泄漏。 一个小进程慢慢泄漏把内存挤满,最后被杀的往往是最大、最无辜的数据库。日志第一行 invoked oom-killer 前面的进程名,是触发那一刻正在申请内存的进程,也未必是罪魁祸首。
  • 每个进程的当前分数能直接看。 /proc/<pid>/oom_score 是内核按总内存折算后的分数,数字越大越先被杀,只适合在同一台机器上横向比较。
# 列出 oom_score 最高的 10 个进程:分数、oom_score_adj、进程名
for p in /proc/[0-9]*; do
  printf '%s %s %s\n' "$(cat $p/oom_score 2>/dev/null)" "$(cat $p/oom_score_adj 2>/dev/null)" "$(cat $p/comm 2>/dev/null)"
done | sort -rn | head -10

进程被杀了,怎么确认是不是 OOM

# 本次启动以来的内核日志,-T 显示可读时间
dmesg -T | grep -E 'invoked oom-killer|Out of memory|Killed process|oom-kill:'
# 机器已经重启过:查上一次启动的内核日志(需要 journald 持久化存储)
journalctl -k -b -1 | grep -iE 'oom|killed process'
# 传统日志文件,RHEL 系在 messages,Debian/Ubuntu 在 kern.log
grep -iE 'out of memory|killed process' /var/log/messages /var/log/kern.log 2>/dev/null

一次整机 OOM 在日志里大致是这样(示例,已删去时间戳和中间的内存明细):

php-fpm invoked oom-killer: gfp_mask=0x140cca(GFP_HIGHUSER_MOVABLE|__GFP_COMP), order=0, oom_score_adj=0
Mem-Info:
active_anon:3612340 inactive_anon:210422 ... free:33711 ...
Tasks state (memory values in pages):
[  pid  ]   uid  tgid total_vm      rss pgtables_bytes swapents oom_score_adj name
[   1123]    27  1123  2852610  2401230 20512768        0             0 mysqld
[   2210]    33  2210    98213    15520   417792        0             0 php-fpm
oom-kill:constraint=CONSTRAINT_NONE,nodemask=(null),cpuset=/,mems_allowed=0,global_oom,task_memcg=/system.slice/mysqld.service,task=mysqld,pid=1123,uid=27
Out of memory: Killed process 1123 (mysqld) total-vm:11410440kB, anon-rss:9604920kB, file-rss:0kB, shmem-rss:0kB, UID:27 pgtables:20032kB oom_score_adj:0

逐行读:

日志位置 看什么
invoked oom-killer 行 触发时正在申请内存的进程;order=0 是申请单个页,order 较大而 free 并不低时,问题多在内存碎片而不是总量
Mem-Info active_anon、inactive_anon 是进程的匿名内存,单位是页;free 很低、anon 占了绝大部分,说明是进程把内存吃光,而不是缓存
Tasks state 表 当时每个进程的占用,rss 单位是页(4 KiB):2,401,230 页约 9.2 GiB。把这张表按 rss 排序,能看出除了被杀的进程还有谁在占内存
oom-kill: 行 global_oom 表示整机 OOM,oom_memcg=... 表示某个 cgroup 的上限到了;task_memcg 能看出进程属于哪个服务或容器
Killed process 行 被杀的进程和它当时的匿名内存 anon-rss;老内核这里写作 Out of memory: Kill process 1123 (mysqld) score 912 or sacrifice child

内核日志里找不到记录,进程却确实被杀了,按下表继续查:

现象 可能的来源 怎么确认
整组进程或整个用户会话被杀,dmesg 无记录 systemd-oomd 等用户态 OOM 守护程序,它在内核 OOM 之前按内存压力杀掉整个 cgroup journalctl -u systemd-oomd
容器退出码 137 137 = 128 + 9,即收到 SIGKILL docker inspect -f '{{.State.OOMKilled}}' 容器名;Kubernetes 看 kubectl describe pod 里的 Reason: OOMKilled
systemd 服务失败 服务所在 cgroup 触发了 OOM systemctl status 服务名,较新版本会显示 Failed with result 'oom-kill'
退出码 137,以上都没有 有人或某个监控脚本执行了 kill -9 查审计日志、定时任务和看门狗脚本

用 oom_score_adj 保护关键进程

oom_score_adj 写在 /proc/<pid>/oom_score_adj 里,直接改是临时的,进程重启后失效:

cat /proc/$(pidof -s sshd)/oom_score_adj     # 查看当前值
echo -500 > /proc/1123/oom_score_adj          # 临时调整
choom -p 1123 -n -500                         # util-linux 自带的工具,效果相同

降低分值(往负数方向调)需要 root 或 CAP_SYS_RESOURCE 权限,普通用户只能往高调。要持久生效,写进 systemd 服务的覆盖配置。下例的服务名以实际为准(RHEL 系的 MySQL 服务叫 mysqld,Debian/Ubuntu 叫 mysql):

mkdir -p /etc/systemd/system/mysqld.service.d
printf '[Service]\nOOMScoreAdjust=-600\n' > /etc/systemd/system/mysqld.service.d/oom.conf
systemctl daemon-reload && systemctl restart mysqld
cat /proc/$(pidof -s mysqld)/oom_score_adj    # 确认已生效

调多少,按进程的角色定(下表数值是经验取值,不是标准):

进程类型 建议取值 理由
sshd、监控与运维 agent -900 到 -1000 占内存很少,保住它们才能登录进去处理问题
数据库等主业务 -500 到 -800 降低被选中的概率,但不要 -1000
报表、批处理、可重跑的任务 300 到 1000 让它们先被牺牲

两个坑要避开:

  • 不要给最大的内存用户设 -1000。 它被完全豁免后,内核只能去杀其他进程;如果泄漏的正是它,其他进程杀光了内存还是不够,最后会因为找不到可杀的进程而整机崩溃,日志是 Out of memory and no killable processes。
  • 子进程会继承 oom_score_adj。 给 sshd 设了 -1000,从 SSH 登录后启动的程序也可能带着 -1000,跑飞了反而杀不掉。设置后登录进去执行 cat /proc/self/oom_score_adj 检查,不是 0 的话,在登录脚本里执行 echo 0 > /proc/self/oom_score_adj 调回来,往高调不需要特权。

根本解决:先算清内存账,再配 swap

oom_score_adj 只决定“杀谁”,不决定“杀不杀”。根治靠两件事:让各服务的内存上限加起来小于物理内存,以及给突发情况留缓冲。

先把每个服务能用到的内存上限列出来。以一台 16 GB 内存的网站服务器为例(配置值和单进程占用都是假设):

组件 配置 估算上限
系统与常驻服务 sshd、systemd、监控 agent 约 1 GB
MySQL 缓冲池 innodb_buffer_pool_size = 8G 8 GB,实际占用还会再多一些
MySQL 连接 300 个连接 × 峰值每连接约 4 MB 约 1.2 GB
PHP-FPM pm.max_children = 60 × 每进程约 50 MB 约 3 GB
Redis maxmemory 2gb 2 GB,后台持久化 fork 期间写入多时还会额外增加
合计 — 约 15.2 GB,只剩不到 1 GB 给页缓存和突发

这个配置平时跑得好好的,高峰期 PHP 进程开满、Redis 又正好在做持久化,就会触发 OOM。把缓冲池调到 6G、max_children 调到 40 后,合计约 12.2 GB,留出约 3.8 GB 给文件缓存和突发。单个 PHP-FPM 进程的占用可以这样粗估(进程名以实际为准,有的发行版叫 php-fpm8.1 之类),它用的是 RSS,会把共享内存重复计入,所以算出来偏保守:

ps -C php-fpm -o rss= | awk '{s+=$1; n++} END {printf "%d 个进程,平均 %.0f MB\n", n, s/n/1024}'

再配 swap。swap 的作用是把长期不用的内存页换出去,并在内存突然吃紧时争取时间,让告警先于 OOM 到达;它不能替内存泄漏续命,配得过大,机器会在大量换页中变得极慢,比直接 OOM 更难处理。业务服务器配几 GB 作为缓冲是常见做法。没有 swap 分区时用 swap 文件:

fallocate -l 4G /swapfile        # ext4、XFS 可用;不支持时改用 dd if=/dev/zero of=/swapfile bs=1M count=4096
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile
echo '/swapfile none swap defaults 0 0' >> /etc/fstab
swapon --show

最后,给可能失控的服务设上限,把 OOM 限制在它自己的 cgroup 里:systemctl set-property 服务名 MemoryHigh=6G MemoryMax=8G。MemoryHigh 是软上限,超过后内核会加紧回收、拖慢这个服务;MemoryMax 是硬上限,超过就只在该服务内部触发 OOM,不连累其他进程。要提前发现内存压力,内核 4.20 起可以读 /proc/pressure/memory,some 和 full 的 avg10 持续上升说明进程已经在等内存了;部分发行版需在内核启动参数中加 psi=1 才会开启。

在管理系统里怎么落地

OOM 发生时,被杀的有时恰好是 sshd,或者系统在大量换页中卡住,SSH 怎么也连不上。这时要走不经过操作系统的 BMC 通道:Toplink DCIM 的 IPMI 远程管理提供浏览器里的 VNC 控制台,内核打印到控制台的 OOM 信息在屏幕上直接可见,也能从本地终端登录排查;控制台也卡死时,可以在同一页面远程重启,每次会话都会录像,事后能回放当时做了什么。出租给客户的服务器,客户可以在客户自助端自己打开控制台、重启;如果是某个服务一开机就把内存吃光、反复触发 OOM,还可以进入救援模式修改配置,不动硬盘数据,救援这类高危操作需要输入 OTP 动态口令确认。

常见问题

OOM killer 可以关掉吗?

不能真正关掉,只能换一种失败方式:vm.panic_on_oom=1 让内存耗尽时整机内核崩溃,适合有集群自动切换的场景;vm.overcommit_memory=2 让程序在申请内存时直接收到失败,而不是事后被杀。对单台业务服务器,这两种结果通常都比杀掉一个进程更糟。

被 OOM 杀掉的进程会自动重启吗?

内核不负责重启。由 systemd 管理、设置了 Restart=on-failure 或 Restart=always 的服务会被重新拉起,容器取决于重启策略,手工启动的进程就停在那里。自动重启只能止血,根因不解决会反复被杀。

发生 OOM 之后要重启服务器吗?

一般不用。被杀的进程已经把内存还回来了,系统通常会恢复正常,重新拉起被杀的服务、查清原因即可。只有机器仍在大量换页、卡到登录不进去,或者关键服务反复被杀起不来时才考虑重启;重启前先把 dmesg 输出保存下来,没有持久化的内核日志会随重启丢失。

想看看在你的机房里怎么用?

从一次产品演示开始,梳理你的业务链路。

查看价格
服务热线 400-112-2951

让我们聊聊你的 IDC 业务

扫码添加企业微信,预约产品演示或申请试用。

Toplink 企业微信二维码

长按保存或使用企业微信 / 微信扫描

也可致电
400-112-2951
Telegram
@TopLink88