ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

深入解读Linux OOM Killer:原理、日志排查与关键进程保护

深入解读Linux OOM Killer:原理、日志排查与关键进程保护 1. OOM Killer是什么它选择杀死谁1.1 从一次真实的“被杀”事故说起先讲个我自己的经历。有一回凌晨两点半线上MySQL实例突然连接全部超时监控平台疯狂告警。我登录服务器一看mysqld进程没了但系统也没重启——这基本就锁定了一个原因OOM Killer动手了。翻了一下/var/log/messages果然躺着一行Out of memory: Killed process 3347 (mysqld) total-vm:12856396kB, anon-rss:6152484kB, file-rss:4120kB这一行信息量极大。“Out of memory: Killed process”说明内核内存管理子系统判定系统内存已经耗尽必须选一个进程杀掉以释放内存进程号3347、进程名mysqld、虚拟内存total-vm约12.8GB、匿名内存anon-rss约6.1GB全部记录在案。很多刚接触Linux运维的朋友会把OOM Killer当成一种“bug”或者“故障”其实它在Linux内核的内存管理中是一个非常古老、必要的兜底机制。Linux采用“过度分配”overcommit的内存策略进程申请内存时内核一般不会立即拒绝而是先记账等到物理内存和swap都不够用的时候内核就必须采取强制手段——选择并杀掉一个进程避免整台机器因为内存耗尽而彻底卡死。它的存在解决的是一个天生死结内存不够是必然的但内核必须在“杀一个进程”和“系统瘫痪”之间做选择。我当然不希望生产环境走到这一步但只要你机器上跑着业务只要你遇到过内存突刺你就必须面对它、理解它然后学会主动配置它。这篇文章要讲的就是在CentOS 7上OOM Killer的选人逻辑是什么、日志怎么看、参数怎么调、关键进程怎么保护。1.2 oom_score评分机制的本质OOM Killer选择的“牺牲品”并不随机内核会为每一个进程计算一个oom_score分数。你可以把它想象成考试排名——分数最高的那个进程在内存不足时最先被“淘汰出局”。从内核源码的实现逻辑来看oom_score的核心由两部分组成一部分是进程实际内存消耗另一部分是oom_score_adj附加分。前者很好理解谁占内存多、谁释放潜力大谁就更危险后者则是一个可以由管理员手动调整的投标值范围是-1000到1000。你可以实际看一眼自己机器上的分数执行cat /proc/1/oom_score cat /proc/1/oom_score_adj正常情况/proc/1/oom_score会是一个正数比如200多/proc/1/oom_score_adj则往往显示0。这两个文件的对应关系是$$ oom_score 原始打分 oom_score_adj $$其中原始打分与进程内存占用、运行时长、进程优先级等因素有关内核源码里有专门的oom_badness()函数计算。我在这篇文章里不展开内核源码的每个细节但你需要记住一个最关键的结论占用物理内存越大、且长时间存活的高消耗进程越容易被OOM Killer选中。我见过不少朋友以为OOM Killer杀的是“最后一个申请内存的进程”这是误解。内核优先杀掉oom_score最高的那个进程而不是最新触发内存不足的那个进程。理解这一点后你就能明白为什么Java堆开得很大的服务、MySQL缓冲池开得很满的数据库在内存吃紧时最容易“被选中”。它们不是“受害者”而是内核视角里“性价比最高”的释放源——杀掉一个6GB的Java进程比杀掉一个300MB的PHP-FPM进程能更快速地解救系统内存危机。2. 被误伤之前的现场取证2.1 日志怎么看关键行是什么OOM Killer杀人的那一刻内核会在日志里留下非常完整的信息。CentOS 7默认的日志系统是rsyslog加journald所以通常有两个地方可以看journalctl -k和/var/log/messages。先用这个命令快速确认最近有没有发生过OOMjournalctl -k | grep -i out of memory或者直接在messages里搜grep -i out of memory /var/log/messages一旦搜到结果不要只看 “Killed process” 这一行它的前面通常会跟着一大段内存状态快照。信息大概长这样Node 0 DMA free:0kB min:68kB low:84kB high:100kB Node 0 DMA32 free:4912kB min:5092kB low:6364kB high:7636kB Node 0 Normal free:18768kB min:49416kB low:61888kB high:74152kB ... Out of memory: Kill process 2322 (java) score 552 or sacrifice child这段信息展示的是系统内存各个zone的状态free低到个位数、min校验不过内核才会进入OOM流程。我个人的建议是不要纠结每一个字段看三样东西就够了——被杀的PID和进程名、score分数、当时还活着的内存大户。这三样就能帮你快速判断是真实内存不足还是某个进程把机器吃穿了。还有一个很隐蔽的信息点如果日志里出现Killed process ... (java) total-vm ... anon-rss ...之后紧接着又有“重启”相关字样那说明OOM发生在内存压力极其严峻的时刻可能连系统本身都不稳定了。这种情况已经不是调参数能救的优先考虑扩容或者迁移。2.2 内存审计的几个实用命令在你动手调整OOM Killer配置之前必须先搞清楚“谁的锅”。我一般会按顺序执行下面几条命令把现场完整撸一遍。第一确认整体内存水位free -h第二按物理内存占用排序查看进程TOP榜ps aux --sort-rss | head -20第三如果用的是cgroup隔离环境还要看看cgroup层面的内存限量与实际占用cat /sys/fs/cgroup/memory/xxx/memory.usage_in_bytes cat /sys/fs/cgroup/memory/xxx/memory.limit_in_bytes第四我强烈建议装一个sysstat系工具或者至少养成观察vmstat 1中si、so字段的习惯。这两个字段分别代表swap换入换出量如果si、so持续高位说明内存已经严重紧张、swap成了生命线这时候离OOM就不远了。一次完整的内存审计应当在5分钟内完成。如果发现某个业务进程长期占用RSS超过物理内存的50%那不用等OOM Killer来杀你自己就得上限流方案或者考虑拆分服务了。3. 系统级参数配置临时改法和永久改法3.1 三个核心开关的含义与推荐值CentOS 7的OOM Killer行为主要由/proc/sys/vm/下的几个参数控制配置文件写死在/etc/sysctl.conf里。下面这三个参数是我认为最核心的。第一个是vm.overcommit_memory。它控制内核是否允许内存过度分配取值有0、1、2三种。0默认值内核启发式判断允许一定程度过度分配。1总是过度分配不打回票风险大生产不建议。2禁止过度分配内核严格按照“物理内存swap”的可用量拒绝超额申请。我见过有人为了“防止OOM”把overcommit_memory改成2结果反而引发奇怪问题——很多应用在启动阶段会申请大量虚拟内存比如JVM预留堆外内存、数据库初始化buffer pool这些申请在0模式下没任何问题改成2以后直接被内核拒掉进程起不来。所以除非你非常明确你的应用内存模型否则不要随意改这个值为2。第二个是vm.panic_on_oom取值为0、1、2。0默认值内核执行OOM Killer杀掉高分进程。1内核直接panic重启系统。2同样是panic但限制在cgroup等特定场景下生效。我推荐生产环境保持0。虽然进程被杀很痛苦但至少机器还活着你可以快速登录排查、恢复服务。把它设成1虽然避免了“杀错进程”的概率但整机宕机带来的损失往往更大。当然如果这台机器是核心数据库你宁可它快速重启也不愿意它把数据库进程误杀了那可以斟酌设成1——但那时“重启”其实也是你主动选择的结果。第三个是vm.oom_kill_allocating_task。0默认值内核按oom_score选最高的杀。1直接杀掉触发内存不足的那个进程。这个参数设为1以后逻辑非常简单粗暴谁引发内存不足就杀谁。但前面说过触发者未必是内存占用最大的所以大多数场景下不建议开除非你在跑了某些特定的批处理任务明确知道是哪个进程在疯抢内存想让“罪魁祸首”承担后果。3.2 sysctl.conf持久化配置实操临时改法很简单直接写/proc/sys/vm/下的文件就能生效比如echo 0 /proc/sys/vm/panic_on_oom echo 0 /proc/sys/vm/oom_kill_allocating_task但要注意服务器一旦重启这些改动会全部丢失。所以持久化配置必须写入/etc/sysctl.conf推荐用sysctl命令来写而不是直接echo vim /etc/sysctl.conf在文件末尾加入vm.panic_on_oom 0 vm.oom_kill_allocating_task 0 vm.overcommit_memory 0保存退出后执行sysctl -p这样改动就会立即生效并且重启后保持不变。这里有一个容易踩的坑sysctl -p执行的时候如果某个参数名或者参数值写错了它会报错并且继续往下执行。所以执行完以后务必再次确认sysctl vm.panic_on_oom vm.oom_kill_allocating_task vm.overcommit_memory看到输出的值和你预期一致才算真正的持久化生效。4. 进程级保护让关键服务“免死”4.1 oom_score_adj手动调节系统级参数只能决定“杀不杀、怎么选”但对于生产环境来说你往往还有一个更急迫的需求让某一个进程尽量别被杀。这就轮到oom_score_adj上场了。oom_score_adj取值从 -1000 到 1000其中 -1000 有一个特殊含义表示该进程对OOM Killer完全免疫内核跳过了它。不过这个免疫是有前提的——只有当oom_score_adj -1000时内核才会把该进程从OOM候选名单里摘除。假设你要保护mysqld先找到它的PIDpgrep -f mysqld # 假设输出 3347然后手动设置echo -1000 /proc/3347/oom_score_adj改完以后立即查看验证cat /proc/3347/oom_score_adj cat /proc/3347/oom_score你会发现/proc/3347/oom_score变成了0。因为oom_score x (-1000)最终被钳制到0。这也从侧面解释了oom_score的最小值是0不可能出现负数。需要特别提醒的是千万、千万、不要一上来就把所有进程都设成 -1000。如果每个进程都免疫了那OOM Killer还怎么选它只能去杀那些没设免疫的进程最终系统依然是崩溃只是牺牲品变成了你没想到的那个。保护要讲优先级只保护真正不能间断的核心服务。我个人的习惯是数据库进程、Redis进程设成 -1000 或者至少 -800普通应用服务设个 -100 到 -400那些本身就无状态、死了能马上重启的Nginx节点、静态页面服务器维持默认0即可必要时甚至可以调高到正数让OOM优先杀它们。4.2 systemd与cgroup场景下的保护配置CentOS 7上的服务大多由systemd管理而systemd又会把服务放进各自的cgroup切片里。此时你更应该用systemd的原生配置去管理OOM保护而不是每次重启后手动写echo -1000 /proc/PID/oom_score_adj。对于systemd管理的服务编辑对应的service文件vim /usr/lib/systemd/system/mysqld.service在[Service]段里加上一行OOMScoreAdjust-1000然后重载配置并重启服务systemctl daemon-reload systemctl restart mysqld systemctl show mysqld -p OOMScoreAdjust最后一条systemctl show是用来验证的它会输出你设定的值。这种方式的优势有两个一是不用记PIDPID怎么变化都由systemd接管二是开机自启时自动生效不会因为服务重启而丢掉保护配置。如果服务不在systemd下而是跑在自定义cgroup里那你需要关注的是 cgroup 自带的OOM控制接口cat /sys/fs/cgroup/memory/your_cgroup/memory.oom_control这个文件里有oom_kill_disable和under_oom两个字段。当oom_kill_disable为1时这个cgroup里的进程不会被内核OOM Killer杀死但代价是cgroup内所有进程可能会被阻塞在内存分配等待中表现就是任务卡死。所以cgroup的 oom_kill_disable 是一把双刃剑除非你非常确定这个cgroup里跑的是绝对不能杀的核心进程且内存突刺出现概率极低否则不建议长期开启。另外如果你的内核支持 memory cgroup v2CentOS 7的默认cgroup版本通常是v1但也可以看到memory.oom.group之类的新选项。这个问题展开讲又是一篇长文我在这只提一句生产环境先确认你的cgroup版本再选择对应的OOM控制接口别把文档里的用法用错版本。5. 常见问题与排查技巧实录5.1 问题速查表把我在运维过程中遇到的典型问题整理成了下面这张速查表按照“症状—定位—解决”三步来写。你可以收藏起来下次遇到OOM相关问题时按图索骥。症状首要排查点常用命令/工具解决思路日志出现Out of memory进程被杀确认被杀进程的oom_score和当时内存占用分布journalctl -k/grep Out of memory /var/log/messages结合日志判断优先扩容或限制单个进程内存服务频繁被杀但free显示内存还有大几百MB检查page cache是否被清空、查看min_free_kbytes是否过高cat /proc/sys/vm/min_free_kbytes/free -h降低min_free_kbytes或优化网络参数避免内核误判内存紧张同一进程反复被杀像中邪一样检查cgroup内存限制是否被击中cat /sys/fs/cgroup/memory/xxx/memory.usage_in_bytes调高cgroup限制或优化业务内存模型jvm堆不要开满宿主内存Java进程被杀堆内存设置远小于物理内存检查是否存在堆外内存泄漏、直接内存Direct Memory超限dmesg -T/jcmd PID VM.native_memory调整DirectBuffer容量、限制RSS上限设置oom_score_adj-1000后进程还是被杀确认是否在cgroup层面被OOM Kill或者进程是否重启导致设置丢失journalctl -k/cat /proc/新PID/oom_score_adj改用systemd的OOMScoreAdjust配置杀完进程后系统恢复但反复触发确认是否为swap过小内存回收不及时cat /proc/sys/vm/swappiness/free -h适当调高swappiness合理用swap缓冲内存压力换用panic_on_oom1后机器直接重启确认是否有其他监控脚本依赖这台机器持续存活sysctl vm.panic_on_oom重新评估panic策略多数场景建议回到0这里有一个点值得单独展开min_free_kbytes很多人不关注但它确实会导致“明明还有内存却被OOM”。原理很简单内核给每个内存zone预留了最低水位线如果你把min_free_kbytes调得过大内核会认为可用内存不足以满足最低预留要求从而提前进入OOM流程哪怕你明明看到还有几百MB空闲。我遇到过一台2GB内存的机器被前同事把min_free_kbytes调成了128MB结果频繁OOM查了很久才发现是这里的问题。所以排查OOM时这个参数必须列入怀疑清单。5.2 我踩过的几个坑第一个坑是“无脑给数据库设置-1000”。有一回我为了保护一台物理机上的MySQL把它设成了oom_score_adj-1000但忽略了同机还跑着一个占用内存极大的Elasticsearch。结果下一次内存突刺时OOM Killer绕过了MySQL直接把ES杀了。ES进程死了以后一堆业务写入报错MySQL本身没死但也被打进来的流量拖垮了。这次事故教会我保护某个进程之前先想想你会把风险转嫁给谁。正确做法不是把所有进程都加保护而是优先控制内存大头防止OOM出现。第二个坑是“改完sysctl.conf不验证”。我见过一次sysctl -p执行后没有任何报错但配置没生效的情况——原因是配置文件里某个参数名写错了比如vm.oom_score_adj这种不存在的路径。sysctl对不存在的参数会报错但对某些“存在但不想让你改”的参数它会静默跳过。所以我在生产中一直坚持“改完必须查”的原则用sysctl 参数名逐一确认。第三个坑是关于Java进程的。很多人把JVM的-Xmx设成总内存的60%以为这样就不会OOM了。但JVM除了堆内存还有Metaspace、线程栈、DirectBuffer和JIT编译后的Native内存加起来完全有可能超过-Xmx。vmstat里如果看到si、so频繁交换同时dmesg里出现OOM先别急着怪OOM Killer配置老老实实算一笔账物理内存总额、swap总额、JVM实际RSS峰值、其它进程RSS峰值四者相加是否超过了物理内存。超过就是真超了参数再怎么调也救不了。还有一个比较隐蔽的坑是“swap分区为0”。CentOS 7安装时如果完全不给swap内存紧张时内核连最后的缓冲都没有OOM会非常急躁。虽然swap性能很差但它是内存压力的泄洪渠。生产环境建议至少配置物理内存20%的swap空间。就算你压根不想用swap也要给它留个一二十GB的心理防线关键时刻能给你争取登录排查的几十秒。6. 一点个人体会按照惯例最后不搞什么长篇总结了说几句真实感受。我给这几十台CentOS 7服务器调OOM Killer参数调了六七年最大的体悟是OOM Killer不是敌人它是一道最后防线。你的目标不是消灭它而是让它不要频繁出手同时防止它出手时误伤你的核心进程。内存充足时谁也不会想起OOM Killer但内存一旦告急它能决定你是“损失一个进程”还是“损失整台机器”。所以不要等收到告警才开始研究找个周末把你线上每台机器的cat /proc/sys/vm/*全部看一遍把你的核心业务进程oom_score_adj主动设好把sysctl.conf里该写的参数写全。这一套动作半小时就能完成但它能避免你在凌晨三点对着日志发呆。另外千万别忘了定期查看日志。OOM是一个结果不是原因。真正的原因永远是内存规划不合理、某个进程有泄漏、或者流量超出预期。配置只是帮你度过危机的拐杖把内存模型和容量规划做好才是根上的解法。愿你的服务器永远不会在半夜弹OOM告警。
返回列表