文章探讨了Linux系统中进程突然消失的原因,指出很可能是OOM Killer(内存杀手)导致。文章详细介绍了如何通过内核日志排查OOM Killer事件,解释了OOM评分算法和如何通过`oom_score_adj`调整进程得分以保护关键进程。
线上跑得好好的业务进程突然不见了,你兴冲冲去翻应用日志,翻到最后一页发现不仅没有任何 Exception,连一条 Warn 都没有,应用就这么平白无故在某分某秒戛然而止。
去机器上看一眼 systemctl status,状态赫然写着:Active: failed (Result: signal),退出原因显示 code=killed, signal=KILL。
这时候新手往往很懵:既然是 crash,为什么没有堆栈?为什么没有 core dump?甚至有的朋友第一反应是有人在终端误执行了 kill -9。
其实这在 Linux 服务器运维里十有八九是被内核的 OOM Killer(Out of Memory Killer)给暗杀了。
事故抓现行:别只盯业务日志,内核日志才是第一现场
既然是 SIGKILL,意味着信号是内核直接发出来的,进程压根没有机会做任何清理,更不可能抓取异常写进应用日志。这时候第一件事必须看内核日志。
最直接的排查命令是:
# 查看内核环形缓冲区,-T 会把时间戳转成人类可读时间
dmesg -T | grep -i -E "oom[- ]killer|killed process"
如果系统配置了 systemd-journald,也可以用:
journalctl -k --since "1 hour ago" | grep -i -E "oom|killed"
如果确实触发了 OOM Killer,你会看到类似下面这样一段非常典型的输出:
[Thu Oct 1 14:23:18 2026] Out of memory: Kill process 18392 (java) score 785 or sacrifice child
[Thu Oct 1 14:23:18 2026] Killed process 18392 (java), UID 1001, total-vm:8452316kB, anon-rss:3652140kB, file-rss:0kB, shmem-rss:0kB
[Thu Oct 1 14:23:18 2026] oom_reaper: reaped process 18392 (java), now anon-rss:0kB, file-rss:0kB, shmem-rss:0kB
看到这段日志,案子基本就破了:系统总内存或者 cgroup 内存吃紧,Linux 内核为了保住整台机器不直接死机,启动了紧急自救程序,挑中了你的 java 进程并一枪毙命。
仔细看上面日志里两个关键参数:
total-vm:该进程申请的虚拟内存大小。anon-rss:该进程实际占用的物理匿名内存(物理内存常驻集)。score:这个进程在 OOM 裁判席上的得分,分数越高越危险。
内核选目标的逻辑:oom_score 到底怎么算的?
很多人会有个疑问:机器上跑着 Nginx、MySQL、Redis、业务服务,为什么内核不杀别人,偏偏挑中了我最核心的那个进程?
Linux 内核有一套评分算法,每个进程在 /proc 目录下都有一个实时的得分文件:
# 查看指定 PID 的 OOM 得分(0 ~ 1000 之间)
cat /proc/<PID>/oom_score
这个得分的基本逻辑其实很粗暴:
- 物理内存占比越高,分越高:内核算的是进程常驻内存(RSS)加页表消耗占系统总物理内存的比例,乘以 10。比如一台 8G 内存的服务器,你的 Java 进程占了 6G,裸分可能直接就干到了 750 分。
- 根进程和特权进程保护:PID 为 1 的 systemd/init 永远不会被杀。
- 关键核心进程加权:可以通过
oom_score_adj人工干预。
怎么给核心进程上一块保命金牌?
在 /proc/<PID>/ 目录下,还有一个文件叫 oom_score_adj:
cat /proc/<PID>/oom_score_adj
它的取值范围是 -1000 到 1000:
- 设置为
1000:表示“有事您先杀我”,只要内存一紧张,内核第一个就把它送走。 - 设置为
-1000:表示“免死金牌”,内核 OOM Killer 无论如何都不会动这个进程(除非没有其他任何进程可杀且内核自身要崩溃)。 - 设置为负数(比如
-500):人工给它的得分散减 500 分,大幅降低被杀概率。
比如线上有个非常关键的 SSH 守护进程或主数据库,你不希望它成为牺牲品,临时保命可以这样写:
# 给予绝对免死金牌
echo -1000 > /proc/<PID>/oom_score_adj
如果是通过 systemd 管理的服务,直接在服务单元文件里加上这一行:
[Service]
# 设置为 -1000 免死,或者 -900 大幅提升优先级
OOMScoreAdjust=-1000
改完后执行 systemctl daemon-reload && systemctl restart your-service 生效。
物理内存明明还有空闲,为什么突然触发了 OOM?
这是很多同学百思不得其解的坑。看监控面板,内存明明还有三四百兆甚至 1G 可用,怎么就报 Out of memory 了?
这里通常有两个隐蔽的原因:
1. 内存碎片化导致的“巨页/连续大块内存分配失败”
内核在处理一些需要连续物理页的分配请求时,如果系统长时间运行导致内存极度碎片化,虽然总量剩 500MB,但都是散落在各个角落的 4KB 小碎页,拼不出连续的物理大块。这时候内核在完成内存规整(compaction)和直接页面回收(direct reclaim)都失败后,也会直接触发 OOM。
可以通过查看伙伴系统状态来确认碎片程度:
cat /proc/buddyinfo
如果大阶数(比如阶 8、9、10)对应的可用块数量全是 0,说明连续内存已经枯竭。
2. 内核的超额放贷机制:vm.overcommit_memory
Linux 默认是个非常乐观的“银行家”。应用程序调用 malloc() 时,内核只是在虚拟地址空间里画了一张支票,根本没有真正在物理内存里扣页,直到程序真正往这块内存写数据触发缺页异常(Page Fault),内核才去拨付物理内存。
这就叫 Overcommit(过度分配)。
控制这个行为的内核参数是 vm.overcommit_memory:
sysctl vm.overcommit_memory
它有三个档位:
0(默认值,启发式过度分配):内核凭感觉估算,如果申请的内存大得太离谱,malloc 就直接失败(返回 NULL);如果觉得还能周转,就先答应下来。但往往等到多个进程同时开始真正用内存时,内核才发现自己兜不住了,立刻唤醒 OOM Killer。1(总是允许过度分配):不管进程申请多大虚拟内存,内核一律批准。这个配置在 Redis 机器上是标配,因为 Redis 做 RDB 持久化或者 AOF 重写时会fork()子进程,fork 采用 Copy-on-Write 机制,虽然虚拟内存瞬间翻倍,但实际修改的物理页很少。如果配 0,经常在 fork 阶段就直接失败报Cannot allocate memory。2(严禁超售):内核严格核算总账。总分配量绝对不允许超过Swap 大小 + 物理内存 * overcommit_ratio%。
如果你的机器跑的是稳健型生产数据库,不希望出现半夜因超额透支而突然被杀的情况,可以考虑设置为 2,并在 /etc/sysctl.conf 里加上:
vm.overcommit_memory = 2
vm.overcommit_ratio = 80
配成 2 的好处是:内存耗尽时,新进来的内存申请直接在用户态报错并抛出错误日志(OOM/OutOfMemoryError),而不是任由内核毫无预警地在后台抽签杀进程。
容器场景新坑:Docker / K8s 里的 Exit Code 137
很多上云或者用 Docker 的朋友常问:“我宿主机内存有 64G,当前只用了 20G,为什么容器里的 Python 脚本跑着跑着就退出了?容器退出码是 137。”
退出码 137 的含义很直白:128 + 9 = 137,也就是接收到了 SIGKILL 信号。
在容器环境里,OOM 有两种完全不同的触发途径:
- 宿主机整机 OOM:整个机器内存被撑满,宿主机内核的 OOM Killer 杀进了某个容器内部的进程。
- 容器 cgroup 单独 OOM:宿主机虽然大把闲置内存,但是给这个容器配置了内存配额(比如
docker run -m 2g或 K8s 的resources.limits.memory: 2Gi)。一旦容器内所有进程使用的内存触顶,cgroup 的内存子系统就会在容器命名空间内触发 OOM,直接把容器干掉。
怎么确认是容器踩了 cgroup 限制?
直接查看容器元数据:
docker inspect <container_id> | grep -i oom
如果看到 "OOMKilled": true,说明 100% 是容器超标了。
在宿主机上,也可以通过 cgroup 状态文件查看 OOM 历史:
# cgroup v1
cat /sys/fs/cgroup/memory/docker/<container_id>/memory.oom_control
# cgroup v2
cat /sys/fs/cgroup/system.slice/docker-<container_id>.scope/memory.events
如果 oom 或 oom_kill 的计数值大于 0,说明这里就是案发现场。
特别警惕:Java 容器化内存配置陷阱
Java 应用在容器里被 137 绝杀是最普遍的事故。
很多人以为给容器分了 4G 内存,把 JVM 参数配成 -Xmx3500m 就万事大吉了。结果上线半天就被杀。
原因在于:-Xmx 仅仅限制了 Java 堆内存!
JVM 的物理内存占用除了堆之外,还包括:
- 元空间(Metaspace)
- 线程栈(每个线程默认 1MB,几百个线程就是几百兆)
- 堆外内存(DirectByteBuffer,Netty 重度依赖)
- JNI 与本地 C++ 内存分配
- JIT 编译器消耗的代码缓存(CodeCache)
如果堆配了 3.5G,容器 limit 只给 4G,留给堆外的只有 500MB,稍微来几批大文件导出或者并发线程一多,堆外瞬间把容器挤爆,直接 137。
合理的做法是:给堆外至少预留 25% ~ 30% 的缓冲垫。如果容器限额 4G,-Xmx 配到 2.5G ~ 2.8G 会稳当很多。
避坑与落地清单
梳理完这些机制,线上维护时可以按这几条准则落地:
- 遇到进程离奇失联,首查 dmesg。不要把时间全耗在翻业务日志上,只要日志截然而止没有任何退出记录,先敲
dmesg -T | grep -i oom,十有八九答案就在里面。 - 保护核心保命进程。把 MySQL、SSHD 或者核心中间件在 systemd 里加上
OOMScoreAdjust=-1000,宁可让外围脚本或边缘计算进程先牺牲,也要保住主库和远程运维通道。 - Redis 机器果断开启 overcommit_memory = 1。避免在定时持久化 fork 时因为内核过于保守而直接失败报错。
- 容器内存配额必须考虑非堆与系统开销。容器 limit 不是应用的全部堆空间,一定要预留堆外、线程栈和基础依赖的常驻集空间,避免触发 cgroup 的冷酷截杀。
- 合理配置 Swap 作为缓冲垫。很多人觉得加了 SSD 就不用开 Swap,甚至把 Swap 彻底关掉。其实适当保留 2G 到 4G 的 Swap,并把
vm.swappiness设为 10 或 1,能给突发内存洪峰争取到宝贵的几秒钟排查和告警窗口,避免内核瞬间硬着陆。
文章标题:进程莫名其妙消失了?排查 Linux OOM Killer 与内存防护我踩过的几个坑
文章链接:https://www.llbbs.cn/jishujaocheng/199.html
本站文章均为原创,未经授权请勿用于任何商业用途
评论一下?