场景概述
进程毫无征兆地消失、SSH 时断时续、服务重启后一切正常,这类现象里 OOM Killer 是很常见的幕后角色。它在系统内存耗尽时按评分挑一个进程杀掉以保住系统本身,动作会记录在内核日志里,包含当时的内存全景与被选中进程的内存占用。排查的关键是理解两件事:被杀的多半是占用最大的受害者而不是真正的元凶,以及容器环境下的 OOM 可能来自 cgroup 限额而不是宿主机内存不足。
场景信息
| 触发条件 | 进程被信号终止(退出码 137),或系统出现短时卡顿后自行恢复 |
|---|---|
| 排查目标 | 确认是否为 OOM、谁在吃内存,以及触发的是物理内存还是 cgroup 限额 |
| 工具链 | dmesg 与 journalctl -k 找 OOM 记录 → free 与 /proc/meminfo 还原现场 → ps/监控曲线定位增长来源 |
| 评分机制 | 内核按 oom_score 选择牺牲对象,主要依据进程占用内存比例,可用 oom_score_adj(单元里对应 OOMScoreAdjust)上下调整 |
常见成因
- 应用内存泄漏:RSS 随时间单向增长,最终把机器内存吃满
- 堆上限设置过大:JVM 等运行时的最大堆接近物理内存,多个实例叠加后必然触发
- 缓存与队列失控:本地缓存没有上限、任务队列积压把小对象堆成内存黑洞
- 并发峰值:突发的批量请求让内存占用短时间翻倍,超过实际容量
- 容器 cgroup 限额:宿主机内存充足,但容器自己的 memory.max 被触及
- 禁用 swap:在没有换页缓冲的情况下,内存峰值更容易直接触发 OOM
三步排查命令
三步排查命令
# 第 1 步:找 OOM 记录
dmesg -T | grep -i -E 'killed process|out of memory|oom-kill' | tail -n 30
journalctl -k --since '2 days ago' | grep -i oom | tail -n 30
# 第 2 步:还原内存现场
free -h; cat /proc/meminfo | head -n 5
ps aux --sort=-rss | head -n 15
# 第 3 步:确认是否为容器限额触发(记录里含 cgroup 字样)
cat /sys/fs/cgroup/memory.max 2>/dev/null
cat /sys/fs/cgroup/memory.events 2>/dev/null | grep -i oom
docker stats --no-stream | head
# 保护关键进程与限制失控进程(systemd 单元)
# [Service] OOMScoreAdjust=-500 或 MemoryMax=2G MemoryHigh=1500M
systemctl show -p MemoryMax -p MemoryHigh myapp.serviceOOM 日志读法
| 片段 | 含义 | 用途 |
|---|---|---|
| Out of memory: Killed process | 最终结论:被选中的进程名与 PID | 确认谁被牺牲 |
| active_anon / inactive_anon | 匿名内存(堆栈与堆)活跃与不活跃部分 | 判断是应用占用还是缓存 |
| active_file / inactive_file | 文件页缓存 | 这部分通常可回收,不构成真正压力 |
| slab | 内核对象占用 | 异常高时怀疑内核对象泄漏或大量小对象缓存 |
| pinned / unreclaimable | 不可回收内存 | 偏高说明内存被锁定,回收手段有限 |
| 被选中进程的 rss 与 total_vm | 该进程的物理占用与虚拟内存 | 判断它是受害者还是长期占用者 |
OOM 日志里的进程表会列出所有候选者的内存占用,重点看被杀进程是否长期占据榜首:如果它一直是最大占用者,问题在于它自身的内存管理;如果它只是平均水平的进程,说明是短时间内有人暴涨,需要结合监控曲线找出那个时间点的增长来源。
systemd 单元可以用两个字段做提前量:MemoryHigh 触发回收与限流但尽量不杀,MemoryMax 到达上限即终止;OOMScoreAdjust 调整被杀优先级,给关键服务设负值降低被选中的概率。这些都是缓解手段,真正解决仍要回到内存使用是否合理。
加内存或开大 swap 只是把问题推后:泄漏型增长会继续吃到新上限,而依赖 swap 会明显拖慢延迟敏感的业务。规范做法是先确认增长曲线与触发条件(定时任务、特定请求类型、发布后开始),定位到具体来源后再决定扩容还是改代码。