系统与运维

Linux 内存耗尽与 OOM 定位:从 dmesg 到个大概率失控进程

服务器说没内存时它往往没说真话,本文教你用 free、/proc/meminfo、cgroup 和 dmesg 一层层逼近真正吃满内存的进程,并把不再需要被 OOM kill 的配置填平。

作者:巧匠团队·8 分钟阅读·更新于 2026-09-06

先分清"看起来没内存"和"真的没内存"

很多人一看到 free 里 available 很低就慌了,但 available 和 free 是两个完全不同的概念。free 只是完全空闲的页,available 才代表系统在遇到内存压力时,还能回收多少页——其中很大一部分是 page cache(文件缓存),这些缓存被回收是廉价的,不会导致 OOM。

所以在下结论之前,先确认 swap 使用量是不是在持续增长,再看 dmesg 里有没有真实的 OOM killer 记录。swap 占用上升意味着内核真的开始把匿名页换出,这是内存压力的实锤,而不是 page cache 那种虚高。

free -h
cat /proc/meminfo | head -n 25
# 看 SwapTotal / SwapFree / SwapCached 趋势
vmstat 2 5

用 dmesg / journalctl 找到被 OOM kill 的名字

OOM killer 每次动手都会在日志里留下完整的"裁决记录":它按 oom_score 挑一个进程杀掉,然后打印该进程的内存画像,包括真实匿名内存、共享内存、文件映射等。这段日志里最关键的是标着 Memory cgroup out of memory 的那几行,以及被 kill 的进程 PID 和名字。

如果内核启用了 oom_score_adj 或者某些进程被 cgroup 保住,真正的元凶可能不是那个被杀掉的进程,而是同一 cgroup 里的其它进程把配额挤爆了。所以要连上下文一起看,而不是只看最后一行例如 java 被杀了。

dmesg -T | grep -i -E "out of memory|killed process|oom-kill"
# systemd 环境优先看 journal
journalctl -k -b -1 -g "oom|killed process"

排查每个进程的真实占用:rss、swap 与 top 的误区

top 的第五行 RES 是常驻内存,但它包含了共享库的占用,而共享库在多个进程间是被扯着算的。多个 Python/C 子进程共享 libc 时,RES 相加会虚高。要看更接近真相的数字,用 smaps 聚合统计 Pss(Proportional Set Size),它把共享页按进程数均摊。

另一个常见盲区是 swap 虫:进程被换出后 top 里 RES 变小了,好像内存压力消失了,其实只是匿名页跑去了 swap 分区。结合 ps 的 SWAP 列和 swap 分区占用一起判断才能解释"明明 RES 不高却 swap 被吃满"的怪象。

ps aux --sort=-%mem | head
# 所有用户的整台机器 PSS 汇总
echo "PSS total: $(grep -E "^Total" /proc/*/smaps_rollup 2>/dev/null | awk "{s+=$3} END {print s/1024 \"MB\"}") "
# 找出占 swap 最多的进程
for p in /proc/[0-9]*; do awk -v pid=$(basename $p) "/Swap:/{s+=\$2} END{print pid, s/1024 \"MB\"}" $p/smaps 2>/dev/null; done | sort -k2 -rn | head -5

被 cgroup 卡死的容器:OOM 其实发生在 memory.max 之内

容器里的现象和宿主机很不一样:宿主机 free 明明很充裕,容器却不断被杀掉。这是因为每个容器都有自己的 memory.max(旧版叫 memory.limit_in_bytes),OOM 判定发生在 cgroup 内部,跟整机 pressure 未必同步。

先用 systemd-cgtop 或 cat /sys/fs/cgroup/<path>/memory.current 看当前用量,再看看是否撞到了 max。很多"容器莫名 OOM"其实是应用在单个 Pod 内把 1GiB 请求上限跑满,与整机无关,这时该调的是容器的配额而不是买更多物理内存。

systemd-cgtop
echo /sys/fs/cgroup/system.slice/docker-<id>.scope/memory.current
cat /sys/fs/cgroup/system.slice/docker-<id>.scope/memory.max
cat /sys/fs/cgroup/system.slice/docker-<id>.scope/memory.events

常见误区与修复对照:明明开了 swap 为何还是被杀

常见的错误是开了 swap 就以为永远安全。其实 swapiness 默认 60,内核在内存压力下会先尽量回收 page cache,swap 只是兜底。如果开启了 vm.overcommit_memory=2,内核还可能直接拒绝新的内存申请,导致 malloc 失败而不是 OOM,表现完全不同。

正确的做法分两档:短期用 sysctl 提高保留量、调整 oom_score_adj 保护关键进程;长期查出到底哪个作业导致峰值,用 ulimit 或 cgroup 给失控进程套上限。下面把一段错误的接线换成修正后的写法。

### 错误示范:只开 swap 不设防护
# vm.swappiness=60 保留默认,无 oom 保护

### 修复对照
sysctl -w vm.swappiness=10        # 极少换出,倾向回收缓存
sysctl -w vm.overcommit_memory=1  # 放开 overcommit,避免 malloc 直接失败
# 保护数据库进程优先级
systemctl set-property mysqld.service OOMScoreAdjust=-500
# 给失控作业套上限,防止它吃掉整机
ulimit -v 4194304

验证是否生效:模拟压力而非靠运气

最强的验证是主动制造一次压力,看系统会不会再次卡死或误杀进程。用 stress 一次性吃掉几 GB 内存,观察 free、dmesg 和控制台日志,确认:关键服务存活、swap 有节制地增长、没有失控进程刷新 OOM 记录。

如果关心具体进程占用分布,还可以在压力中抓一帧 smaps_rollup,把每个进程的 PSS 落盘,压力结束后对比,能直接判断整改后是不是那只进程不再扩张。

# 用 stress 制造 2GB 瞬时压力
stress --vm 2 --vm-bytes 2G --vm-hang 10 --timeout 15
# 压力中看是否再次被杀
watch -n 1 "dmesg -T | tail -3; free -m | grep Mem"
# 抓 PSS 快照
for p in /proc/[0-9]*; do awk -v pid=$(basename $p) "/^Pss:/{s+=\$2} END{print pid, s}" $p/smaps 2>/dev/null; done | sort -k2 -rn | head -5 > /tmp/pss_snapshot.txt

官方参考来源

下方为命令对应的官方权威文档,供你核对最新用法与深入查阅。