凌晨三点告警响,爬起来一看 MySQL 没了。dmesg 最后一行白底红字:
Out of memory: Kill process 9821 (mysqld) score 612 or a child
Killed process 9821 (mysqld) total-vm:18.2GB, anon-rss:14.7GB
这玩意叫 OOM Killer,不是 bug,是 Linux 内核的保命机制。内存真不够、又申请不到新页的时候,与其整个机器卡死(连 ssh 都登不上),不如挑一个"最该杀"的进程干掉,腾出内存让别的活着。砍人是暴力的,但它确实比死机强。
问题是——它挑谁杀,经常不挑你最想保的那位。
先搞清楚到底是不是 OOM
别急着背锅给内存。先看内核日志:
dmesg -T | grep -i "out of memory"
# 或者
journalctl -k | grep -i "killed process"
真 OOM 会有上面那行 Out of memory: Kill process。如果只是进程自己崩了,那是别的事,别误诊。
score 那列是内核算的"该杀指数",范围 0–1000,越高越先死。它看的是进程占了多少内存、是不是 root、有没有子进程等。MySQL 占 14G,分数自然高,于是第一个被点名。
案发之后,谁吃了内存
进程被杀是结果,得找原因。几种常用姿势:
# 按常驻内存排序,前 15 个
ps -eo pid,ppid,cmd,%mem,rss --sort=-%mem | head -16
# 看单个进程的内存明细(比 ps 准)
cat /proc/9821/status | grep -i vm
# VmRSS 是真实物理内存,VmSwap 是换出去的
# 进程里哪块匿名内存大(排查内存泄漏常用)
pmap -x 9821 | sort -k3 -n -r | head
如果是个长跑的服务慢慢涨上去的,八成是内存泄漏——要么代码问题,要么第三方库(Java 的 metaspace、Python 的循环引用没 GC 掉、C++ 忘了 free)。这类得在代码层查,系统命令只能帮你定位"是哪个进程",定位不到"哪行代码"。
如果是突然暴涨,多半是某次大查询、大文件加载、或者别的进程同时挤进来。我见过最离谱的是备份脚本和定时报表在同一台 8G 机器上撞车,俩加起来把内存吃穿。
cgroup 里的 OOM 更阴
现在大部分服务跑在容器或 systemd 的 cgroup 里,OOM 不一定发生在"整机内存见底"时,而是这个 cgroup 的限额到了。
# 看某个 service 的 memory 上限和实际用量
systemctl show -p MemoryCurrent mysql.service
cat /sys/fs/cgroup/system.slice/mysql.service/memory.max
# 容器里更常见:docker 设了 -m 2g,里面程序吃超了就被杀
# dmesg 里会看到 oom-kill 但整机 free 还很充裕
这种最坑:你 free -h 看机器还有 10G 空闲,服务却被杀了——因为它在那个 2G 的小笼子里 OOM 了。排查时别只看整机,要看它所在的 cgroup 限额。
三个能救命的旋钮
1. 给关键进程降被杀优先级
oom_score_adj 范围 -1000 到 1000,设成 -1000 就永远不被杀(慎用,设太多反而没进程可杀时内核更被动)。
# 保住 mysqld:写入负值,越小越安全
echo -500 > /proc/$(cat /var/run/mysqld/mysqld.pid)/oom_score_adj
# 持久化(systemd 服务)
# 在 [Service] 段加:OOMScoreAdjust=-500
反过来,那种"挂了也无所谓、但很能吃内存"的批处理,可以调高分数,让内核优先杀它:
echo 300 > /proc/$BATCH_PID/oom_score_adj
2. swap 不是洪水猛兽
很多人觉得"服务器不要 swap",但一点 swap 都没有时,内存一紧张立刻触发 OOM。留个几 G swap,至少给内核一点缓冲,把最不活跃的页换出去,争取不死进程。代价是换入换出慢一点,但比被杀强。
swapon --show # 看有没有 swap
free -h # 看 swap 用没用上
3. 实在怕误杀,可以改策略
/proc/sys/vm/panic_on_oom 设成 1 的话,OOM 时直接 panic 重启整机(一般别这么干)。默认 0 是触发 killer,这是大多数场景的正确选择。还有 vm.overcommit_memory,设成 2 可以禁止过度申请、让 malloc 在超限时直接返回失败(应用自己处理),适合那种"宁愿申请失败也不要被偷偷杀掉"的数据库场景——但有风险,改之前先在测试环境试。
平时该做的几件事
- 关键服务用
OOMScoreAdjust降优先级,别等出事; - 跑在 cgroup/docker 里的,限额留 20% 余量,别卡着上限设;
- 监控
anon-rss涨势,慢涨的泄漏比突发更难查但更好防; - 留 swap,别为了那点磁盘空间赌命;
- OOM 后别只重启,去翻 dmesg 的
total-vm/anon-rss,确认是泄漏还是并发挤兑,否则下次还杀你。
说白了,OOM Killer 是背锅侠,真正的锅在"内存规划"和"泄漏没人管"。查清楚原因比会读那行日志重要得多。