进程莫名被杀?OOM killer 排查指南

网站突然 502,进程莫名其妙没了,八成是 OOM killer 动的手。小够教你从 dmesg 确认、找元凶、临时缓解到根治,内存不足的完整排查链路。

我是小够。最诡异的故障之一:网站好好的,突然 502,登录一看,应用进程没了,没有报错,没有日志,就像凭空消失。这八成是 OOM killer 干的——系统内存耗尽,内核亲自下场杀进程保命。

确认是不是 OOM killer

dmesg | grep -i "oom\|killed process"

看到类似这样的输出,就是它:

Out of memory: Killed process 1234 (python) total-vm:..., anon-rss:...

也可以查系统日志:journalctl -k | grep -i oom。确认了再往下查,别一上来就加内存,可能是别的问题。

谁被杀了,为什么是它

OOM killer 不是随机杀人,它按一套评分来:占用内存多、运行时间短、非系统关键进程的更容易被选中。你的应用进程通常就是完美目标——吃内存多,又不是 sshd 这种系统进程。

看 dmesg 那行里的 anon-rss,就是被杀进程当时占用的内存,心里有个数。

临时缓解:先让服务活着

  1. 加 swap:2G swapfile 五分钟配好,给系统一个缓冲垫(前面讲过)
  2. 降应用的内存占用:比如 PHP-FPM 子进程数调小、MySQL 的 innodb_buffer_pool_size 调小
  3. 重启大户:有些应用有内存泄漏,定时重启是土办法但有效

这些是止血,不是治病。

根治:找到吃内存的元凶

# 看当前谁吃内存最多
ps aux --sort=-%mem | head -10
# 看内存都去哪了
free -h

常见元凶排行:

  • 数据库缓存配太大:innodb_buffer_pool_size 设成了内存的 80%,再跑个应用直接爆
  • PHP-FPM/Worker 开太多:每个子进程几十 MB,几十个加起来很可观
  • 内存泄漏:内存缓慢上涨,重启后从低点重新涨,基本就是泄漏,修代码或定期重启
  • 被入侵挖矿:CPU 100% + 内存异常,先查这个,top 按内存排序看到陌生进程先警惕

oom_score_adj:保护关键进程

实在没办法,可以调低关键进程被杀的优先级(分数越低越不容易被杀,-1000 表示永不杀):

# 让 sshd 尽量不被杀(系统默认已经保护了,一般不用动)
echo -500 | sudo tee /proc/<pid>/oom_score_adj

小够提醒:别给应用进程设 -1000。OOM killer 杀应用是保系统,应用不死,系统死了更惨。调分数是精细活,新手别碰。

预防:监控比排查重要

  • 内存用到 80% 就告警,别等到 100%
  • 画内存使用曲线,缓慢上涨的要警惕(泄漏信号)
  • 小内存 VPS(1G/2G)标配 swap + 内存告警

小够的结论

OOM killer 是症状,不是病因。dmesg 确认 → 找元凶 → 止血 → 根治 → 上监控,这条链路走一遍,下次内存再报警,你就知道该看哪了。记住:频繁 OOM 的唯一正解是加内存或优化应用,swap 和调参都是缓冲。

cgroup 内存限制:给应用画个圈

与其让 OOM killer 全系统随机杀,不如用 cgroup 给应用画个内存上限,超了只杀圈里的:

# systemd 服务里加两行
[Service]
MemoryMax=1G
MemoryHigh=800M

MemoryHigh 是软限制(超了开始节流),MemoryMax 是硬限制(超了就杀这个服务的进程)。这样 OOM 的影响被关在笼子里,不会波及 sshd、数据库这些关键服务。

Docker 用户更熟:docker run -m 1g 就是干这个的。

earlyoom:用户态的提前预警

内核的 OOM killer 是最后手段,earlyoom 是用户态的"提前量":内存还剩 10% 时就开始按规则杀,比内核更早、更可控:

sudo apt install earlyoom
sudo systemctl enable --now earlyoom

适合桌面和小 VPS,服务器上用了 cgroup 限制后一般不需要。

实战:MySQL + PHP 的经典爆内存

小够见过最多的组合:1G 内存 VPS,MySQL 默认配置 + PHP-FPM 默认配置,直接爆。

# MySQL:innodb_buffer_pool_size 别超过内存的 50%
innodb_buffer_pool_size = 256M   # 1G 机器上
# PHP-FPM:算一下能开几个子进程
; 每个子进程约 30-50M,1G 机器开 10 个左右
pm.max_children = 10

公式:(总内存 - 系统预留 300M)/ 单进程内存 = 最大进程数。先算再配,别用默认。

案例复盘:缓慢上涨型泄漏

现象:机器每周三凌晨被 OOM,重启后正常,下周三又来。

排查:画出 7 天内存曲线,看到一条缓慢上升的直线。用 ps aux --sort=-%mem 每天记录,锁定一个 Python 脚本的内存每天涨 100M。

根因:脚本里有个全局列表只增不减。修了一行代码,世界清净了。

教训:OOM 排查一定要看趋势,free -h 只看现在,等于只看冰山一角。

常见问题

Q:加了 swap 还是 OOM? A:swap 满了照样 OOM。swap 是缓冲,不是印钞机。free -h 看 swap 用了多少,长期占满就是内存真不够了。

Q:怎么知道是哪个进程被杀? A:dmesg | grep -i "killed process",输出里有进程名和 PID。配个监控,dmesg 出现 oom 就告警。

Q:云商家的"突发性能实例"和 OOM 有关系吗? A:有。突发实例 CPU 是受限的,内存压力大时系统更慢,OOM 来得更突然。这种实例更要配好 swap 和内存告警。

用 cgroup 给全家桶画圈

不止单个服务,整组应用都可以圈起来。比如"网站全家桶"(nginx+php+mysql)一共只给 1.5G:

# 建一个 slice,里面跑网站相关服务
sudo systemctl set-property website.slice MemoryMax=1.5G

超了只杀圈里的,sshd、监控这些系统服务不受影响。这是 cgroup v2 的玩法,现代发行版都支持。

内存告警:80% 就叫

#!/bin/bash
used=$(free | awk '/Mem/{printf "%.0f", $3/$2*100}')
[ $used -gt 80 ] && echo "内存已用 ${used}%" | mail -s "内存告警" [email protected]

每天跑几次,或者接进监控。OOM 发生前一定有预兆,内存不会从 50% 瞬间到 100%(挖矿和内存泄漏除外,那个看的是趋势)。

常见问题

Q:dmesg 里没有 oom 记录,但进程还是没了? A:那可能不是 OOM:systemd 的 MemoryMax 杀进程不走 OOM killer 的日志;或者是段错误(segfault)自己崩的。journalctl -u xxx 和 coredumpctl 查。

Q:Java 应用老被杀? A:JVM 的堆内存 + 元空间 + 线程栈,加起来经常超预期。-Xmx 设小点,给系统留 30% 余量。Java 在容器/小内存机器上是 OOM 重灾区。

Q:swap 加了,系统变慢了? A:频繁换页(swap in/out 很高)说明内存真的不够,swap 只是在"死"和"慢死"之间选了后者。这时候就别调参了,加内存或减负载。