1. 从一次深夜报警说起:OOM 到底是什么
前几天凌晨,我负责的一个数据采集服务突然挂了。登录服务器一看,dmesg -T的输出里躺着一行熟悉又扎眼的日志:Out of memory: Killed process 4521 (java) total-vm:..., anon-rss:...。业务方在群里发消息问“服务怎么死了”,我一边安抚情绪,一边在脑子里过了三遍排查思路。这已经不是第一次遇到 OOM,但每次遇到,都有一些新体会。
OOM 全称 Out Of Memory,指 Linux 内核在系统物理内存严重不足时触发的一套“紧急自救”机制。别把它想得太玄,打个比方:你住一个整租的屋子,房东规定所有房间加起来不能超过总面积,结果你把每一间都塞满了东西,连过道都占了,这时候房东就要从你的房间里挑一件最佔地方的东西扔出去——那个“被扔东西”的动作,就是内核的 OOM Killer 在杀进程。
对新手来说,第一次遇到 OOM 的体验一般是:服务莫名其妙退出了,free -h一看内存还有几个 G,可日志里明明写着 Out of memory。这个“内存看着够,却还是被杀”的困惑,恰恰是理解 OOM 的关键入口。系统剩余内存不够给内核自己用了,或者一次突发的连续内存申请导致了内存水位瞬间告急,OOM Killer 就会直接介入,杀掉它认为“最該死”的进程来释放空间。
这篇文章不打算只丢给你一句“加内存就完事”。我会从现象识别、内核机制、排查手段、长期策略四个层面,把 OOM 这件“小事”彻底摊开,完整覆盖我这些年在一线踩过的坑和验证过的方案,尽量让新手看完之后,下次再遇到 OOM 能心里有数、手上有招。
2. OOM 的触发链条与内核决策逻辑
2.1 内存到底是怎么“满”的
“内存不足”不是一个瞬间事件,而是一条由慢到快的滑坡。程序申请内存时,内核并不会立刻分配物理页,而是走“请求-延迟分配”机制:申请的时候只记账,真正写数据的时候才分配物理页。这就导致了一种很常见的假象——一堆进程各自申请了一大片虚拟内存,系统看起来还剩不少空闲,但某个瞬间所有进程同时写数据,物理页瞬间被瓜分干净。
这种“同时写”的场景在服务端很常见。比如一个 Java 应用在做 Full GC 前需要快速分配一个大数组,又比如一堆临时线程同时创建并各自初始化缓冲区,再比如某个算法在峰值期一次性 malloc 了几百 MB。内存水位瞬间从 70% 冲到 99%,内核的伙伴系统(Buddy System)在找连续内存页时反复失败,最终触发重分配(Direct Reclaim),重分配还不行,就进入了 OOM 判定流程。
还有一类容易被忽略的情况:内核自己也需要内存。比如页缓存被压缩、文件系统的元数据刷盘、网络收包缓冲区等。当系统剩余内存低到某个阈值以下,内核连“自救”的余力都没有时,它会比普通用户态进程更早地触发 OOM。这就是为什么有时候你看用户态程序占用的内存并不高,但系统还是报 OOM——内核留给自己的那部分“安全垫”(通常由vm.min_free_kbytes参数控制)都没了。
2.2 OOM Killer 如何决定杀谁
OOM Killer 的决策并不随机,它依据每个进程的oom_score来排序,数值越高越容易被杀。默认情况下,这个分数一大半来自进程的物理内存占用(RSS),进程占用内存越多,oom_score就越高。另外,系统会依据进程优先级(nice值)做微调,优先级越低(nice 数值越高)的进程,分数会稍微偏大。
oom_score这个值可以在/proc/<pid>/oom_score里看到,取值范围一般是 0 到 1000。你还能通过/proc/<pid>/oom_score_adj去手动干预——想保护某个关键进程,就给它设一个负值,比如echo -1000 > /proc/<pid>/oom_score_adj表示“永不杀死”;想让某个进程优先被牺牲,就设一个正值,比如echo 500 > /proc/<pid>/oom_score_adj,内核会把这个值直接加到oom_score上。
有朋友会问:既然/proc能改,那是不是可以全系统把核心进程都设置成 “永不杀死”?理论上是可行的,实际中我强烈不建议全盘这么做。一旦触发 OOM,一个候选进程都挑不出来,内核就得面对一个致命选择题:选不出要杀谁,那就只能通过 Panic(内核恐慌)自杀式重启来保命。你要的是“瘫痪一个进程”,不是“整台机器重启”,所以oom_score_adj的调整要克制,只针对真正需要保护的进程。
提示:
oom_score_adj取值为 0 是默认,最低可以到 -1000,最高到 1000。设置 -1000 时进程几乎不可能被 OOM Killer 选中,但要注意这不是绝对保险——有一种例外是内存不足已经严重到内核无法正常调度时,一切保护机制都可能失效。
2.3 一次完整的 OOM 生命周期
把整条链路串起来看,一次典型的 OOM 事件大体经过五个阶段:
- 某个或多个进程的物理内存使用量急剧上升,剩余可用内存快速下降到水位阈值。
- 内核触发内存回收(直接回收或后台回收),尝试把脏页写回磁盘、清空页缓存。
- 回收速度跟不上分配速度,内存请求反复失败,内核进入“重分配”路径。
- 系统判断已经没有进程愿意释放内存,进入 OOM 判定,OOM Killer 开始计算
oom_score。 - 选定并杀死目标进程,向进程发送 SIGKILL,同时在内核日志中记录一句
Out of memory: Killed process ...。
这条链路的每一环都有对应的观察手段。比如第二阶段的回收动作,可以在/proc/vmstat里看到pgscan_direct和pgsteal_*这类计数器的跳变;第四阶段是否进入 OOM,可以通过dmesg的 “Out of memory” 文本判断,也可以开启vm.oom_dump_tasks=1让内核把每个进程的内存详情全部打印出来,方便事后复盘。
理解整条链条的核心意义在于:你解决 OOM 的思路取决于它停在哪一环——如果是内存管理机制参数配置不当导致回收乏力,调内核参数有效;如果是某个进程本身存在内存泄漏,调参数就是治标不治下,必须换解法。把视角从“杀进程”抬升到“内存生命周期管理”,后续所有工具和策略就有了正确的锚点。
3. 现场急救:OOM 发生后的第一反应
3.1 快速判断“是不是 OOM”的硬指标
我发现很多新人在服务挂掉的第一时间会先看free -h,然后盯着 cached 那一列陷入沉思:明明显示有几 G 可用,为什么还 OOM?这个误解的根源在于free对 available 的定义——available 是“估算的、能随时被回收给程序用的内存”,它包含了大部分 page cache,但 page cache 不是无限可用的,内核需要保留一部分页缓存来保证文件系统性能。所以真正的可用内存其实比 available 显示的还要少一些。
我的排查习惯是:第一时间执行free -h看趋势,然后立刻dmesg -T | tail -50。只要你在 dmesg 里看到类似Out of memory: Killed process的记录,基本就能实锤是 OOM 干的。至于 “还剩几个 G 为什么会死”,往往是因为那不过是可回收页缓存,真正的匿名页(进程堆栈)和不可回收页已经把系统挤得捉襟见肘了。
另外可以注意一个细节:dmesg里如果出现大量 “page allocation failure” 或 “Out of memory: Kill process ... or sacrifice child”,说明系统已经进入非常紧张的重分配循环。这类日志出现时,就算当下还没杀进程,也是即将 OOM 的前兆,值得立刻处理。
3.2 先止损:快速释放内存空间
确认是 OOM 之后的“黄金三分钟”,我建议按以下顺序做:
- 杀掉非核心的高占用进程或容器,把最占内存的无关服务停掉。可以用
ps aux --sort=-rss | head -20快速列出物理内存占用最高的前 20 个进程。 - 检查 swap 是否为空。
free -h看 swap 这行——如果 swap 还是 0,说明系统压根没有 swap 或者根本没用到,在内存告急的时候,一个可用的 swap 可以充当缓冲垫,给排查争取时间。 - 如果系统已经处于“一杀进程就满,一满就再杀”的抖动状态,别犹豫,直接把单机问题隔离出来,先把关键流量切到备用节点,再慢慢跑诊断。
这三点里最难拿捏的是“杀哪个”。我遇到过业务方说“这个进程很重要,别动”,但那个进程实际上就是内存泄漏的元凶,留着它,整个机器都要跟着陪葬。所以我的原则是:先统计内存占用,再结合服务的实际角色来判断——凡是能够快速重启的服务,优先级一律低于那些不能重启的核心服务。
3.3 别急着重启,先收集现场证据
新手最常犯的一个错误是:服务一挂,立刻systemctl restart xxx,先把服务重新拉起来再说。这本身没错,但我强烈建议在重启之前,花 30 秒把现场证据收集齐全,否则之后想复盘问题,日志已经滚动覆盖,只能拍到脑门后悔。
至少收集以下内容:
dmesg -T | tail -200:内核日志,里面包含被杀进程的名字、PID、total-vm、anon-rss等关键信息。cat /proc/meminfo和cat /proc/vmstat:内存快照,记录当前系统内存分布。ps aux --sort=-rss | head -30:进程内存占用快照,找出当时的“大胃王”。- 如果系统启用了 systemd journal,
journalctl -k -b还能搜到上一次启动时的内核日志。
把这些存在一个文件里,命名成oom_debug_时间戳.log。再顺手看一下dmesg里被杀进程的rss总量,对比它正常运行时的大小,如果被杀时 RSS 是平时的 3 倍以上,大概率这就是元凶。
注意:重启会清空大部分运行时信息,
/proc下的很多动态指标也会归零。所以“先取证、再重启”的顺序一定不能反过来。
4. 从日志读出门道:OOM 信息的逐行解读
4.1 dmesg 里那几行要命的日志
很多新手看到 dmesg 里的 OOM 日志就是一行“Out of memory: Killed process”,然后就没有然后了。但如果内核开启了完整 dump(很多发行版默认关闭,可以通过vm.oom_dump_tasks=1开启),你会看到一段非常详细的信息,逐行拆开看非常有价值。
典型日志结构大致如下:
java: invoked oom-killer: gfp_mask=0x4000c0, order=0, oom_score_adj=0 java: cpuset=/ mems_allowed=0 CPU: X PID: 1234 Comm: java ... [ pid ] uid tgid total_vm rss pgtables_bytes swapents oom_score_adj name [ 1234] 0 1234 3712345 123456 ... 0 0 java [ 5678] 0 5678 4567890 98765 ... 0 0 python3第一行里invoked oom-killer表示哪个进程把内核拽进了 OOM 判定流程,通常是“内存分配请求方”,但不一定是要被杀的那个。gfp_mask是内存分配的标志位集合,order=0表示分配的是单个内存页,如果order大于 0,说明是在申请连续物理页块,这种情况往往是内存碎片化导致的。
真正有用的信息在[ pid ]开始的进程列表里面。rss列是进程的物理内存占用量(单位是页,乘以 4KB 就是实际大小),total_vm是虚拟内存总大小,name是进程名字。对比几个候选进程的rss,你能非常直观地看出当时谁吃内存最多、被杀的为什么是它。
4.2 从日志反推触发场景
OOM 日志里还有一个信息容易被忽视:Mem-Info那一段。它会把当前的内存情况列出来,包括active_anon、inactive_anon、active_file、inactive_file等。我一般会重点看active_anon和active_file的比例:如果active_anon飙升,说明匿名页(程序堆、栈)占用极高,大概率是用户态应用的问题;如果active_file飙升,说明页缓存压力很大,可能是突发大量读写导致缓存暴涨。
举个例子,我曾经排查过一个“每天凌晨三点定时死机”的诡异问题。从 dmesg 的 Mem-Info 看到active_file异常高,顺着查才发现是凌晨的日志归档任务产生了大量读文件操作,把页缓存直接顶满。问题解决后,我顺手给日志归档加了 cgroup 内存限制,再也没出过事。这就是读日志读出来的价值——如果不看 Mem-Info,我可能还在傻乎乎地加内存条。
4.3 内核日志里容易忽略的伴生信号
OOM 通常不是孤零零发生的,它会伴随一系列其他内核告警。常见的有:
page allocation failure:内存分配请求失败,但还没进入最终 OOM 阶段。INFO: task ... blocked for more than 120 seconds:进程因为等待内存分配而长时间阻塞,说明系统内存压力已经非常严重。hung_task_timeout_secs触发的 hung task 告警:常常和内存分配死锁伴生。
这几种日志在 dmesg 里出现的顺序往往能帮你还原事件的“时间线”:先有 allocation failure,再出现等待阻塞,最终触发 OOM。看到这条链,基本就能确定是分配压力持续叠加导致的问题,而不是某个瞬间的巧合。
5. 排查内存峰值的长效工具与手段
5.1 用好系统自带的轻量工具
长期排查内存问题,我习惯组合使用几个轻量工具,不需要额外安装什么重型监控,先把基本盘摸清:
ps aux --sort=-rss:看当前进程的实时物理内存占用,一秒钟出结果。free -h:看系统整体内存水位,重点关注 available 和 swap。cat /proc/meminfo:细看内存组成部分,包括MemTotal、MemAvailable、SReclaimable、Slab等。top/htop:观察动态变化,能持续看几秒到几分钟,找到内存涨幅最快的进程。cat /proc/<pid>/status:看单个进程的详细内存字段,其中VmRSS是它当前的物理内存占用,VmSize是虚拟内存大小。注意区分这两个,很多新手拿VmSize当成物理内存去排查,方向就偏了。
另外,/proc/pressure/memory文件记录了内存压力的 PSI(Pressure Stall Information)数据,包括some和full两个维度。some表示有些任务在等待内存,full表示所有任务都在等。这个指标能帮你量化“内存吃紧到什么程度”,结果比单纯的百分比更直观。
5.2 统计峰值内存的三板斧
项目里的临时问题排查完之后,还是要面对一个本质问题:峰值内存到底多大?稳定复现了吗?我用过三种比较可行的手段:
第一种,ps定时采样。写一个简单的 while 循环,每 2 秒把ps -eo pid,comm,rss输出到文件,跑上半个小时,再用 awk 统计每个进程 RSS 的最大值和平均值。这个方法简单、零侵入,适合临时应急。
第二种,使用systemd-cgtop或cgroup的memory.peak计数。如果你的服务跑在 systemd 或容器里,cgroup v2 提供了memory.peak这个只读文件,记录该 cgroup 历史最高内存用量。一行cat /sys/fs/cgroup/<路径>/memory.peak就能拿到自启动以来的峰值,这是最准确也最省事的办法,强烈推荐优先使用。
第三种,临时开启内核的perf内存事件,或者用valgrind的 massif 工具对你怀疑的进程做详细内存画像。这个方法开销大、周期长,一般只在确认某个进程存在内存泄漏后才会用。
5.3 识别“看似正常实际异常”的进程
有些进程隐藏得很深,它们平时 RSS 不高,但偶尔瞬间冲高,比如 Java 的 Eden 区、Python 的临时大列表、Node 的一次全量 GC 等等。这类“脉冲式内存”很难在事后复盘时捕获,因为 OOM 发生时进程已经被杀掉了,原始证据只存在于日志里。
对于这类问题,我的经验是“两查一防”:查日志里被杀进程的total-vm和rss比值,如果total-vm极大而rss相对较小,说明进程申请的逻辑内存很多但实际写入的少,可能是虚拟内存无限膨胀型问题;一防是给这类进程提前设置内存上限,让它在膨胀到失控前先被自己的限制拦住,而不是拖垮整机。
6. 从根上解决问题:一套可落地的止血流程
6.1 评估要加内存还是调服务
“OOM 最佳解决方案”没有统一答案,而是要分场景判断。如果一台服务器上跑的服务内存总量本来就接近物理上限,长期使用率经常在 85% 以上,那我建议直接加内存或拆分服务,因为降负载是系统性的,靠调参数只能短暂缓解。
如果服务本身存在无界缓存、大集合长期不释放、连接池过度膨胀这类问题,光加内存就是把问题挪到更大的内存条上重演一遍。这时候应该去代码或配置层面找到那个“无限扩张”的源头,比如把 HashMap 缓存改成带过期策略的本地缓存,把 JDBC 连接池的 maxActive 设置到一个合理水平。
判断依据我给三条参考:
- 内存长期平均使用率是否超过 85%?是 → 扩容或拆分。
- 峰值内存与均值内存的差异是否超过 2 倍?是 → 服务内部存在瞬时大对象或缓存堆叠。
- 同一进程反复成为 OOM 被杀对象?是 → 大概率是应用层问题,而不是系统配置问题。
6.2 给关键进程套上安全绳:swap 与 cgroup
在应用侧彻底修复之前,先把安全绳挂上。第一根安全绳是 swap。很多系统为了性能直接关闭了 swap,但代价是内存进入紧急状态时毫无缓冲。我建议在物理内存允许的前提下,分配一个大小约等于物理内存 20%~50% 的 swap 分区或 swap 文件。它真正的价值不是让你“用内存换性能”,而是给 OOM 判定提供一个缓冲时间窗口,让你有足够的时间介入处理。
第二根安全绳是cgroup内存限制。给每个业务进程或容器设置独立的内存上限,把“内存爆炸”的爆炸半径限制在单个服务内。比如 Docker 容器设置--memory=2g,systemd 服务设置MemoryMax=2G,这样某个服务的泄漏就不会拖垮整台宿主机。这是我在生产环境中最推荐的防守性配置,成本极低、效果立竿见影。
6.3 内核参数的合理调优清单
内核参数不是调剂神器,但要本着“少调、精准调”的思路来做。以我自己的经验来看,下面几个参数在 OOM 问题上最值得关注:
| 参数 | 默认值 | 调整建议 | 适用场景 |
|---|---|---|---|
vm.overcommit_memory | 0(启发式) | 1 或 2 | 系统经常因突发申请而 OOM,可设置 2 加大保守程度,禁止超卖 |
vm.overcommit_ratio | 50 | 按需调整 | 配合overcommit_memory=2使用,控制可超卖比例 |
vm.panic_on_oom | 0 | 默认保持 0 | 设置为 1 意味着 OOM 时直接内核 panic 重启,生产环境慎用 |
vm.oom_kill_allocating_task | 0 | 1 | 直接杀掉触发 OOM 的进程,而不是系统自动挑选 |
vm.min_free_kbytes | 动态 | 适当调大 | 内核保留的安全内存垫,调大能减少频繁进入直接回收的几率 |
vm.swappiness | 60 | 0~10 或按需 | 调整匿名页回收与页缓存的偏好 |
这里特别提醒一点:vm.panic_on_oom这个参数经常被误配置。有些人为了“OOM 时自动重启”,把它设成 1,结果本来只是杀掉一个进程就能恢复的问题变成整机重启,业务中断时间反而更长。我见过最合理的用法是设置为 0,让内核自动找进程杀掉,再用 node_exporter 之类的监控盯住节点状态。
6.4 应用层的自我调节
系统层兜底之外,应用侧也要有自我调节机制。以 Java 为例,配置正确的堆大小是前提:-Xmx不要设成物理内存的全部,留 20% 以上给系统页缓存和其他进程;同时开启-XX:+ExitOnOutOfMemoryError,让 JVM 在堆 OOM 时直接退出进程而不是无限 GC 阻塞线程——这样上层编排系统(K8s 或 systemd)能更快感知并重启服务。
对 Python、Node、Go 这类应用,重点在于限制并发和缓冲:连接数限制、队列长度上限、缓存过期时间,都应该设一个明确的上限。很多 OOM 的根本原因就是“代码里有一处没有边界的缓存或列表”,没有任何调参能补救无限增长的数据结构。
7. 常见问题速查与深度避坑
7.1 高频问题排查速查表
| 现象 | 可能原因 | 首选排查路径 |
|---|---|---|
服务被杀但free -h显示还有内存 | 可回收页缓存掩盖了真实内存压力 | /proc/meminfo中查看MemAvailable和active_anon |
| 每次 OOM 杀的都是同一个进程 | 该进程存在内存泄漏或无界缓存 | 对该进程做长时间监控 + 查看内存峰值 |
| 多个进程同时被杀 | 系统内存整体耗尽,OOM 一次性挑了一批 | 检查是否突然有大流量或大面积任务并发 |
| 杀完进程后系统依然卡顿 | 内存反复紧张,回收线程在持续工作 | 查看vmstat的si、so和 CPU 的 iowait |
| 容器被杀,宿主机完好 | cgroup 内存限制生效 | 查看容器日志和memory.events中的oom记录 |
| OOM 日志出现频繁但无规律 | 可能是瞬时大对象集中分配 | 统计峰值内存,分析应用 GC 或任务周期 |
7.2 swap 的脏坑与正确用法
swap 不是万能药,这里有个脏坑:如果磁盘 I/O 能力差,大量使用 swap 反而会让系统更慢,甚至因为 swap 读写卡顿导致进程远超预期地阻塞。我建议把“swap 是否该开”和“swap 开多大”当成两个问题来看——前者决定缓冲垫有没有,后者决定缓冲垫多大、多快。在 SSD 或 NVMe 上开 swap 基本无感,在老旧机械硬盘上要谨慎。
另外一个常见问题是很多人开了 swap 但vm.swappiness=0,这几乎等于没开。swappiness控制的是内核在回收内存时偏好回收匿名页(进入 swap)还是页缓存,如果设为 0,内核会优先回收页缓存,在缓存极少时才会碰 swap。我的建议是保留默认 60 或者在内存压力大的机器上设 10 左右,确保紧急情况下 swap 真的能派上用场。
7.3 容器场景下的 OOM 误判
如果你用 Docker 或 Kubernetes,很容易把宿主机 OOM 和容器 OOM 搞混。容器 OOM 往往不是因为宿主机内存不够,而是容器的 cgroup 内存限制到了。区分方法很简单:dmesg里如果出现宿主机 OOM 日志,说明整机都受影响了;如果只有容器被杀而宿主机正常,去容器目录下的memory.events看oom计数是否大于 0。
容器场景里还有一个典型坑:内存 limit 配置不正确。limit 低于应用实际运行需求,服务一启动就被 CGroup OOM Killer 杀掉,看起来像“系统不稳定”,实则是配置失当。这种问题最好通过设置memory.peak监控或kubectl describe pod观察 events 来定位。
7.4 事后复盘的三板斧
问题解决了,不等于结束。我建议每次都做一个简短复盘,内容围绕三条主线:
- 触发点:当时什么操作或流量变化把内存顶到了极限?
- 放大点:为什么系统没能更早介入,是警阈值太高还是监控缺失?
- 阻断点:后续如何做才能让同样的问题不再发生,或者发生时影响降到最小。
在此基础上,把这次 OOM 的核心内存数据(峰值、触发进程、系统参数)记录下来,建立一个“内存事件档案”。长期积累下来,你会发现自己对系统内存的判断力提升很快——很多问题在发生之前就已经能预判了。
8. 综合复盘:一个模拟项目 X 的 OOM 处理全流程
为了把前面所有内容串起来,我用最近刚处理完的一个模拟项目 X 的具体场景做一次完整复盘。这个项目的服务部署在一台 8 GB 内存的服务器上,跑着一个 Web 服务和一个定时任务进程。
第一天深夜,监控告警弹出“进程失联”,我登录服务器后按顺序做了下面几件事:
第一步,free -h看到 available 只剩 320 MB,swap 为 0。第二步,dmesg -T | tail -100看到 OOM Killer 把定时任务进程杀了,日志里显示该进程的rss达到了 4.7 GB,而平时它只有 800 MB。第三步,cat /proc/meminfo看到MemAvailable极低,Slab也偏高。第四步,ps aux --sort=-rss | head -20确认 Web 服务本身只占 1.8 GB,问题不大。
初步判断是定时任务进程在夜里某次运行中产生了海量临时对象,内存瞬间飙升。于是我先给这个服务配置了MemoryMax=5G的 systemd 限制,同时开启了一个 2 GB 的 swap 文件做缓冲,防止再出现问题时直接杀进程。接下来我在应用的日志采集侧加了分段导出逻辑,把一次性全量大对象拆成小块分批处理,内存峰值从 4.7 GB 降到了 1.5 GB 左右。
最后,我在监控系统里加了memory.peak的每日记录和进程 RSS 的 5 分钟平均采样,再遇到类似问题就不再是“深夜爬起来应急”,而是早上打开面板看数据就能定位根源。整个过程从出现告警到问题基本落地解决,花了不到两天,核心方法论就是本文提到的:先取证、再隔离、后优化、上监控。
9. 几句实操心得
文章写到这里,核心内容已经全部铺开。最后我再分享几个只有自己亲手调过 OOM 才会有的真实体会。
第一,OOM 不是“内存不够用”那么简单,它反映的是整个系统的内存水位管理是否健康。只看free -h的表层数字,往往会漏掉真正的风险。养成看/proc/meminfo、vmstat、dmesg这三件套的习惯,比什么监控工具都靠谱。
第二,参数调优永远是最后一步,不是第一步。先把业务侧的无界缓存、超大堆、失控连接数查干净,再把内核参数作为补充手段来微调。顺序反了,你会在错误的方向上消耗大量时间。
第三,每个系统的 OOM 特征都不完全一样。有人的服务是被瞬时大对象打死,有人的是被某个隐藏的线程池撑爆,有人的是 swap 配置形同虚设。与其照搬别人的配置,不如花时间把你自己的 OOM 日志读细、读透。我遇到过一次特别隐晦的情况,OOM 日志显示被杀的是一个无关紧要的后台进程,但真正的问题在另一个进程的内核线程上——如果不看 Mem-Info 那一段,怎么都猜不到。
第四,容器环境下的内存治理和裸机不完全一样。cgroup 限制既是保护也是陷阱——保护是爆炸半径可控,陷阱是容器内的应用可能根本不知道自己的内存上限是多少,配置错得离谱却归咎于系统不稳定。给容器应用加内存参数校验和启动期自检,能省掉大量莫名其妙的排查时间。
最后想说的是,OOM 这件事看着吓人,其实本质就是“内存管理中的一次强制淘汰”。把它当成一次系统健康的体检,反而能帮你发现自己平时注意不到的隐患。我后面还会单独写一篇关于 cgroup v2 内存水位控制与 PSI 预警的进阶内容,如果这篇文章对你有帮助,欢迎关注后续更新。