如果你的服务器突然像死机一样卡顿,SSH半天敲不出一个字符,登录上去发现大规模服务进程集体消失,或者干脆远程连接直接掉线。不要急着怀疑被入侵,先看一个最容易被新手忽略的记录:dmesg里那一屏Out of memory: Kill process。这种情况基本就是Linux遇到了OOM(Out Of Memory,内存耗尽)。
OOM不是硬件故障,也不是玄学,它是Linux内核的一道“自毁式”保护机制:当物理内存和Swap被消耗到极限,系统为了活下去,会强制挑选进程并杀掉,把内存还给系统。问题是,它挑的进程往往是你的核心服务——数据库、缓存、Java应用。这个指南不打算从枯燥的内核源码讲起,我直接把过去几年在Linux服务器上排查OOM的完整思路、命令、拆解逻辑和警务经验整理出来,从“为什么会杀进程”到“怎么快速定位凶手是谁”,再到“如何彻底优化避免再犯”,一次性讲透。不管你是刚上手的运维,还是被线上OOM折磨过的后端开发,这篇都能帮你少走弯路。
1. 先搞清楚 OOM 到底是怎么触发的
1.1 内核的“内存账本”远比想象复杂
很多新手的第一反应是“我的内存是不是买小了”。实际上,OOM的触发点从来不是某一个单纯的内存条容量,而是内核自己那套完整的内存分账体系。现代Linux内核会把物理内存按用途拆分成很多部分:进程的私有数据(anon_rss)、文件页缓存(page cache)、内核自身的slab对象、页表项,甚至临时映射都需要占用物理内存。你平时用free -h看到的那一行used,指的只是“被某个具体对象占住”的内存,真正危险的是当你申请新内存的时刻,内核突然发现“账上已经没有可用页了,连Swap也装不下”,这个时候OOM就会被激活。
我见过一个非常典型的场景:一台32GB内存的服务器,从free看还剩下6GB可用,以为很安全,结果线上服务照样触发OOM。后来一查原因是该服务器上的应用程序疯狂写日志,因为物理内存里的page cache会被写回机制占用大量缓冲,同时一个Java服务申请了一个非常大的堆内存,瞬间内外交困。所以,你在诊断OOM的时候,不能只盯着free的输出,必须有全局观:进程内存、页缓存、内核态内存、磁盘I/O之间的交互失衡,才是触发OOM的导火索。
1.2 OOM 不只是“物理内存不够”这么简单
这里必须纠正一个常见误区:OOM和“物理内存彻底用完”其实不是一回事。Linux内核有一个投机取巧的设计:进程申请内存时,只要虚拟地址空间允许,内核通常都会放行(默认策略),并不立即分配真正的物理页。这种机制叫做内存过量使用(Overcommit)。它带来一个后果——运行中的服务总申请量加在一起,可能远远超过物理内存+Swap的总和。当所有进程开始真实地写入内存页时,物理内存才会被真正耗尽,这时候内核的“暴脾气”OOM Killer才会登场。
还有一个容易被忽视的触发场景:Cgroup内存限制。现在的容器环境,比如Docker、Kubernetes,每个容器都有独立的Cgroup内存上限。假设宿主机内存剩余100GB,但某个容器的内存Cgroup限额只有2GB,这个容器内一旦内存跑满,内核就会对这个容器执行OOM Kill,而宿主机完全无感。所以线上问题排查时,我习惯先确认环境中有没有容器隔离,否则你在宿主机上查半天内存,也找不到真正被限制的那个进程。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 快速定位:日志才是第一现场
2.1 dmesg 里的 OOM 日志怎么看
当内核执行OOM的时候,它会往内核环形缓冲区里写入一条非常完整的元凶信息。第一步永远都是翻日志。不同发行版的中转位置略有差异,但内核日志总体可以通过下面几个命令来查:
bash复制# 直接看内核环形缓冲区,最经典的方式
dmesg | grep -i -B5 -A15 "out of memory"
# 如果你的日志比较大,可以额外看文件
journalctl -k | grep -i -B5 -A15 "out of memory"
# 或者直接看/var/log/目录下的内核日志文件
cat /var/log/messages | grep -i "out of memory"
很多新手看到dmesg日志时会一头雾水,因为里面的字段确实繁琐。我来拿一段真实日志手动拆一下:
code复制[593350.234567] Out of memory: Killed process 2834 (java) total-vm:10485760kB, anon-rss:3067188kB, file-rss:34kB, shmem-rss:0kB, UID:0 pgtables:8456kB oom_score_adj:0
[593350.234567] oom-kill:constraint=CONSTRAINT_NONE,nodemask=(null),cpuset=/,mems_allowed=0,global_oom,total_vm=10485760,anon_rss=3067188,file_rss=34,shmem_rss=0
第一行告诉你三件事:process 2834是Java进程;total-vm是它的虚拟内存总大小,大约10GB;anon-rss才是它实际占用的物理内存,约3GB。第二行的constraint=CONSTRAINT_NONE表示这次是整机级别的内存耗尽,而不是某个Cgroup的限额导致的。如果你看到constraint=CONSTRAINT_MEMCG,则代表问题出现在Cgroup容器层。
这里我个人的习惯是,永远优先关注anon-rss 而不是total-vm,因为虚拟内存本身可能非常高,但不代表它真实吃满了物理内存;真正让系统崩掉的是那些anon_rss很大的进程,它们才是内存占用的大头。
2.2 定位后要收集哪些信息
看完日志,别急着清日志重启服务,那叫治标不治本。你要像侦探一样把现场存证做完整。我的标准采集清单包括下面几项:
bash复制# 1. 看内存整体水位,注意available这一列
free -h
# 2. 看当前哪个进程占内存最高(按RSS物理内存排序)
ps aux --sort=-rss | head -20
# 3. 实时交互式查看(可在top里按M键排序)
top -c
# 4. 更细粒度查看内存分配器与缓存状态
cat /proc/meminfo | grep -E "MemTotal|MemFree|MemAvailable|Buffers|Cached|SwapTotal|SwapFree|Slab|SReclaimable"
free -h的重点是available列,很多老手只看这里。它估算的是当前还有多少内存可以被新进程分配,比free列更有参考价值,因为内核会把部分可回收的页缓存算进去。如果available长期低于总内存的5%,就说明这台机器长期处于危险边缘。
/proc/meminfo里那个SReclaimable也值得专门看一眼。如果它特别大,说明内核态的slab内存(常用于文件系统元数据缓存)在被持续占据,这类内存有时候很难直接被回收,会变相推高OOM风险。这部分很多人不看,但实际排查顽固OOM时经常是关键线索之一。
3. 内核的“裁决规则”:OOM Killer 是怎么选进程的
3.1 oom_score 怎么算
日志看完,下一步就该理解内核选择“牺牲品”的逻辑了。有时候你会想:我明明有个内存占用不高但很重要的进程,为什么内核把它给杀了?答案都藏在/proc/<pid>/oom_score这个文件里。
简单来说,Linux内核给每个进程算一个“坏分值(badness score)”,分越高,越容易被选中作为OOM牺牲品。它的核心计算依据有几条,实际内核实现还会结合进程占用物理内存、运行时长、进程优先级等做修正,但你可以先理解成三个关键词:
- RSS内存越大,分数越高
- swap占用越多,风险越高
- nice值越低(优先级越高),被杀的倾向也会增加
每个进程的实时分值,随时可以查:
bash复制cat /proc/2834/oom_score # 查看某个进程的坏分值
正常来说,这个值在0~1000之间浮动。越高代表越危险,恰恰是那些吃内存最多的服务最容易成为首选目标。很多时候你小心翼翼运维的Java应用并不是内核想针对你,它就是单纯地“你内存占用最大,杀你对释放内存最有效”。
每个进程理论上都有一个可调节项(/proc/<pid>/oom_score_adj),范围是-1000到1000。值越大,被选中的概率就越高。如果你把某个进程设成1000,那么下一次OOM它必死;如果你设成-1000,那么内核会尽量绕过它,优先选择其他进程。
3.2 oom_score_adj 和 systemd 的 OOMScoreAdjust
对于长期监控的守护进程,我强烈建议不要每次手动写/proc/<pid>/oom_score_adj,那是临时手段,进程一重启就失效。通过systemd来配置才是正解。核心服务(如SSH、监控客户端)我会偏向设置负值保护,批处理任务设置默认值就行。
ini复制# /etc/systemd/system/my-app.service.d/oom.conf
[Service]
OOMScoreAdjust=-500
改完以后请务必重载并确认一下:
bash复制systemctl daemon-reload
systemctl restart my-app
# 确认设置生效
cat /proc/$(pgrep -f my-app | head -1)/oom_score_adj
这里分享一个曾经让我头大过的场景:哪天真遇到OOM,挨杀的不一定是内存占用最大的那个,有时候是某个PID刚好触发了内核的启发式判断。比如在一些Vector数据库节点,普通查询进程的RSS不大,但由于某些原因,多个小进程同时申请内存时把内存水位顶爆了一次,内核居然优先挑走了那些偶发的长耗时段任务,而不是最占内存的主服务。这种案例在cgroup v2环境下尤其常见,所以内核对oom_score的计算还叠加了Cgroup的层级权重。这也是为什么我线上调试时,一定会把systemd或容器编排里的OOMScoreAdjust显式配好。
4. 解决路径:短中长期的调整手段
4.1 立即止血:重启服务与手动清理
遇到OOM最紧急的动作是让业务先恢复。如果系统还没完全锁死,优先尝试手动释放内存,不必马上重启机器。先杀掉那几个排查出来的内存大户进程,或者直接重启关键服务。但重启服务前,最好先用journalctl -xe或dmesg -T看一眼OOM发生前1分钟内核还汇报过什么,比如大量page allocation failure之类,这些信息可能直接指明是谁持续申请内存。
触发OOM后系统如果已经处于半死状态,ssh还能敲进去就先用systemctl restart <service>恢复;敲不进去,一般只能强制重启虚拟机或物理机,这属于保命操作,没有太多技巧。需要注意的是,重启完成后不要马上就当无事发生——要用uptime确认机器已经运行了重新计时,再用free -h确认内存已经回流。
4.2 内核参数调优:Overcommit、Swappiness 与 panic_on_oom
定期遭遇OOM的项目,不做运行时参数调整等于没解决。这里给出一套我实测后最稳妥的内核参数组合,直接可以落地:
bash复制# 关闭启发式过度分配,改为严格模式
# 0=启发式(默认), 1=总是允许, 2=禁止超额分配
sysctl -w vm.overcommit_memory=2
# 减小Swap的积极程度,除非内存严重不足,否则更倾向于用物理内存
sysctl -w vm.swappiness=10
# 调整缓存回收压力,防止page cache一直占据内存不吐
sysctl -w vm.vfs_cache_pressure=200
# 发生OOM时让内核主动panic并自动重启,比僵死状态可控
sysctl -w vm.panic_on_oom=1
# 配合内核panic后5秒自动重启
sysctl -w kernel.panic=5
这组参数我专门解释几个容易被问到的点。vm.overcommit_memory=2一旦开启,就意味着系统承诺的内存上限不能超过Swap + RAM × 默认比率。你要同时留意vm.overcommit_ratio,默认通常是50,意思是“CommitLimit = swap + 物理内存 / 2”,如果业务需要用到更大承诺上限,可以按公式自己调整。反之,如果进程内存申请被拒绝,可能就是commit限制卡住了,虽然不会OOM,但应用会直接报错,你得权衡。
vm.swappiness=10是个非常个人的偏好,不代表调小之后就绝对不触发OOM,但它能减少Swap换页颠簸,从而减少页面生命周期里的瞬时高峰。对于大部分内存相对充裕、追求低延迟的服务,调低它是值得的。
vm.panic_on_oom=1 是个双刃剑。如果你没有配置kernel.panic这个后续动作,系统一旦OOM会直接卡死在内核panic状态,而不是杀进程后恢复运行,相当于把“部分服务挂”升级成“整机挂”。但配上自动重启或让监控系统拉起之后,它的价值很高:一台机器可以自主地从内存事故中恢复了。个人建议生产环境按“服务重要性”来决定,核心数据库节点我建议用0(只杀进程),边缘计算节点用1(直接重启),以保障业务连续性。
强调一句:这些参数写入/etc/sysctl.conf才能真正持久化生效。只敲命令行的话,一重启就回滚。
4.3 长期改进:应用层与容器层优化
只调内核参数,相当于大出血时一直输血浆,不找出血点。真正要治本,得从内存占用源头下手。代码层面你至少要能回答三个问题:项目中每个服务的内存分配基数是多少?峰值时内存会涨到多少?有没有释放不了的“常驻对象”?以Java服务为例,堆内存上限(-Xmx)绝不能超过容器或物理内存的60%~70%,同时要留好Metaspace、线程栈、JVM内部使用、原生内存的余量。Java新建线程默认为1MB,256个线程就是256MB,这些不计算在Xmx里面,非常容易掉进“Xmx明明只有2G,实际进程占用却是4G”的坑。
容器层面,如果使用Docker/Kubernetes,需要把宿主机资源限制和容器limits同时考虑进去:
bash复制# docker运行时限制容器内存上限
docker run -m 2g --memory-swap 2g my-service
同时建议在Kubernetes的Deployment里为每个容器显式声明requests.memory和limits.memory,不要只写limits。requests给调度器使用,limits给内核执行约束,两者缺一都会导致资源调度异常或容器直接被杀。
另一个经常被忽视的方向是监控预警。OOM是事后的天崩地裂,更好的做法是在内存水位到达80%之前给开发者推送告警。Prometheus + node_exporter里有一个指标叫node_memory_MemAvailable_bytes,直接拿它除以机器总内存,当低于20%时触发告警就是一条很好用的预警线。这件事做起来成本很低,收益却特别明显。
5. 常见问题与避坑记录
5.1 常见问题速查表
因为Daily在群里被问过太多类似问题,我直接整理一个速查表格,把高频疑点统一放这里。表格左列是问题场景,右列是优先动作和排查方向。
| 问题现象 | 第一步排查动作 | 快速处理思路 |
|---|---|---|
| 整机OOM,日志显示CONSTRAINT_NONE | free -h查看总体水位;ps aux --sort=-rss看TOP进程 |
直接重启内存占用的异常进程,配合排查应用内存泄漏 |
| 容器内报OOM,日志显示CONSTRAINT_MEMCG | 查看容器/sys/fs/cgroup/memory/memory.usage_in_bytes |
查看容器参数里--memory值,按业务实际占用调高或优化代码 |
| 系统还有剩余free,但频繁OOM | 查看Available列与CommitLimit,确认overcommit策略 |
调整 vm.overcommit_memory=2,同时检查page cache和Slab占用 |
| 日志中出现“Page allocation failure” | 查看是否发生了大量碎块导致无法申请连续内存页 | 检查应用程序是否在分配超大块内存,必要时调整内存池或换分配器 |
| 某核心服务总是被杀,明明不是最大占用者 | 查看该进程的/proc/<pid>/oom_score_adj |
显式设置OOMScoreAdjust=-500或-1000,让内核弱化选择它 |
| 一OOM整机就卡到彻底失联 | 立即通过带外管理控制台重启,后续配置panic_on_oom | 生产环境按业务容忍度决定是panic重启,还是默认杀进程 |
5.2 几条亲身踩过的坑
最后分享几条真正让我记到现在的坑。第一条是Swap问题。如果你在机器上创建了Swap但它几乎没被用过,内存却依然触发OOM,请检查swappiness是否被设置成了接近0的值。我遇到过运维同事为了“内存快”直接把swappiness设成0,结果内存峰值一来,Swap完全无法兜底,直接OOM。合理做法是设置10~60之间,让系统在内存即将告急时能提前把一部分数据交换到磁盘,起到缓冲作用。
第二条是缓存引起的“假OOM”。有时候运行大数据任务时,page cache会占很多内存,但它们是可回收的,不会直接触发OOM。但如果这时候某个进程同时申请了大量不可回收的匿名内存,内核就会被迫同时回收缓存,回收一段时间后还是不够,就开始杀进程。这个过程中free的available会急剧下降。遇到这种情况,最好的做法不是去清drop_caches,而是给任务加上内存配额或分批执行,避免脉冲式内存申请。
第三条是关于oom_score_adj的一个冷门细节:如果你在systemd里给某个服务设置了OOMScoreAdjust=-1000,这个值对systemd自身的子进程是生效的,但有些服务会自己fork出子进程,如果子进程没有继承这个设置,照样可能被内核选中杀掉。所以设置完之后,最好遍历确认所有相关子进程的实际oom_score_adj,别只盯着主进程看一眼就放心了。
还有一条经验很重要:调优参数要分环境验证。同样的vm.overcommit_memory=2在数据库节点上能有效防止恶性OOM,但放到一个大量使用共享内存的消息队列节点上,反而可能导致它申请内存被拒,业务报错连天。每个参数都要有自己的“灰度环境”,先在一台非核心机器上运行几天,看应用的申请行为再决定要不要全量铺开。
我个人的习惯是每次设计OOM防御方案时,都会先在测试环境跑一次高并发压测,刻意把内存打空,然后故意观察整个系统如何恢复。事后的复盘记录比任何理论都管用。做运维最怕的不是出故障,而是出了故障以后什么都不知道、什么都不记录。OOM并不可怕,它是一个明确的信号,告诉你“这里有一个内存管理失衡的点”,顺着日志、参数、应用层一层层往下挖,绝大部分问题都能在当天定位并解决。希望这套排查思路能让你下次遇到OOM时,不再是满屏“Killed process”界面里手足无措的那一个。
