系统卡死了,然后发生了什么?
你曾经遇到过这样的情况吗:某个程序疯狂吃内存,系统响应越来越慢,鼠标几乎动不了,然后突然——像是被人按下了开关——卡死的程序消失了,系统恢复正常。
这不是运气好,也不是程序自己崩溃了。这是Linux内核的最后一根救命稻草:OOM Killer(Out-Of-Memory Killer,内存不足杀手)。
内核的艰难抉择
当系统物理内存耗尽,且swap空间也填满了,内核再也无法为任何进程分配内存时,它面临一个严峻的问题:要么杀掉一些进程,要么整个系统崩溃。
Linux选择前者。OOM Killer会被触发,它的任务很简单:找到一个或多个进程,杀掉它们,释放内存,让系统活下去。
但问题来了:杀谁?
评分机制:谁是最该死的进程?
OOM Killer使用一套评分系统来决定谁该挨刀。每个进程都会被计算一个 oom_score(可以通过 /proc/[pid]/oom_score 查看)。分数越高,被选中的概率越大。
评分的核心因素包括:
- 内存占用:占得越多,分数越高。这很合理——杀掉一个大内存进程,释放的空间最多。
- 进程运行时间:运行时间越短,分数越高。刚启动的短暂进程比运行了好几年的守护进程更值得牺牲。
- 进程类型:内核线程拥有极高的优先级,几乎不会被选上(它们对系统运行至关重要)。
- 用户权限:root用户的进程分数会被略微降低,但不是绝对保护。
- 手动调整:通过
/proc/[pid]/oom_score_adj,你可以手动给进程加减分。范围是-1000到1000。设置为-1000,进程就会变得“不可被杀”。
手动保护你的重要进程
假设你运行着一个关键的数据库服务,你绝对不希望OOM Killer在内存紧张时把它杀掉。你可以这样做:
echo -800 > /proc/数据库进程PID/oom_score_adj
负值会显著降低它的oom_score,让内核优先考虑其他进程。如果你想让一个进程完全免疫,可以用 -1000(注意,不是0)。
反过来,如果你想让某个进程成为“替死鬼”,可以设置正值。
一个常见的误区:swap 设置越大越好?
很多人觉得,既然内存不够会触发OOM,那把swap设大一点不就好了?比如给服务器配个32GB swap。
这不一定明智。当系统开始大量使用swap时,性能会急剧下降——因为磁盘比内存慢几个数量级。系统可能会因为频繁换页而陷入“颠簸”状态(thrashing),响应变得极其缓慢,甚至比直接触发OOM Killer更糟糕。
一个常见的建议是:swap大小设为内存的1到2倍(对于内存较小的系统),或者干脆设为固定值(比如4-8GB)。对于大内存服务器(64GB以上),有时候甚至不需要swap,因为OOM Killer比swap颠簸更可控。
冷门知识点:内存超售与“惰性分配”
Linux默认允许内存超售(overcommit)。什么意思呢?当一个程序申请100MB内存,内核并不会立刻真的给你100MB物理内存,而是先记下“你承诺会用这么多”,等到程序真的去写数据时,才分配物理页面。
这种策略能提高内存利用率,因为很多程序申请的内存其实用不完。但风险在于:当多个程序同时开始实际使用它们承诺的内存时,总需求可能超过物理内存+swap的总和。这时候,OOM Killer就不得不登场了。
你可以通过 /proc/sys/vm/overcommit_memory 控制超售行为:
012:严格不超售,申请内存不能超过 overcommit_ratio 限制
在某些关键系统(如数据库服务器)中,设置 overcommit_memory=2 可以避免因超售导致的意外OOM,但代价是某些申请内存较多的程序可能会直接失败。
触发OOM Killer之后如何追溯?
当OOM Killer出手时,内核会在系统日志中留下详细的“处决记录”。你可以用 dmesg 或查看 /var/log/messages 找到类似这样的内容:
Out of memory: Kill process 12345 (mysql) score 456 or sacrifice childKilled process 12345 (mysql) total-vm:12345678kB, anon-rss:8765432kB
这些信息会告诉你哪个进程被杀了,它的虚拟内存和物理内存占用是多少,以及当时的系统内存状态。这是排查内存泄漏问题的宝贵线索。