日志里出现Out of memory: Killed process的那一刻你往往没有什么思考时间内核已经替你做了决定。我自己做运维和系统设计这些年见过太多人在这一行日志面前手足无措然后一顿乱调参数最后也不知道自己调的东西到底是救了系统还是埋了雷。我更喜欢把 Linux 的 OOM 机制当成一整套“制度设计”来看待。所谓“斩杀线”不是某一个神秘参数而是一条完整的制度链条从哪里触发、按什么规则找牺牲者、凭什么给部分进程豁免权、又靠什么在容器和 cgroup 层面把治理范围切割清楚。这篇文章不会只讲oom_score_adj怎么调而是把这套制度从上到下拆开给你一份可以落到生产环境的“制度设计手册”。1. 先看大局OOM机制为什么算得上“制度设计”1.1 虚拟内存的“信用体系”是制度的土壤要理解 OOM 为什么存在首先得接受一个反直觉的事实你写的程序申请内存得到的并不一定是真正的物理内存而是一张“欠条”。在 Linux 里malloc()成功返回之后内核只是给你分配了一段虚拟地址空间并没有真的把这些页放到物理内存上。等你真正写入数据触发缺页异常内核才会把物理页映射过来。这个过程叫“按需分配”英文是 demand paging。换句话说虚拟内存就是一套“信用体系”进程以为自己在使用内存但内核只是在账面上记账。正常情况下这个体系运转良好因为大量进程申请了虚拟内存之后并不会全部写满实际用到的往往只有一小部分。但账面上欠得多了总有兑付不了的一天。当所有进程都开始去写自己申请过的内存物理内存被真正吃满系统又回收不出足够页面的时候信用体系就面临挤兑。挤兑之下怎么办内核必须有一套强制处置的制度这就是 OOM Killer 存在的意义。1.2 三道防线回收、换页、处决很多人的理解里OOM 是“内存不够了就直接杀进程”这是个错误的印象。真实的内核应对顺序更像一家企业现金流紧张时的处理方式第一道防线是后台回收。内核通过 kswapd 这类内核线程在内存页下降到一定水位线时启动异步回收把不常用的页缓存和可回收页释放出来。这个阶段进程都还活着只是速度可能变慢。第二道防线是换页也就是 swap 的介入。当物理内存不足且页面无法快速回收时内核会把一部分匿名页写到 swap 空间腾出物理内存给更需要的进程。这个过程相当于“借新债还旧债”短期内能续命但债总是要还的。第三道防线才是直接斩杀。如果前两道防线都没有解决问题内核的分配路径会持续失败最终进入out_of_memory()这个函数按照一套打分机制选择某个进程处决掉。很多新手有个误区以为 OOM 是“突然发生”的。其实不是它是前面回收和换页都失效之后的终局。理解这一点你就知道 OOM 治理的核心思路不是把第三条线画得又多又密而是让前两道防线足够有效。1.3 制度的底线牺牲进程保护系统默认情况下Linux 在内存耗尽时选择杀死进程而不是直接让整个系统崩溃。这个选择本身就是制度设计的核心。vm.panic_on_oom这个参数就体现了不同侧重。默认值是 0意思是“内核来做裁决该杀就杀”如果设成 1那么触发 OOM 时内核会直接 panic设成 2 的话在很多场景下也会重启系统。我见过一些团队把panic_on_oom设成 1理由是“系统状态不可恢复不如重启”。这个想法有一定道理但你得为此承受业务中断的代价。对于绝大多数在线业务宁可牺牲一个批量任务或缓存进程也不应该让整台机器宕机。说白了OOM 制度设计的底线就是“用局部牺牲换取整体存活”。这个思路跟容灾设计里的“丢车保帅”是一样的。2. 第一根线画在哪里watermark 与内存超卖2.1 三条水位线决定内核何时介入很多人问内核到底从哪一刻开始觉得“内存不够了”答案不是等内存用到 0而是看几个水位线WMARK_HIGH、WMARK_LOW、WMARK_MIN。简单理解这三个水位线把空闲页面分成了几档。当空闲页面低于WMARK_LOW时kswapd 开始把后台回收线程唤醒尝试释放内存如果继续跌到WMARK_MIN以下内存分配就会走紧急路径内核会进入直接回收甚至 OOM 流程。这就好比一个大楼储备了不同级别的应急物资水位越高说明物资越充足一旦水位降到最低线保安系统就要进入警戒模式了。水位线的具体数值受多个因素影响其中最关键的是vm.min_free_kbytes。它直接参与计算最低水位线调大它相当于把“斩杀线”整体抬高系统会更早启动回收更积极地保持空闲内存。但凡事都有代价min_free_kbytes设得太大会有更多内存被预留而不能用于业务造成浪费。我的经验是不要盲目抄网上的“推荐值”。4G 内存的小机器和 128G 内存的数据库服务器对应急余量的需求完全不同。生产环境应该结合机器内存总量、业务内存峰值、是否启用 THP透明大页等因素来调。不同版本内核的具体计算策略也会有差异重点理解这个水位的含义而不是死记参数。2.2 overcommit 机制制度对“欠条”的容忍度前面讲过虚拟内存是信用体系。但系统对“信用扩张”的容忍度是可以配置的这就是vm.overcommit_memory。0是启发式模式也是大多数发行版的默认值。内核会根据当前可用内存和申请大小做一个粗略判断明显离谱的分配会被拒绝但绝大多数申请都会放行。1是“总是允许”模式。只要虚拟地址空间还能容纳内核基本不做限制。这种模式适合某些特殊计算场景但也是最容易触发 OOM 的模式。2是“严格模式”。内核会先算出一个 CommitLimit所有进程累计的虚拟内存总量超过这个限额时分配就会直接返回失败。CommitLimit 的计算大约是物理内存 × vm.overcommit_ratio / 100 swap 总量。我经常把 overcommit 比作银行放贷。0模式是“看人下菜碟”的信贷经理1模式是“只要身份证就放贷”2模式是“咱们行里设了硬性授信额度谁的贷款都不能超”。如果业务环境对内存一致性要求很高比如数据库集群的某些场景我更倾向于用2模式。它至少让内核在“信用账本”上有明确的红线而不是等到物理内存耗尽才发现问题。但在用2模式之前一定要先观察现有业务的Committed_AS与CommitLimit的实际关系否则很容易出现“内存还有富余但分配失败”的诡异问题。2.3 swap给斩杀线留一条缓刑通道swap 这个东西很多人又爱又恨。恨它的理由是 swap 读写太慢爱它的理由是它能在内存紧张时给系统留出反应时间。从制度设计的角度看swap 相当于“缓刑通道”。有了它进程在物理内存不足时不一定立刻被处决而是先把一部分数据挪到磁盘争取到几秒甚至更长的调度窗口。vm.swappiness控制内核使用 swap 的积极程度范围是 0 到 100。之前流行一个说法是“设置成 0 就永远不用 swap”这在老内核上基本成立但新内核的行为有所变化即使 swappiness 为 0在内存压力极大时仍然可能发生换页。我的建议是对 SSD 机器swapiness 设置在 10 到 30 之间比较合理对传统机械盘建议设置更低否则频繁换页会让 IO 成为新的瓶颈。更重要的是swap 设置要有足够的空间去承担“缓冲”的角色不能只留几百 MB 意思一下。3. 核心审判规则OOM Killer 凭什么选中某个进程3.1 打分机制不是在杀“谁占内存最多”OOM Killer 的裁决逻辑并不是简单地找占用物理内存最多的进程。内核里有一整套打分机制最终会算出一个oom_score进程得分越高越容易被杀。这个打分参考的因素包括进程常驻内存大小、swap 中的页面数、页表本身占用的页面等。此外进程运行时间也会被纳入考虑刚运行几秒的进程往往会比已经跑了几个月的进程承担更高分数。这个看起来冷酷无情的规则其实有它的道理新进程刚启动杀了它的代价最小老进程运行了很久可能保存了大量中间状态杀掉它损失更大。更反直觉的是OOM 打分看的不仅是当前真实使用量。进程申请了大量虚拟内存即使还没有完全写满物理页也会显著影响得分。因为虚拟内存代表的是“潜在的风险敞口”内核在作出生死裁决时会把这部分风险也考虑进去。所以你会看到一种现象一个看起来“只占了几百兆”的 Java 程序因为虚拟内存高达几十 G反而比另一个真实占用 2G 的进程更容易被选中。3.2 oom_score_adj制度里的“自由裁量权”默认打分只反映了内核视角的“公平”但实际业务中我们往往需要人为干预。这时oom_score_adj就派上用场了。它的取值范围是 -1000 到 1000默认是 0。负数代表降低被杀的优先级正数代表提高。设成 -1000 时进程几乎获得永久豁免权设成 1000 时OOM Killer 会第一个盯上它。我在生产环境中做制度设计时会对核心服务单独设置数据库、注册中心、配置中心这类基础设施设置oom_score_adj-800甚至-900常规 Web 服务保持默认 0让内核按真实内存占用来裁决临时批量任务或数据处理进程设置正数让它们成为“优先牺牲对象”。这里有一个重要的原则豁免权不能滥用。如果整个系统里所有进程都把oom_score_adj设成 -1000那么 OOM Killer 就无差可杀最后只能走 panic 路线反而让整台机器直接宕机。制度的善意必须留给真正关键的少数派。如果你用 systemd 管理服务可以直接在 service 文件里配置[Service] OOMScoreAdjust-800这个配置比自己在启动脚本里写echo更加规范systemd 会在拉起服务时自动设置好。3.3 执行阶段线程组、候选者与最终处决OOM Killer 选“谁”来杀并不是只看一个进程的单独得分。现代内核里多个线程往往处于同一个线程组内核会选择性地评估整个组的影响有时候为了释放足够内存会同时杀掉同一进程组里的多个进程。实际执行上触发 OOM 的进程往往不是得分最高的那一个而是“更容易被杀”的那一个。内核会从所有候选进程里挑一个它认为最合适的目标然后向它发送 SIGKILL。这里有个容易踩的坑如果某个容器的根进程承担了oom_score_adj-1000OOM 时内核找不到合适的候选者可能直接跳过它去杀别的进程甚至导致全局 OOM 处理逻辑进入异常路径。所以千万不要把生产环境里所有进程都保护起来那不是制度设计那是制度失效。4. 分层制度cgroup 给不同业务划边界4.1 全局 OOM 太粗暴需要更小的治理单元全局 OOM 机制最让人头疼的问题是“殃及池鱼”。一个业务把内存吃满可能让另一套无辜的业务被系统杀掉。要想真正做到“谁的问题谁负责”你需要一套比全局级别更细的治理制度这就是 cgroup 的用武之地。cgroup 是 Linux 内核提供的一组资源控制机制它可以把一组进程圈在限定范围内对它们的内存占用单独设限。cgroup v2 里的memory.max就相当于一个硬性的“制度围墙”这个 cgroup 内的进程总内存超过这个值内核就会在 cgroup 内部先执行回收回收不过来就会杀掉该 cgroup 内某个进程。这个设计和全局 OOM 最大的区别是cgroup 的 OOM 只影响圈内进程不会波及外面的服务。相当于把全局无差别的“禁杀令”替换成了“地方法规”谁踩红线谁担责。4.2 hard limit 与 soft limit 的区别memory.max vs memory.highcgroup v2 里有三个关键参数需要搞清楚参数含义行为memory.max硬性上限超限后回收收不回来就杀进程memory.high软性上限超限后对进程进行节流但不立即杀memory.min最低保证相当于给圈内进程预留兜底额度memory.high是一个很实用的制度设计工具。它不会立刻杀死进程而是让这些进程的内存申请速度变慢给系统留出足够的处理时间。比如一个数据分析任务你可以给它设置memory.high略高于memory.max。正常运行时不触发节流一旦出现突发内存增长先是性能下降如果继续涨到硬上限则由内核执行处决。这就像企业先给业务发预警函而不是直接走司法程序。4.3 事件监控让制度留有“档案”cgroup v2 下可以通过memory.events这个文件查看内存治理的统计信息/sys/fs/cgroup/mybench/memory.events里面会记录low、high、max、oom等关键事件的发生次数。其中max表示触发了硬性上限oom表示发生了 cgroup 级的进程斩杀。我在生产环境做监控时会定时采集这些事件把它们接入监控系统。一旦看到oom事件增长即使当前系统还能跑也应该立即排查是哪个业务在持续冲高。制度设计的价值不在于“出事了再杀”而在于让你能提前知道“可能要出事”。5. 制度落地一份可直接抄的生产环境参数参考5.1 参数速查表以下是我在常见生产场景下比较常用的起始配置注意是“起始配置”具体数值需要结合你的业务调整配置项建议起始值设计意图vm.overcommit_memory0 或 2通用业务用 0核心账务类用 2vm.overcommit_ratio80-95在严格模式下预留信用额度vm.min_free_kbytes按机器内存预留调高可提前回收但会浪费vm.panic_on_oom0默认交给内核处理vm.swappiness10-30保留 swap 逃生通道vm.admin_reserve_kbytes合理余量防止 root 操作被内存不足卡死vm.user_reserve_kbytes合理余量保障普通用户仍可操作这些参数都可以通过/etc/sysctl.conf持久化配置。改完参数用sysctl -p使其生效。5.2 给关键服务设豁免权假设你有三台机器部署了一个数据库服务还有一些边缘计算任务。边缘任务的内存峰值很高很容易吃满系统如果每次都杀你心爱的数据库整个业务就废了。用 systemd 管理时在数据库服务的 service 文件里加[Service] OOMScoreAdjust-800加上这个之后数据库进程的oom_score会被大幅降低OOM 时会优先考虑其他候选进程。我做过的经验总结是核心数据库可以给到 -900 左右但不要轻易给到 -1000。虽然 -1000 等于直接免死金牌但如果这个进程内部有严重的内存泄漏且没有其他进程可杀反而会让系统进入更危险的状态。留一点点余地不是坏事。5.3 最小复现实验亲手触发一次 cgroup OOM纸上谈兵不如动手做一次。你可以用 cgroup v2 快速复现一次局部 OOM观察整个过程。先创建一个 cgroup设置内存上限mkdir /sys/fs/cgroup/mydemo echo 160M /sys/fs/cgroup/mydemo/memory.max然后写一个小脚本不断申请内存#!/bin/bash pages while true; do pages$(head -c 1048576 /dev/zero | tr \0 a) done把这个脚本的进程放入 cgroupecho $PID /sys/fs/cgroup/mydemo/cgroup.procs运行一段时间后在dmesg里会看到类似这样的日志Memory cgroup out of memory: Killed process 12345 (bash) total-vm:...同时查看cat /sys/fs/cgroup/mydemo/memory.events能看到oom计数加一。这个过程能帮你建立非常直观的印象cgroup 级别的制度边界是如何起作用的。6. 常见问题与排查实录6.1 一行日志里的信息量OOM 日志里经常出现这种格式Out of memory: Killed process 12345 (myservice) total-vm:5123001kB, anon-rss:812340kB, file-rss:45678kB, shmem-rss:1234kB, UID:1000 pgtables:4567kB oom_score_adj:0很多人盯着total-vm看以为这就是进程的真实占用。但total-vm其实是虚拟内存总量真正决定“内存压力”的是anon-rss匿名页常驻内存和部分file-rss文件页缓存。如果一个进程 total-vm 巨大但 anon-rss 很小说明它只是分配了虚拟空间但没有实际写满不一定是首要的斩杀目标。排查 OOM 时建议结合free -g、ps aux --sort-rss一起看先找真实匿名内存占用最高的进程再做下一步判断。6.2 明明内存还有余量为什么还是被杀这是我被问得最多的问题。现象通常是free -h显示还有几个 G 空闲但日志里就是触发了 OOM。可能的原因有以下几种内存碎片化严重即使总空闲内存不少但系统在某个内存水位线下找不到足够连续的空闲页来满足分配请求被迫触发 OOM不可回收内存太多page cache 里有大量脏页swap 空间又满了匿名页无法换出导致系统“手上没牌”内存水位线设定过高min_free_kbytes被调大后系统认为“可安全使用的内存”很少虽然数值上空闲还挺多。遇到这种情况只看free是远远不够的。至少看一眼/proc/buddyinfo和/proc/pagetypeinfo来判断是否存在碎片问题。碎片导致的 OOM靠杀进程往往治标不治本该考虑调整内存分配策略或者迁移到更合适的内存模型。6.3 容器内进程被杀但容器还在运行云原生环境下cgroup OOM 和全局 OOM 的差异会带来一个非常常见的误判容器里的某个进程被杀了但容器本身没有退出。这是因为容器一般用自己的 cgroup而容器内 PID 1 可能是 init 进程。如果被杀的是业务程序只要 PID 1 没死容器就不会整体退出。于是监控系统看到的就是“容器存活服务挂了”。排查时需要区分两个层面这个 OOM 是发生在 cgroup 内部看memory.events还是发生在整个宿主机层面看系统日志。如果只看容器状态你根本找不到真正的根因。6.4 与 OOM 长期共存的治理心态写到最后我想分享一个在实际运维中形成的观点不要试图把 OOM 从系统里完全消灭掉而要通过制度设计把 OOM 的杀伤范围控制在一个可接受的水平。我的具体做法是三个字先分类、再设线、后监控。先分类是说把业务按重要程度分为核心服务、普通服务、临时任务再设线是给每一类服务设置不同的oom_score_adj和 cgroup 内存限制后监控是持续采集 OOM 事件和内存压力指标。这套思路看起来简单但执行起来非常考验对业务的了解。你越清楚你的服务在内存耗尽时谁该活、谁该死你的制度设计就越成熟。经历过几次因为配置混乱导致的正反馈以后我再也不会把所有进程放在同一条起跑线上等内核裁决。内核就像一个维护秩序的执法者它的默认规则是公平但我们作为系统设计者有责任提前告诉它“哪些是城市命脉哪些是可以牺牲的耗材”。这就是我理解的 OOM“制度设计”不是让内核乱杀而是让它在极端情况下依然能保护你真正想保护的东西。