书接上文内存排查第一步:停止迷信 free。
在上一篇文章中,我们提到了检测内存问题的几种手段,如果截止此时,都没有解决内存增长问题,那么可能就会出现本文的主题--OOM。
当 OOM killer被触发时,它的行为绝非随机——这是内核最后的“保命机制”。
我们可以通过如下命令查看完整内核日志细节:
journalctl -k | grep -A 30 "Out of memory"这条命令会输出三大关键信息,相当于内核决策前的“现场取证快照”:
比如,我的本机输出如下:
Out of memory: Kill process 30449 (xxx) score 435 or sacrifice childkernel: Killed process 30449 (xxx), UID 0, total-vm:4907316kB, anon-rss:3589616kB, file-rss:788kB, shmem-rss:0kBkernel: brand_bmss_serv invoked oom-killer: gfp_mask=0x201da, order=0, oom_score_adj=0kernel: brand_bmss_serv cpuset=/ mems_allowed=0kernel: CPU: 3 PID: 14357 Comm: brand_bmss_serv Kdump: loaded Not tainted 3.10.0-1160.6.1.el7.x86_64 #1kernel: Hardware name: OpenStack Foundation OpenStack Nova, BIOS rel-1.10.2-0-g5f4c7b1-20240428_141054-szxrtosci10000 04/01/2014kernel: Call Trace:kernel: [<ffffffffae181400>] dump_stack+0x19/0x1bkernel: [<ffffffffae17bd20>] dump_header+0x90/0x229kernel: [<ffffffffc00651f8>] ? vp_set+0xb8/0xf0 [virtio_pci]kernel: [<ffffffffc02f34b0>] ? update_balloon_size.isra.8+0x40/0x60 [virtio_balloon]kernel: [<ffffffffadbc20cd>] oom_kill_process+0x2cd/0x490kernel: [<ffffffffadbc1abd>] ? oom_unkillable_task+0xcd/0x120kernel: [<ffffffffadbc27ba>] out_of_memory+0x31a/0x500kernel: [<ffffffffae17c83d>] __alloc_pages_slowpath+0x5db/0x729kernel: [<ffffffffadbc8db6>] __alloc_pages_nodemask+0x436/0x450kernel: [<ffffffffadc18a18>] alloc_pages_current+0x98/0x110kernel: [<ffffffffadbbdb87>] __page_cache_alloc+0x97/0xb0kernel: [<ffffffffadbc0b20>] filemap_fault+0x270/0x420kernel: [<ffffffffc020b756>] ext4_filemap_fault+0x36/0x50 [ext4]kernel: [<ffffffffadbede7a>] __do_fault.isra.61+0x8a/0x100kernel: [<ffffffffadbee42c>] do_read_fault.isra.63+0x4c/0x1b0kernel: [<ffffffffadbf5c70>] handle_mm_fault+0xa20/0xfb0kernel: [<ffffffffae18f653>] __do_page_fault+0x213/0x500kernel: [<ffffffffae18fa26>] trace_do_page_fault+0x56/0x150kernel: [<ffffffffae18efa2>] do_async_page_fault+0x22/0xf0kernel: [<ffffffffae18b7a8>] async_page_fault+0x28/0x30kernel: Mem-Info:kernel: active_anon:1788752 inactive_anon:45648 isolated_anon:0 active_file:4635 inactive_file:4633 isolated_file:0 unevictable:0 dirty:932 writeback:135 unstable:0kernel: Out of memory: Kill process 30450 (xxx) score 436 or sacrifice childkernel: Killed process 30450 (xxx), UID 0, total-vm:4907316kB, anon-rss:3592852kB, file-rss:1096kB, shmem-rss:0kB可能有人会问我如何知道哪个进程可能会产生OOM?
我们可以使用如下命令:
for pid in /proc/[0-9]*; do score=$(cat $pid/oom_score 2>/dev/null) name=$(cat $pid/comm 2>/dev/null) echo "$score $name $pid"done | sort -rn | head -15内核会给每个进程分配一个分数oom_score。分数越高,OOM的概率越大。
输出如下:
254 xxx /proc/28758202 yyy /proc/2038713 xxx /proc/493212 java /proc/72612 aaa /proc/48308 gunicorn /proc/180028 gunicorn /proc/180018 gunicorn /proc/180008 gunicorn /proc/17999也就是说上面进程ID 28758这个进程产生OOM的风险最大。
对于某些核心进程,如果我们不想其被OOM Killer掉,我们可以通过 oom_score_adj 来影响内核的决策,保护核心进程,或指定优先被杀死的非核心进程。
# 保护核心进程,使其永远不会被 OOM killer 杀死(-1000 = 永不杀死)echo -1000 > /proc/<PID>/oom_score_adj# 让非核心进程成为 OOM 优先杀死目标(+1000 = 优先杀死)echo 1000 > /proc/<PID>/oom_score_adj数值范围:
-1000——永不杀戮0- 默认+1000先杀如果保护过多进程,内核可能会被迫杀死更关键的进程(比如系统核心服务),反而加剧故障。
除了上面这种方式,我们还可以通过调整参数**vm.overcommit**以防止内存溢出 (OOM)
Linux 允许进程分配的内存超过系统实际可用的物理内存,这种行为称为“内存过度提交(memory overcommit)”。
这种机制有时能正常工作,但有时会延迟故障爆发,直到系统彻底崩溃。我们可以通过 vm.overcommit_memory 控制这种行为。
cat /proc/sys/vm/overcommit_memory有如下三种值:
我们可以将值改为2(严格模式),并设置合理的过度提交比例:
sysctl vm.overcommit_memory=2sysctl vm.overcommit_ratio=80当模式为 2 时,Linux 会强制执行一个硬性内存分配限制,计算公式如下:
CommitLimit(分配上限)=(物理内存 × 过度提交比例 / 100)+ 交换分区(Swap)举个例子,便于理解:
若系统配置为:32GB 物理内存 + 8GB 交换分区,过度提交比例设为 80,则:
可分配内存上限 ≈(32 × 0.8)+ 8 = 33.6GB
超过这个上限,新的内存分配请求会直接失败,而非延迟到最后触发 OOM 杀死进程,更便于提前排查问题。
通过以下命令查看当前内存提交情况,判断是否接近分配上限:
grep Commit /proc/meminfo比如输出:
CommitLimit: 33423360 kBCommitted_AS: 22020096 kB其中:
在严格模式(vm.overcommit_memory=2)下,如果 Committed_AS 超过 CommitLimit,后续的内存分配请求会直接失败。
不过,有时候,内存压力并非由内存泄漏或工作负载峰值导致,而是由**透明大页(Transparent Huge Pages,THP)**引起的。THP 是内核的一项特性,它使用更大的内存页(2MB 而非默认的 4KB),通过减少 TLB 未命中来提升性能。
大多数 Linux 发行版默认启用 THP,但它会给数据库(Redis、PostgreSQL、MongoDB 均建议禁用)带来难以解释的延迟峰值和内存膨胀,还可能导致难以追踪的异常内存使用模式。
cat /sys/kernel/mm/transparent_hugepage/enabled# Output:[always] madvise never其中:
对性能要求较高的系统应避免使用always。
使用下面这个命令来查看大页的使用情况:
grep -E "AnonHugePages|HugePages" /proc/meminfo输出如下:
AnonHugePages: 446464 kBHugePages_Total: 0HugePages_Free: 0HugePages_Rsvd: 0HugePages_Surp: 0其中:
接着,我们聊最后一个关键点:交互分区。
Swap(交换分区)是内存不足时系统的“安全网”。但和所有安全网一样,如果频繁依赖它,说明系统存在问题。我们的目标不是“零 Swap 使用”,而是可控、可预测的 Swap 行为。
健康的 Swap 应该是“安静且无波澜”的。如果它变得活跃且频繁读写,说明上游存在需要修复的问题:
使用下面这个命令,来查看哪些进程使用了Swap:
for pid in /proc/[0-9]*/status; do name=$(awk '/^Name:/{print $2}' $pid) swap=$(awk '/^VmSwap:/{print $2}' $pid) [ "${swap:-0}" -gt 1024 ] && echo "${swap} kB $name"done 2>/dev/null | sort -rn | head -10输出如下:
2097152 kB java 524288 kB python3 65536 kB postgres真正的线上稳定性,从来不是等 OOM 发生后再救火,而是提前看懂内核的信号、管好内存的边界、避开那些看不见的坑。
看懂日志、预判风险、守住底线、保护核心,这才是一个成熟工程师面对内存危机时最该有的底气。
愿我们的服务永远稳定运行,再无突如其来的 OOM。

如果对本文有疑问可以加笔者微信直接交流,笔者也建了C/C++相关的技术群,有兴趣的可以联系笔者加群。
2、一个编程小技巧