上周四晚上十点多,我被值班告警吵醒。某后端服务的容器被 OOM Kill,自动重启了两次。我第一反应是登录宿主机看内存,free -h显示还剩十几个 G,buff/cache加起来也就用了六成多,看起来一点都不紧张。可容器里的进程就是被内核杀了。这种“内存明明很充足,进程却死了”的现象,我在排查 Linux 内存管理问题时见过太多次。
它背后涉及的远不止一条命令,而是一整套机制:虚拟内存、页表、缺页异常、page cache、回收策略、OOM 评分,还有 cgroup 层面的隔离。这篇文章想把这些机制串起来讲一遍,重点放在“拿到问题现场后如何正确判断和处置”。无论你是后端开发、SRE,还是自己维护服务器的爱好者,把这套逻辑理顺之后,再遇到内存告警,就不会只有重启大法这一招了。
1. 经典误判:free 显示内存充足,进程却还是被 OOM 杀掉
1.1 同一台机器,为什么会有两套“内存账本”
Linux 内存管理最容易被忽视的一点,是“系统全局内存”和“进程可用内存”不是一回事。free默认显示的是当前内存命名空间(多数情况下就是宿主机)全局的内存状态。一旦进程被放进 cgroup 里,它能用多少内存,就不是宿主机有多少内存说了算,而是看 cgroup 的memory.max(cgroup v2)或memory.limit_in_bytes(cgroup v1)。
举一个我真实遇到的例子:某容器 cgroup 限制是 4G,进程实际已经用到了 4.5G,其中一部分页面可能被换出到了 swap,但在 cgroup 的记账里依然算在头上。这个时候内核会在 cgroup 内部触发 OOM,把这个容器里的进程杀掉。宿主机上free -h显示总量可能还剩几十 G,完全不影响其他容器,可这个容器里的进程就是被杀了。
所以判断“某个进程会不会因为内存被杀”,不能只看宿主机全局内存,还得看它所属的 cgroup 账本。这就是 Linux 内存管理里的“两套账本”:一套是全局物理内存和交换空间的总量,另一套是 cgroup 层级上每个控制组的内存用量和上限。两套账本同时存在、同时生效,漏掉任何一个都会误判。
1.2 容器场景下的二次误解:free 不等于 cgroup 状态
更麻烦的是,容器里跑free -h很多时候并不反映容器的真实配额。因为普通容器内的/proc/meminfo读到的还是宿主机的全局数据,除非用了类似lxcfs这样的虚拟化工具去把宿主机的内存信息改写成容器配额视角。所以容器里free显示剩余很多,不一定代表该容器还能继续吃内存。
正确做法是直接看 cgroup 文件。cgroup v2 体系中,重点关注这几个:
memory.current:当前 cgroup 的内存用量,单位是字节。memory.max:cgroup 的内存上限,max表示不设限。memory.swap.current和memory.swap.max:swap 部分的使用量及上限。memory.events:记录过oom、oom_kill、throttle等事件次数,一旦有oom_kill计数,说明这里发生过杀进程。memory.stat:详细分类,比如anon、file、slab、pgpgin、pgpgout,可以用来分析内存构成。
比如这次告警,我登录上去后先看的就是/sys/fs/cgroup/memory.events,里面oom_kill计数已经增加了。再配合dmesg里的 OOM 信息,基本就能确认是这个 cgroup 的限制被突破了,而不是宿主机物理内存真的耗尽。
提示:在 cgroup v2 下,
memory.current统计的内存包括进程的匿名页、page cache、内核对象等多个部分。即使进程本身的 RSS 不大,但只要它读写过的文件页缓存全部挂在它的 cgroup 名下,memory.current一样会被撑爆。这一点在排查“进程 RSS 不高但容器被杀”时尤其重要。
2. 手把手读懂 /proc/meminfo:别只盯着 free 和 cached 两列
2.1 几个关键字段到底是什么意思
/proc/meminfo是 Linux 内存信息中最权威的来源,free命令最终读取的也是这个文件。很多人只看free命令那两行输出,其实文件里每个字段背后都对应一种内存状态。我把最常用、也最容易误读的几个字段列出来。
| 字段 | 含义 | 判断价值 |
|---|---|---|
| MemTotal | 物理内存总量 | 确认机器的内存规格 |
| MemFree | 完全空闲的物理页 | 很容易误导人,空闲少不代表紧张 |
| MemAvailable | 估算的、可分配给新程序的内存量 | 比 MemFree 可靠得多,建议作为核心监控指标 |
| Buffers | 块设备/原始磁盘缓存 | 一般不大,影响有限 |
| Cached | page cache + tmpfs 等共享内存 | 大部分可回收,但要排除 Shmem |
| Shmem | tmpfs、共享内存段 | 计入 Cached 但不可作为普通缓存随便回收 |
| Slab | 内核对象使用内存 | 分 SReclaimable 和 SUnreclaim 两半 |
| SReclaimable | slab 中可回收部分 | 内存紧张时可回收 |
| SUnreclaim | slab 中不可回收部分 | 无法通过 vmscan 直接回收,增长异常要警惕 |
| AnonPages | 匿名页(进程堆、栈等) | 进程私有内存的主要构成 |
| Dirty | 等待写回磁盘的脏页 | 数值偏高说明写过大量数据还没落盘 |
| PageTables | 页表占用的内存 | 进程多或虚拟地址空间大时会涨,一般不大 |
| CommitLimit / Committed_AS | 当前系统承诺的虚拟内存量/上限 | 和 vm.overcommit_memory 配合判断是否可能分配失败 |
我经常遇到有人看到Cached几十 G,就判断“内存被吃光了”。其实这一大块本来就是内核拿空闲页做的文件缓存,目的是减少磁盘访问。系统内存紧张时,这部分是第一个被回收的,对业务来说反而是好事。
2.2 用 MemAvailable 和 PSI 判断“真紧张”
MemAvailable这个字段是内核在 3.14 之后加入的。它不像MemFree只统计真正的空闲页,而是把“可以回收的 page cache 中不需要写回/可丢弃的部分”、“SReclaimable”、“MemFree”等综合起来,估算出一个“在不触发 swap、不显著影响性能的前提下,还能给新进程分配多少内存”的数值。
所以判断内存紧张程度时,我的习惯是:
- 先看
MemAvailable。如果长期低于总内存的 10%,说明内存压力已经很大。 - 再看
Dirty。如果Dirty很大且持续不降,说明落盘速度跟不上写入速度,内存可能堵在写回路径上。 - 再看
SUnreclaim。这一块不可回收,如果它异常增长,通常意味着内核对象泄漏,比如某些驱动、网络连接或文件系统元数据没有正确释放。 - 最后配合
/proc/pressure/memory(PSI)来看。PSI 里some avg10如果超过 10,说明已经有进程因为内存回收等待了很长时间。这是比MemAvailable更直观的“痛苦指数”。
我见过不少系统MemFree长期接近零,但业务跑得非常稳,因为所有空闲页都被拿去做page cache了。反而见过另一个系统MemFree还有两个 G,但MemAvailable只有几百 M,这时候新起一个进程就被杀。所以,MemAvailable才是那个“关键时刻能救命”的指标。
2.3 一段来自压力场景的字段组合快照
假设某次内存告警现场截取到这样的/proc/meminfo(数值仅为示意):
MemTotal: 65712372 kB MemFree: 1040324 kB MemAvailable: 20112480 kB Buffers: 50310 kB Cached: 44825344 kB Shmem: 1024268 kB Dirty: 61234 kB Slab: 4512372 kB SReclaimable: 3698821 kB SUnreclaim: 813551 kB AnonPages: 15310235 kB PageTables: 94332 kB Committed_AS: 345678912 kB CommitLimit: 32856186 kB只看第一眼,MemFree只有 1G 左右,Cached 高达 44G,很多人会以为“内存暴了”。实际上MemAvailable还有 20G,说明大量 Cached 是可以回收的;Dirty只有几十 M,说明写回没有积压;SUnreclaim不超过 1G,内核对象也算正常。这个状态其实并不危险。
但如果MemAvailable掉到 1G 以下,Dirty涨到几个 G,SUnreclaim持续攀升,那就不是简单的“缓存太多”,而是回收路径已经不够用了。再加上 PSI 里some avg10高于 10,基本可以确定内存压力正在直接影响业务延迟。
3. 从 malloc 到缺页异常:虚拟内存是怎么一步步找到物理内存的
3.1 地址空间、页表与多级映射
说完了全局指标,再往底一层看进程视角。Linux 里每个进程看到的是一段从 0 开始的连续虚拟地址空间,x86-64 下用户空间通常是 0x0000000000000000 到 0x00007fffffffffff,约 128TB。这个地址空间是“虚拟”的,它必须通过页表映射到物理页帧上。
页表是多级结构,在 x86-64 上是 PGD、PUD、PMD、PTE 四级。每个 PTE 记录一个 4KB 物理页对应的物理地址、权限位、脏位、访问位等。进程每访问一次虚拟地址,CPU 的 MMU 就会去查页表,如果命中 TLB 缓存还好,一旦不命中,就要逐级查表,代价不小。
也正因如此,Linux 提供了大页机制。2MB、1GB 的大页能显著减少页表层级和 TLB miss。现代系统中广泛使用的 Transparent Huge Pages(THP)是内核自动把适合的匿名映射合并成大页,但它在部分高并发场景下会导致内存分配路径变长、page fault 延迟变大,所以很多服务会选择关掉它。这个取舍必须结合业务实测,不能人云亦云。
3.2 内核为什么采取“懒分配”
malloc申请内存的时候,内核其实只做了一件很便宜的事:在进程的地址空间里建立一段新的虚拟内存区域(VMA),告诉页表“这段地址将来要映射”,但并不会立刻给每一页分配物理内存。真正的物理内存分配发生在第一次访问这些地址的时候,也就是触发缺页异常(page fault)的时候。
你可以用一个很小的 C 程序来验证这一点。申请 1GB 内存但不读写,进程的VmRSS几乎不变;一旦按页去写,RSS 立刻涨上去。
#include <stdio.h> #include <stdlib.h> #include <unistd.h> int main(void) { size_t len = 1024UL * 1024 * 1024; char *p = malloc(len); if (!p) return 1; printf("allocated 1GB, pid=%d, press enter to touch pages\n", getpid()); getchar(); for (size_t i = 0; i < len; i += 4096) { p[i] = 1; } printf("touched all pages, press enter to exit\n", getchar()); return 0; }编译后跑起来,在另一个终端里看/proc/<pid>/status:
# 第一次,只 malloc 不写入 grep -E 'VmSize|VmRSS' /proc/<pid>/status # 按回车写入后再看一次 grep -E 'VmSize|VmRSS' /proc/<pid>/status实际效果非常明显:VmSize一直是 1GB 出头,但VmRSS从几 MB 跳到 1GB 左右。这说明真正占用物理内存的不是malloc,而是访问动作。
缺页异常又分 minor fault 和 major fault。minor fault 表示页表项缺失,但数据就在内存里,比如 page cache 命中;major fault 表示需要读磁盘,比如文件的 mmap 冷页或者 swap 换出的页换回来。线上判断内存问题时要看这个:如果major fault数量急剧上升,通常意味着内存压力导致数据被频繁换出/换入,或者冷启动文件映射太多,这在性能上是很糟糕的信号。
3.3 RSS、共享页与 PSS:衡量进程真实内存的三个视角
RSS(Resident Set Size)是大家最常看的指标,ps aux里的 RSS、top里的 RES 都是它。但 RSS 有一个很不准的地方:它把进程映射的共享库、共享内存段全部算在了这个进程头上。比如 20 个进程同时加载同一个.so,每个进程的 RSS 都把这个.so的大小算了一遍,结果 RSS 总和远大于真实物理内存占用。
衡量“单个进程真正独占了多少内存”,更合理的指标是 PSS(Proportional Set Size),也就是把共享页按共享进程数均摊后计入。smem这个工具统计的就是 PSS。
我建议你在排查内存时至少看三个视角:
- RSS:看这个进程对物理内存的占用峰值。
- PSS:看这个进程的“份额”是多少,常用于评估多进程架构(比如 PHP-FPM、Java 多实例、Nginx worker)的真实内存总量。
- VmSize:看虚拟地址空间的大小,但它基本不能代表真实内存占用,只能用来发现明显的地址空间泄漏。
另外别忘了写时复制(COW)。fork之后父子进程共享同一批物理页,只有在某一方写入时才复制。所以某个进程 fork 出大量子进程后,内存占用不会立刻成倍增长,但子进程一旦开始写入,物理内存就会开始翻倍。这也是很多服务从单进程模型切到多进程模型后,内存观测数据出现“延迟暴涨”的原因。
4. page cache 与内存回收:Linux 为什么不着急释放缓存
4.1 页缓存是加速器,不是内存浪费
在 Linux 里,读过的文件页并不会在被读完之后立刻丢弃,而是留在 page cache 中。下次再读同一段文件时,直接从内存返回,不用碰磁盘。写也一样,写文件先写进 page cache 标记为 dirty,再由内核的 writeback 机制异步刷回磁盘。
这套设计的根本思路是:物理内存闲着也是闲着,不如全部拿来做缓存,等业务真正需要内存时再回收。所以 Linux 宁可让MemFree趋近于零,也不主动把 cache 释放掉。这是性能优化,不是“内存泄漏”。
真正需要警惕的是两类特殊情况:
一类是Shmem。tmpfs、共享内存段、/dev/shm也会计入Cached,但它们有实际内容,回收意味着丢弃数据,所以内核不能像普通文件缓存那样随便回收。Shmem增长过快时,内存一样会紧张。
另一类是 mmap 文件但长期不访问的“冷页”。虽然 page cache 可以回收,但如果业务反复读取大文件且没有命中,就会出现“缓存一直在换血”,表现为pgpgin很高、major fault很多,业务延迟抖动。
4.2 回收时机与路径:kswapd、直接回收和 OOM
内核的内存回收不是等物理内存全满了才开始,而是按照水位线(watermark)来触发。系统把每个内存区域划分为min、low、high三个水位。空闲页低于low时,后台的kswapd内核线程开始异步回收;如果分配路径上等不及 kswapd 回收,就会进入direct reclaim(直接回收),也就是调用者线程自己一边分配一边回收。
direct reclaim是非常明显的性能拐点。因为申请内存的线程要停下来做回收,可能还要扫 LRU 列表、执行 shrink_slab 回调,整个分配耗时从微秒级变成毫秒级。很多服务在高并发下出现的 RT 突刺,背后就是direct reclaim被触发。
如果直接回收也拿不出足够内存,最后一步就是 OOM killer。它是内核宁可杀死进程也不让自己内存耗尽的一种保护机制。所以在内存问题上,OOM 不是独立的“事故”,而是整个回收链条走到尽头的信号。
4.3 几个值得谨慎调整的内核参数
关于内存回收,大家最想改的就是这几个参数。先说结论:不要照抄网上的“调优模板”,每个参数都要结合业务实际測。
| 参数 | 作用 | 我的建议 |
|---|---|---|
| vm.swappiness | 控制匿名页和文件页回收的偏好,值越大越倾向换出匿名页 | 一般服务器保持 10-30 即可,SSD 环境可以偏低,但不建议直接置 0,极端情况下会导致文件缓存回收过度 |
| vm.dirty_background_ratio | 脏页占可用内存比例达到该值时,后台开始写回 | 默认约 10,不需要频繁动 |
| vm.dirty_ratio | 脏页占可用内存比例达到该值时,写文件的进程会被迫同步刷盘 | 如果业务有很多大文件写入,可以适度调高,但太高会导致一次刷盘风暴 |
| vm.min_free_kbytes | 保留空闲内存的下限,但调高并不等于“留着内存不回收”,它会影响水位线和回收激进程度 | 我通常会保留默认,除非有 NUMA 相关的分配失败问题 |
| vm.vfs_cache_pressure | 控制 dentry/inode 缓存回收倾向 | 默认 100,常见调低到 50 来保留文件系统缓存,但需要测试 |
调参必须以监控数据为准。调完之后观察pgpgin/pgpgout、direct_reclaim次数、PSI 指标是否改善,而不是只盯着 free 列的数字。
5. OOM killer 与 cgroup 内存控制:守护关键进程的最后一环
5.1 内核怎么打分决定杀谁
当内存耗尽、回收也没用的时候,内核会触发 OOM killer 来选一个“牺牲者”。它并不随机,而是根据一个 badness 评分调低选。简单来说,一个进程占用的内存越多,越容易被选中;页面表、swap 用量也会叠加进去;同时还会考虑进程的oom_score_adj调整值。
你可以在/proc/<pid>/oom_score里看到每个进程当前的分数,分数越高越危险。oom_score_adj的取值范围是 -1000 到 1000:
- 默认 0,不调整。
- 负数表示降低被选中的概率。
- 正数表示提高被选中的概率。
- -1000 表示直接禁用该进程被 OOM killer 选中。
这里有一个很容易误解的点:OOM killer 不是按“谁占内存最大就杀谁”的绝对逻辑来执行的。它还考虑了很多细节,比如进程是否root、是否拥有比较长的运行时间等。所以在容器环境里,常常出现“明明有其他进程更吃内存,被杀的却是我的业务进程”这种情况,原因很可能是业务进程没设置oom_score_adj,而其他进程通过 systemd 配置了较低的打分。
5.2 如何用 oom_score_adj 保护关键进程
保护重要进程的最直接方法是调低它的oom_score_adj。比如我要保护监控 agent 和为远程排障保留的 sshd:
# 假设监控进程 pid 为 1234 echo -200 > /proc/1234/oom_score_adj # sshd 通常跑在 systemd 下,直接放到 service 配置更合适 # /etc/systemd/system/sshd.service.d/oom.conf [Service] OOMScoreAdjust=-500如果跑的是 systemd 管理的业务服务,比如 Java 后端、数据库实例,我会在 service 文件里加上OOMScoreAdjust=-500,让它在整台机器内存告急时活得更久一点。但注意,负数只降低被选中的概率,不代表绝对安全。把数据库这种最关键的进程设置成 -1000 也可以,但要承担风险——一旦它自身真的发生严重内存泄漏,内核连杀掉它的机会都没有,整台机器可能直接卡死,甚至连 sshd 都无响应。
我个人的平衡做法是:
- 监控 agent、sshd:-500 左右。
- 数据库、业务主进程:-200 到 -300。
- 真正可以被杀的重型批次任务:0 或正值。
这样既避免误杀关键服务,又保留内核最后的“止损”能力。
5.3 cgroup v2 的 memory.max、swap.max 与 oom.group
对于容器场景,OOM 往往不是在“全局物理内存耗尽”时发生的,而是在 cgroup 层级发生的。cgroup v2 下,memory.events里的oom计数就是 cgroup 层级的 OOM 触发事件。它和全局 OOM 的区别在于,它只杀这个 cgroup 里的进程,不影响其他 cgroup。
控制容器内存,除了memory.max外,还有一个非常容易漏掉的值:memory.swap.max。如果不设置 swap.max,cgroup 内部内存超限时会先用 swap,等 swap 也耗尽后再触发 OOM。这意味着“机器内存本来够用,但某个容器疯狂吃 swap,最后连 swap 都填满,还是被杀”。最稳妥的姿势是:memory.max和memory.swap.max一起设置,甚至把 swap.max 设置成 0,让容器内存边界清晰可预期。
还有一个参数memory.oom.group。把它设成 1 时,如果 cgroup 触发 OOM,内核会杀掉整个 cgroup 里的所有进程,而不是只杀触发者。这个语义对多进程服务来说很重要:如果你希望“容器内某个 worker 惹祸,整个容器一起重启”,就设 1;如果你希望“牺牲单个 worker,其他 worker 继续工作”,就保持默认的 0。
5.4 通过 oom 日志复盘内存突增
每次 OOM 发生后,dmesg里会保留完整的内核日志,包括被选为 victim 的进程、它的 RSS、swap 用量和当时的内存状态。这是最宝贵的复盘材料。
dmesg -T | grep -i -E "out of memory|oom-killer|killed process"日志里通常能看到类似这样的表述:某个进程以多少内存、预计多少页被选为 victim,以及Memory cgroup out of memory或Killed process。配合memory.events和memory.stat,基本可以还原出“是哪一层(cgroup/全局)在什么压力下触发”的完整链条。
我每次排完一个内存事故,都会把这几件事记录到排障文档里:触发时间、是 cgroup 还是全局 OOM、被杀进程的 RSS 与 oom_score、当时的 MemAvailable 和 PSI 数值。积累久了,很多看似突发的 OOM,其实都在早一两个小时的指标曲线上有迹可循。
6. 一次真实内存告警的完整排查链路
6.1 第一步:拉取现场,先还原 cgroup 与 dmesg 时间线
回到文章开头那次告警。我登录机器后按顺序执行了下面这些命令,每一条命令都有它要回答的问题。
# 1. 看内核日志里有没有 OOM 记录,被杀进程是谁 dmesg -T | tail -500 | grep -i -E "out of memory|oom|killed" # 2. 看宿主机全局内存 free -h grep -E "MemTotal|MemFree|MemAvailable|Dirty|SUnreclaim|AnonPages|PageTables" /proc/meminfo # 3. 看目标容器 cgroup 的内存账本 cat /sys/fs/cgroup/memory.max cat /sys/fs/cgroup/memory.current cat /sys/fs/cgroup/memory.swap.current cat /sys/fs/cgroup/memory.events这里有个经验:dmesg -T里带时间戳,可以精确对齐 OOM 发生时间;而 cgroup 的memory.events只会累计计数,没有时间戳。所以我会把系统里已有的监控记录拉出来,看 OOM 前后是 cgroup 用量先爆,还是全局指标先变差。
那次排查中,dmesg显示被杀的是容器里的主进程,而/sys/fs/cgroup/memory.events里oom_kill为 1,memory.current明显超过memory.max。也就是说,宿主机全局内存虽然很充裕,但容器这一层的账本先爆了。
6.2 第二步:把指标分类,找到“凶手”类型
接下来要做的是判断memory.current为什么会超过memory.max。我看memory.stat和宿主机/proc/meminfo时比较看重几个比例:
anon是不是涨了。如果匿名页涨得厉害,通常是进程堆、线程栈、JVM/Node 之类的运行时内存增长。file是不是涨了。如果 file 占大头,可能不是进程本身吃内存,而是这个容器读写文件产生的 page cache 挂在了 cgroup 头上。slab是否暴涨。slab通常归内核,如果SUnreclaim持续上升,很可能不是业务进程的锅,而是内核对象泄漏。
那次排查里,memory.stat的anon在 OOM 前一个小时内从 1.2G 快速爬到 3.8G,而memory.max只有 4G。再看进程的历史 RSS,主进程的 Java 堆内在那个时间段明显增长,和业务日志里的流量峰值完全吻合。答案已经很清楚:这是一次“业务高峰 + 容器内存上限设置过小”的双重结果,不是内存泄漏。
这里我特别想说一个常见误判:很多人一看到 OOM 就怀疑内存泄漏,其实泄漏和峰值是两回事。内存泄漏是“不访问也会持续增长”,特征是曲线单调上升、不回落;峰值是“流量上来就涨,流量下去就降”,特征是和业务曲线强相关。如果在 OOM 之后观察一段时间,发现内存在低谷期能回落,并且反复出现在流量高峰,那就不是泄漏,而是容量规划问题。
6.3 第三步:针对根因调整并验证
既然根因是“业务高峰太高 + cgroup 上限太低”,临时处置和长期治理就要分开。
临时处置:
- 把容器的
memory.max从 4G 调到 6G。但前提是宿主机的MemAvailable充足,否则只是把风险转嫁到宿主机全局。 - 同时把
memory.swap.max设为 0,避免它先用 swap 造成性能劣化后再被杀。
长期治理:
- 监控
memory.current的历史趋势,而不是只看 CPU。我给自己服务的容器都加了memory.current / memory.max比例告警,超过 80% 就开始关注。 - 对 Java 服务重点观察堆内、堆外、Metaspace、线程栈等细分,避免只看到“总的 RSS 涨了”却不知道涨在哪。
- 对频繁读写文件的容器,专门看
file和Cached是否把 cgroup 配额吃满。如果是,可以考虑在应用层加缓存控制或调整容器配额。
验证方式很简单:调大限额并重启容器后,继续观察memory.current曲线是否平滑、memory.events是否还在持续增加。如果oom_kill计数停下来了,同时宿主机全局内存又没有吃紧,这次调整就是有效的。
6.4 复盘之外:我长期保留的内存监控习惯
这几年的排查经验下来,我形成了一套比较固定的习惯,也适用于想少踩坑的你:
- 不要用
free列的free做内存告警,改用MemAvailable。 - 每个容器/服务都记录
memory.current、memory.max、memory.events、memory.stat,至少保留 7 天。 - 关注
/proc/pressure/memory的 PSI,尤其是some avg10。它能在内存回收开始影响业务前给出信号。 - 定期检查
SUnreclaim和Dirty。一个稳定增长的SUnreclaim常常是内核态内存问题的前兆。 - OOM 之后先把 dmesg、cgroup、业务日志对齐到同一条时间线,再判断是泄漏、峰值还是配额问题,不要急着加内存。
事后我复盘那个晚上,发现其实在 OOM 发生前两个小时,memory.current / memory.max已经反复触及 90%,只是当时我的监控只覆盖了 CPU 和流量,没人注意到内存配额曲线早就顶到天花板了。等真正被 OOM 杀掉,才从告警里反应过来,属实有点后知后觉。
那次之后,我把所有关键服务的内存配额使用率和 PSI 指标都加进了告警规则,给数据库和主业务进程调低了oom_score_adj,并且彻底检查了一遍 cgroup 层级的swap.max配置。如果你现在再去问那次值班的教训,我只会说一句话:内存管理的问题,绝大多数不是“内存不够”,而是“你不知道内存是怎么被算账的”。把这套账算明白,告警再响也不会慌。