Linux 内存去哪了:一次 OOM Killer 排查实录
一台 2 核 2G 的小型服务器上跑着 Nginx、Gunicorn 和 Redis。最近每隔两三天,就有一个 Python worker 突然消失,应用日志里没有任何异常,SSH 却还能正常登录。这不是程序自己退出,而是被内核的 OOM Killer 杀掉了。
本文按实际排查顺序,从确认内存状态到定位 cgroup 限制,整理一套可复用的命令。
一、先确认:是真的没内存,还是缓存背锅
登录后第一件事通常是看 free:
1free -h典型输出:
1 total used free shared buff/cache available
2Mem: 1.9Gi 1.6Gi 90Mi 12Mi 260Mi 180Mi
3Swap: 2.0Gi 1.1Gi 900Mi很多人看到 free 只剩 90Mi 就判断内存耗尽,其实关键数字是 available。Linux 会把暂时不用的页缓存放进 buff/cache,需要时可以直接回收;available 才是内核对“还能拿出多少内存给新进程”的估算。
如果 available 还有几百 MiB,但进程仍然被杀,就要怀疑另一种情况:某个 cgroup 或进程受到了单独的内存限制,而不是整机内存不足。
再看一眼更细的内核数据:
1grep -E 'MemTotal|MemFree|MemAvailable|Buffers|Cached|SwapTotal|SwapFree' /proc/meminfo二、找到 OOM 的直接证据
内核 OOM 会在 ring buffer 和 journal 里留下记录:
1dmesg -T | grep -i -E 'out of memory|killed process|oom'
2journalctl -k --since "2 hours ago" | grep -i -E 'out of memory|killed process|oom'一段典型的日志:
1Out of memory: Killed process 18432 (python3) total-vm:2456788kB,
2anon-rss:1420000kB, file-rss:8000kB, shmem-rss:0kB, UID:1000这里最值得看的是 anon-rss:约 1.4GiB,说明进程的匿名内存(堆、栈、未落盘的分配)确实涨上去了,而不是文件缓存造成的假象。total-vm 是虚拟地址空间,通常远大于真实占用,不能直接当作物理内存使用量。
如果 dmesg 里没有记录,但容器或 systemd 服务反复重启,日志可能被 cgroup 级别拦截,需要继续看下一节。
三、定位占用最大的进程
先按 RSS 排序:
RSS 会把多个进程共享的内存重复计算。要更接近真实分摊,可以安装并使用 smem:
PSS 会把共享页按共享者数量分摊,适合判断“谁真正吃掉了内存”。
对可疑进程继续下钻:
1PID=18432
2grep -E '^(VmRSS|RssAnon|RssFile|RssShmem|VmSwap):' /proc/$PID/status
3grep -E '^(Rss|Pss|Pss_Anon|Pss_File|Pss_Shmem):' /proc/$PID/smaps_rollup| 指标 | 含义 | 排查价值 |
|---|---|---|
VmRSS | 进程当前驻留物理内存 | 快速看总占用 |
RssAnon | 匿名页,主要是堆和栈 | 持续上涨通常意味着泄漏或数据堆积 |
RssFile | 映射的文件页 | 可回收,通常不是 OOM 主因 |
RssShmem | 共享内存 | 关注 tmpfs、共享缓存 |
Pss | 按共享比例分摊后的内存 | 多进程场景更准确 |
如果 RssAnon 随时间单调上涨,基本可以认定是应用层问题,例如请求结果被无界缓存、连接未释放、任务队列堆积。
四、别忽略 cgroup 的独立限额
容器和 systemd 服务都可能设置独立内存上限。先确认 cgroup 版本和进程所属层级:
在 cgroup v2 下,查看某个服务的限制与当前用量:
1UNIT=myapp.service
2CGROUP=/sys/fs/cgroup/system.slice/$UNIT
3
4cat $CGROUP/memory.max
5cat $CGROUP/memory.current
6cat $CGROUP/memory.eventsmemory.max 为 max 表示不限制;memory.current 的单位是字节。memory.events 中出现非零的 oom_kill,说明该 cgroup 内确实发生过 OOM:
1low 0
2high 12
3max 3
4oom 3
5oom_kill 3也可以直接问 systemd:
1systemctl show myapp.service -p MemoryMax -p MemoryHigh -p MemoryCurrent这里有一个容易混淆的点:MemoryHigh 是软限制,超过后内核会施压、回收并节流;MemoryMax 是硬限制,超过后通常触发 cgroup OOM。只设 MemoryHigh 不会直接杀进程,但可能让服务变慢。
五、常见误判与处理方式
1. 把页缓存当成泄漏。
free 里的 buff/cache 高并不等于内存被浪费。优先看 available,不要为了“释放内存”反复执行 echo 3 > /proc/sys/vm/drop_caches,那会丢掉有用的缓存。
2. 只看整机,不看 cgroup。
整机还有空闲内存,但容器限额只有 512MiB,同样会 OOM。容器场景要同时看宿主机和容器两层。
3. 盲目调整 overcommit。
检查承诺内存:
1grep -E 'CommitLimit|Committed_AS|MemAvailable' /proc/meminfoCommitted_AS 接近 CommitLimit 时,即使还有空闲页,新的内存分配也可能失败。把 vm.overcommit_memory 改成 1 只是放宽承诺,并不能解决真实的内存泄漏,反而可能把问题推迟到更危险的时刻。
4. 用重启代替定位。
重启能临时恢复服务,但会丢掉现场。至少保留 OOM 日志、memory.events 和进程的 RssAnon 趋势,再决定是修代码还是调限额。
六、一套可落地的预防配置
对 systemd 服务,可以用 drop-in 同时设置软硬限制和重启策略:
1sudo systemctl edit myapp.service写入:
1[Service]
2MemoryAccounting=yes
3MemoryHigh=800M
4MemoryMax=1G
5OOMScoreAdjust=200
6Restart=on-failure
7RestartSec=2sOOMScoreAdjust 取值从 -1000 到 1000,数值越大越容易被杀。把可牺牲的批处理任务调高,把关键服务调低,但不要用它来“保护”一个本身就在泄漏的进程。
监控上,除了内存绝对值,还建议采集 PSI(Pressure Stall Information):
1cat /proc/pressure/memory当 some avg10 持续升高时,说明进程已经在为内存等待,这通常比 OOM 更早发出信号。对于桌面或小内存机器,也可以考虑 systemd-oomd 或 earlyoom,在系统完全卡死前主动处理高压力进程。
最后记住排查顺序:先看 available,再找 OOM 日志,然后按 RssAnon 定位进程,最后检查 cgroup 限额。大多数“内存突然没了”的问题,都能在这四步内收敛到具体原因。