news 2026/10/14 4:19:38

Linux内存管理实战:内存充足却OOM Kill的排查与处置

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Linux内存管理实战:内存充足却OOM Kill的排查与处置

上周四晚上十点多,我被值班告警吵醒。某后端服务的容器被 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块设备/原始磁盘缓存一般不大,影响有限
Cachedpage cache + tmpfs 等共享内存大部分可回收,但要排除 Shmem
Shmemtmpfs、共享内存段计入 Cached 但不可作为普通缓存随便回收
Slab内核对象使用内存分 SReclaimable 和 SUnreclaim 两半
SReclaimableslab 中可回收部分内存紧张时可回收
SUnreclaimslab 中不可回收部分无法通过 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、不显著影响性能的前提下,还能给新进程分配多少内存”的数值。

所以判断内存紧张程度时,我的习惯是:

  1. 先看MemAvailable。如果长期低于总内存的 10%,说明内存压力已经很大。
  2. 再看Dirty。如果Dirty很大且持续不降,说明落盘速度跟不上写入速度,内存可能堵在写回路径上。
  3. 再看SUnreclaim。这一块不可回收,如果它异常增长,通常意味着内核对象泄漏,比如某些驱动、网络连接或文件系统元数据没有正确释放。
  4. 最后配合/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时比较看重几个比例:

  1. anon是不是涨了。如果匿名页涨得厉害,通常是进程堆、线程栈、JVM/Node 之类的运行时内存增长。
  2. file是不是涨了。如果 file 占大头,可能不是进程本身吃内存,而是这个容器读写文件产生的 page cache 挂在了 cgroup 头上。
  3. 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 复盘之外:我长期保留的内存监控习惯

这几年的排查经验下来,我形成了一套比较固定的习惯,也适用于想少踩坑的你:

  1. 不要用free列的free做内存告警,改用MemAvailable。
  2. 每个容器/服务都记录memory.current、memory.max、memory.events、memory.stat,至少保留 7 天。
  3. 关注/proc/pressure/memory的 PSI,尤其是some avg10。它能在内存回收开始影响业务前给出信号。
  4. 定期检查SUnreclaim和Dirty。一个稳定增长的SUnreclaim常常是内核态内存问题的前兆。
  5. OOM 之后先把 dmesg、cgroup、业务日志对齐到同一条时间线,再判断是泄漏、峰值还是配额问题,不要急着加内存。

事后我复盘那个晚上,发现其实在 OOM 发生前两个小时,memory.current / memory.max已经反复触及 90%,只是当时我的监控只覆盖了 CPU 和流量,没人注意到内存配额曲线早就顶到天花板了。等真正被 OOM 杀掉,才从告警里反应过来,属实有点后知后觉。

那次之后,我把所有关键服务的内存配额使用率和 PSI 指标都加进了告警规则,给数据库和主业务进程调低了oom_score_adj,并且彻底检查了一遍 cgroup 层级的swap.max配置。如果你现在再去问那次值班的教训,我只会说一句话:内存管理的问题,绝大多数不是“内存不够”,而是“你不知道内存是怎么被算账的”。把这套账算明白,告警再响也不会慌。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/14 4:19:22

VarInt变长整数编码原理与二进制协议实战解析

最近在写一个二进制协议的解析模块&#xff0c;刚开始我沿用之前做业务接口的习惯&#xff0c;遇到整数就按固定 4 字节小端来读。后来把抓到的线上数据导出来一看&#xff0c;绝大部分字段的值都在 0 到 100 之间&#xff0c;一条消息里 id、seq、type、status 全是这种小数字…

作者头像 李华
网站建设 2026/10/14 4:18:49

手写轻量级资源内联工具:把静态资源自动组装进HTML

先从我手头一个真实的项目说起吧。前段时间我在负责一个活动页的优化&#xff0c;页面不大&#xff0c;但静态资源特别多&#xff0c;十几个小图标、几段内联样式、一两个首屏脚本&#xff0c;单独发请求的话&#xff0c;移动端首屏时间很难看。当时的第一反应是引入打包工具&a…

作者头像 李华
网站建设 2026/10/14 4:18:17

【回眸】Ngram 2.0 文本分析与智能应用场景指南

在数据驱动的业务场景中&#xff0c;如何从海量文本中快速提取高价值信息&#xff0c;往往是决定决策效率的关键。很多团队在面对杂乱的搜索日志、社交媒体评论或客服对话记录时&#xff0c;常常陷入“数据很多&#xff0c;洞察很少”的困境。传统的关键词统计方法往往只能看到…

作者头像 李华
网站建设 2026/10/14 4:17:32

网络热词“rea”是如何火起来的?传播链与追词方法论

刷短视频的时候突然看到评论区里冒出一句“这也太rea了吧”&#xff0c;我愣了一下&#xff0c;以为是自己眼误。接着又在另一个社交平台的帖子下面刷到“真的惊到rea了”&#xff0c;再点开几个群聊&#xff0c;居然也有人顺手在打“rea”。一个来历不明、拼写都算不上完整的词…

作者头像 李华
网站建设 2026/10/14 4:17:25

Navicat for MySQL 实战指南:连接配置、数据操作与避坑技巧

简介&#xff1a;Navicat for MySQL 是一款专为 MySQL 与 MariaDB 设计的图形化数据库管理开发工具&#xff0c;面向数据库管理员、后端开发者及需要频繁操作数据库的运维人员。它把连接管理、SQL 编辑、数据导入导出、备份恢复、结构同步、性能监视等常用操作集中到统一界面&a…

作者头像 李华
网站建设 2026/10/14 4:16:51

WPF单实例启动全解析:Mutex互斥、事件唤醒与管道通信实战

1. 先想清楚&#xff1a;单实例启动到底要解决什么问题你有没有遇到过这种场景&#xff1a;用户手速快&#xff0c;或者电脑卡顿&#xff0c;双击了桌面图标两次&#xff0c;结果界面上弹出了两个一模一样的主窗口。如果这个应用只是看个时间倒还好&#xff0c;但如果它背后要占…

作者头像 李华