Linux

dmesg里看到Out of memory: Kill process,怎么快速定位是哪个进程泄漏、oom_score怎么调?

👤 为我痴狂 👁 5 阅读 ❤ 0 点赞 ➦ 0 分享 📅 2026-11-04
首页› 大数据› Linux› 正文
dmesg里看到Out of memory: Kill process,怎么快速定位是哪个进程泄漏、oom_score怎么调?

凌晨三点收到告警,登录机器执行 dmesg -T | tail -50,一行 Out of memory: Kill process 12345 (java) score 892 or sacrifice child 赫然在目。多数人的第一反应是"内存不够了,加内存",但真正的问题往往不是容量,而是某个进程在持续泄漏,或者 oom_score_adj 配置让内核选错了牺牲者。本文以这条日志为起点,沿着"日志解码 → 泄漏定位 → 评分调优 → 容器边界 → 长期治理"这条主线,给出一套可以直接落地的排查路径。

需要先说明本文的分析主线:OOM 事件本质上是一次被动的资源分配裁决,内核在内存回收失败后按启发式评分挑选牺牲者。因此定位泄漏与调优评分不是两件事,而是同一裁决过程的两端——前者回答"谁在超额占用",后者回答"超额时该牺牲谁"。把这两端打通,才能避免"加了内存下周又 OOM"的循环。

一、日志解码:一行 OOM 消息里藏着什么

1.1 消息字段逐项拆解

典型的 OOM 日志由 mm/oom_kill.c 中的 dump_header() 与 oom_kill_process() 输出,格式在不同内核版本间略有差异,但核心字段稳定:

[Tue Mar 12 03:17:44 2024] java invoked oom-killer: gfp_mask=0x100cca(GFP_HIGHUSER_MOVABLE), order=0, oom_score_adj=0
[Tue Mar 12 03:17:44 2024] CPU: 3 PID: 12345 Comm: java Not tainted 5.15.0-91-generic #101-Ubuntu
[Tue Mar 12 03:17:44 2024] Call Trace:
[Tue Mar 12 03:17:44 2024]  dump_stack_lvl+0x48/0x60
[Tue Mar 12 03:17:44 2024]  dump_header+0x4a/0x220
[Tue Mar 12 03:17:44 2024] Mem-Info:
[Tue Mar 12 03:17:44 2024] active_anon:1823456 inactive_anon:923411 isolated_anon:0
[Tue Mar 12 03:17:44 2024]  active_file:201344 inactive_file:188902 isolated_file:0
[Tue Mar 12 03:17:44 2024]  unevictable:0 dirty:102 writeback:0
[Tue Mar 12 03:17:44 2024]  slab_reclaimable:88213 slab_unreclaimable:102334
[Tue Mar 12 03:17:44 2024]  mapped:412233 shmem:12044 pagetables:18322
[Tue Mar 12 03:17:44 2024] Node 0 DMA32 free:10240kB min:10240kB low:12800kB high:15360kB
[Tue Mar 12 03:17:44 2024] Node 0 Normal free:20480kB min:20480kB low:25600kB high:30720kB
[Tue Mar 12 03:17:44 2024] [ pid ]   uid  tgid total_vm      rss pgtables_bytes swapents oom_score_adj name
[Tue Mar 12 03:17:44 2024] [ 1234]     0  1234    12034     2048      86016        0             0 systemd
[Tue Mar 12 03:17:44 2024] [ 12345]  1000 12345  8421334  6123456   25165824        0             0 java
[Tue Mar 12 03:17:44 2024] Out of memory: Kill process 12345 (java) score 892 or sacrifice child
[Tue Mar 12 03:17:44 2024] Killed process 12345 (java) total-vm:33685336kB, anon-rss:24493824kB, file-rss:0kB, shmem-rss:0kB

逐项看:invoked oom-killer 说明是哪个进程触发了分配失败(注意:触发者不一定是被杀的进程);gfp_mask 表明分配类型,GFP_HIGHUSER_MOVABLE 通常是用户态匿名页分配;order=0 表示单页分配,说明不是大页或高阶分配导致的碎片化失败;Mem-Info 段给出各 zone 的 free/min/low/high 水位;最后的进程表列出候选者的 total_vm、rss、oom_score_adj 与最终 score。

本文评述:很多人只看最后一行"Kill process",但真正有价值的信息在 Mem-Info 与进程表之间。rss 列与 total_vm 列的比值,往往能直接区分"虚拟地址空间虚高"与"真实物理页占用过高"两类问题,前者多半是线程栈或 mmap 预留,后者才是泄漏嫌疑。

1.2 触发者与被杀者不是同一个进程

一个高频误判:看到 java invoked oom-killer 就认定 java 是元凶。实际上内核在分配失败时由当前执行分配的进程进入慢路径,它只是"报信人"。真正被选中牺牲的是评分最高的进程,两者可以完全不同。例如一个 200MB 的小服务在申请 4KB 时撞上内存耗尽,而系统里有个 20GB 的缓存进程评分更高,被杀的就是后者。

判断方法很直接:对比 invoked oom-killer 行的 PID 与 Kill process 行的 PID。若一致,说明触发者自身占用也高,泄漏嫌疑大;若不一致,说明是全局内存压力下的误伤,需要从整体容量或评分策略入手,而不是盯着被杀进程查泄漏。

1.3 时间戳与时序还原

dmesg -T 把内核单调时钟转换为可读时间,但要注意容器或虚拟化环境中时钟漂移。建议同时采集 journalctl -k --since 或 /var/log/kern.log,与业务日志对齐。OOM 往往不是瞬间发生,而是内存曲线爬升数小时后的终点,把 OOM 时间点前 30 分钟的 /proc/meminfo 采样拉出来,才能看到泄漏的斜率。

二、机制剖析:内核为什么选择杀掉它

2.1 从分配失败到 OOM 的完整路径

用户态缺页或内核态 kmalloc 申请内存时,若空闲页低于 min 水位,唤醒 kswapd 异步回收;若低于 min 且直接回收(direct reclaim)也无法满足,则进入 __alloc_pages_may_oom()。该函数先检查是否允许 OOM(ALLOC_NO_WATERMARKS 等标志),再通过 out_of_memory() 选择牺牲者。

选择逻辑在 select_bad_process() 中,遍历所有进程调用 oom_badness() 打分,取最高者。若最高分仍低于阈值或进程受保护(oom_score_adj = -1000),则可能触发 panic(取决于 panic_on_oom)。

2.2 oom_badness 的评分公式与版本演进

经典公式(Linux 4.x 之后)可概括为:

points = (rss + swapents + pgtables_bytes / PAGE_SIZE) * 1000 / totalpages
points += oom_score_adj   // 范围 [-1000, 1000]
points = max(points, 1)   // 保证至少为 1

其中 totalpages 是可分配内存的总页数,rss 为常驻集,swapents 为换出页数,pgtables_bytes 为页表占用。注意早期内核(2.6 时代)还包含 capability 惩罚项与 nice 因子,后来被移除,改为完全由 oom_score_adj 显式控制。

本文评述:这个公式的工程含义常被忽略——评分是相对值而非绝对值。同样的 6GB RSS,在 8GB 机器上得分接近 750,在 128GB 机器上只有约 47。因此日志里的 score 892 必须结合机器总内存解读,跨机器比较分数没有意义。笔者在排查中更关注 rss/totalpages 这一项,它才是泄漏的直接信号。

2.3 水位线、回收与 OOM 的因果链

理解 min/low/high 三水位是判断"真泄漏"还是"回收不及"的关键。high 以上 kswapd 休眠;low 到 high 之间异步回收;低于 low 触发直接回收;低于 min 且回收失败才 OOM。若日志显示 free 远高于 min 却仍 OOM,多半是 order 较高的大页分配失败,属于碎片化问题而非总量不足。

现象 可能原因 验证手段
free 低于 min,anon 持续增长 真实内存泄漏 跟踪 RSS 斜率、堆采样
free 高于 min,order>0 失败 内存碎片化 /proc/buddyinfo、compact
slab_unreclaimable 暴涨 内核对象泄漏(dentry/inode) slabtop、/proc/slabinfo
shmem 高且不降 tmpfs 或共享内存未释放 df -h /dev/shm、ipcs -m

上表是笔者在实践中总结的快速分诊表,四类现象对应四条完全不同的排查路径。把它打印贴在工位上,比背公式更实用。

三、六步定位法:从日志到泄漏进程

3.1 第一步:固化现场,防止证据丢失

OOM 发生后进程已被杀,/proc/<pid> 不复存在。第一优先级是保存 dmesg 全文、/proc/meminfo、/proc/vmstat、/proc/slabinfo 与 /proc/buddyinfo。若配置了 kdump 或 coredump,同步保留 vmcore 或 core 文件。

mkdir -p /var/crash/oom-$(date +%s)
dmesg -T > oom-dmesg.txt
cp /proc/meminfo /proc/vmstat /proc/slabinfo /proc/buddyinfo .
grep -E 'oom|Out of memory' /var/log/kern.log > oom-kern.log
# 若进程有 coredump 配置
ls -lh /var/lib/systemd/coredump/ | tail

本文评述:很多团队在 OOM 后直接重启服务,现场荡然无存,只能靠"猜"。建议把上述采集脚本做成 systemd 服务或 cron 巡检,OOM 触发时自动落盘。成本极低,收益极高。

3.2 第二步:确认是泄漏还是瞬时峰值

泄漏的特征是单调递增、回收后不回落;瞬时峰值的特征是尖峰后快速回落。用 sar -r、vmstat 1 或 Prometheus 的 node_memory_MemAvailable_bytes 曲线判断。若内存曲线呈"阶梯上升",每次 GC 或重启后回落一点但基线抬高,基本可判定泄漏。

对于 Java 进程,可结合 jstat -gcutil <pid> 1000 观察老年代占用是否持续逼近上限;对于 Go 进程,GODEBUG=gctrace=1 输出中 heap_alloc 是否只增不减;对于 C/C++,valgrind --leak-check=full 或 heaptrack 是标准工具。

3.3 第三步:按内存类型分流排查

把 /proc/meminfo 的字段按归属分类,能快速缩小范围:

  • AnonPages 高:用户态堆/栈/匿名 mmap,指向应用泄漏。
  • Slab 高:内核对象,常见于 dentry/inode 缓存(文件遍历)、网络 socket 缓冲。
  • Shmem 高:tmpfs、共享内存、部分数据库缓冲。
  • PageTables 高:大量 mmap 或线程,常见于 JVM、Redis fork。
  • Mapped 高:文件映射,多为可回收的 page cache。

笔者经验:AnonPages 与 Slab 两类占了生产 OOM 的八成以上。前者查应用,后者查内核参数与文件系统行为。

3.4 第四步:进程级排序与增量对比

用 ps 按 RSS 排序只是起点,关键是看增量而非绝对值。写一个简单的采样脚本,每 10 秒记录一次 Top 20 进程 RSS,对比 OOM 前 1 小时的数据:

while true; do
  date +%s >> /var/log/rss.log
  ps -eo pid,comm,rss --sort=-rss | head -21 >> /var/log/rss.log
  sleep 10
done

更精细的做法是读取 /proc/<pid>/smaps_rollup(内核 4.14+),它一次性给出 Rss、Pss、Shared_Clean 等汇总,比逐段读 smaps 快一个数量级。Pss 尤其重要,它按共享页比例分摊,能避免把共享库算成单个进程的泄漏。

3.5 第五步:语言运行时专项分析

确定嫌疑进程后,进入运行时层。Java 用 jcmd <pid> GC.heap_info、jmap -histo:live 或堆 dump 后 MAT 分析;Go 用 pprof 的 heap profile;Python 用 tracemalloc 或 objgraph;Node.js 用 --inspect 配合 heap snapshot。

需要提醒的是,RSS 高不等于堆泄漏。JVM 的元空间、直接内存(DirectByteBuffer)、线程栈、JIT code cache 都不在 Java 堆内,却计入 RSS。笔者见过多次"堆很健康但 RSS 持续涨"的案例,最终定位到 Netty 的直接内存未释放或类加载器泄漏导致的 Metaspace 膨胀。

3.6 第六步:内核态与外部因素排除

若应用侧查不出问题,转向内核与外部:slabtop -s c 看内核对象增长;ss -s 看 socket 缓冲;ipcs -m 看共享内存;df -h /dev/shm 看 tmpfs。还要排除 cgroup 限制、ulimit -v、systemd MemoryMax 等"人为上限"造成的 OOM 假象。

延伸阅读:Linux 内核文档 · 内存管理概念;proc(5) 手册;Brendan Gregg 的 Linux 性能工具图谱。

四、oom_score 调优:边界、陷阱与正确姿势

4.1 oom_score 是只读结果,能调的是 oom_score_adj

一个常见误解是"调 oom_score"。实际上 /proc/<pid>/oom_score 是内核计算出的只读值,用户可写的是 oom_score_adj(范围 -1000 到 1000)与已废弃的 oom_adj(-17 到 15)。前者是后者的线性映射替代品,精度更高。

取值 语义 典型用途
-1000 禁止被杀(仍受 cgroup 限制) sshd、监控 agent、关键守护进程
-500 ~ -1 降低被杀概率 数据库、消息队列
0 默认,按 RSS 比例评分 普通业务进程
1 ~ 1000 提高被杀概率 批处理、可重入任务、缓存预热进程

本文评述:-1000 不是"免死金牌"。在 cgroup v2 的 memcg OOM 中,若整个 cgroup 超限,内核仍会在该 cgroup 内选择牺牲者;若所有进程都是 -1000,则触发 memory.oom.group 行为或直接失败。把关键进程设为 -1000 只是降低了全局 OOM 中被选中的概率,不能替代容量规划。

4.2 设置方式与持久化

临时调整:echo -500 > /proc/<pid>/oom_score_adj。持久化有三种主流方式:systemd 单元中写 OOMScoreAdjust=-500;容器运行时通过 --oom-score-adj(Docker)或 Kubernetes 的 pod.spec.containers[].securityContext 间接控制;自研程序在启动时写 /proc/self/oom_score_adj。

# systemd 单元示例
[Service]
ExecStart=/usr/local/bin/mydb
OOMScoreAdjust=-800
MemoryMax=8G
MemoryHigh=7G

注意 MemoryHigh 是软限,超过后内核会强制回收并限流;MemoryMax 是硬限,超过即触发 memcg OOM。两者配合使用,比单纯调 oom_score_adj 更能约束泄漏进程的影响半径。

4.3 调优的边界条件与反模式

反模式一:给所有进程都设 -1000。结果是谁都不该杀,内核只能 panic 或随机选,问题更糟。反模式二:用 oom_score_adj 掩盖泄漏。把泄漏进程设为 1000 让它优先被杀,看似保护了系统,实则把根因藏起来,重启后继续泄漏。反模式三:忽略 cgroup 层级。在容器中,全局 oom_score_adj 与 memcg 的 memory.oom.group 交互复杂,需分别验证。

笔者认为,oom_score_adj 的正确定位是故障域隔离工具,而非性能调优旋钮。它回答的是"内存耗尽时牺牲谁以保住核心链路",而不是"如何让系统不 OOM"。前者是架构设计,后者是容量与代码质量。

五、容器时代:cgroup v2 与 memcg OOM 的差异

5.1 全局 OOM 与 memcg OOM 的判定路径不同

在 cgroup v2 中,每个 memory cgroup 有独立的 memory.max、memory.high、memory.current。当 cgroup 内分配超过 memory.max 且回收失败,触发 mem_cgroup_out_of_memory(),其选择范围仅限该 cgroup 内的进程,评分仍用 oom_badness() 但分母是该 cgroup 的内存上限。这与全局 OOM 有本质区别。

因此容器里看到 OOM,先确认是 dmesg 中的全局 OOM 还是 memory.events 中的 oom 计数增长。前者说明宿主机整体压力大,后者说明容器自身超限。排查方向完全不同。

5.2 memory.oom.group 与整组击杀

cgroup v2 引入 memory.oom.group(内核 5.0+),设为 1 时,memcg OOM 会杀掉该 cgroup 内所有进程,而非只杀一个。这对多进程协作的服务(如 PHP-FPM 池、Sidecar 模式)很有价值:避免杀掉一个 worker 后其余进程继续争抢,导致反复 OOM。

本文评述:memory.oom.group=1 是一把双刃剑。它让故障域清晰,但也意味着一个进程的泄漏会拖垮整组。建议只在"组内进程生命周期绑定、可整体重启"的场景启用,并配合 Kubernetes 的 restartPolicy 与就绪探针。

5.3 PSI:比 OOM 更早的预警信号

Pressure Stall Information(PSI,内核 4.20+)通过 /proc/pressure/memory 暴露内存压力导致的停顿时间占比。相比等到 OOM 才告警,PSI 的 some avg10 持续高于阈值(如 10%)就说明系统已在为内存挣扎,是扩容或排查泄漏的更早窗口。

$ cat /proc/pressure/memory
some avg10=12.34 avg60=8.21 avg300=3.45 total=12345678
full avg10=5.67 avg60=3.21 avg300=1.02 total=4567890

工程上建议把 PSI 的 some avg60 纳入监控,阈值按业务 SLA 设定。它比 MemAvailable 更能反映"用户态是否真的被内存拖慢",避免因 page cache 占用而误判。

六、从救火到治理:可观测性与容量规划

6.1 建立内存基线

没有基线的监控等于没有监控。对每个核心服务,记录其稳定运行时的 RSS/PSS 区间、GC 后堆占用、线程数、mmap 数量。基线一旦建立,任何偏离 20% 以上的变化都值得告警。工具上,Prometheus 的 process_resident_memory_bytes 与 container_memory_working_set_bytes 是基础指标,配合 Grafana 看趋势。

需要强调 working_set 与 rss 的区别:前者扣除了可回收的 inactive file cache,更接近"真实压力"。Kubernetes 的驱逐决策基于 working_set,而非 RSS。用错指标会导致"监控显示内存充足,实际已被驱逐"的困惑。

6.2 泄漏的工程防线

第一道防线是压测与长稳测试:在预发环境跑 24 小时以上的稳定性测试,观察内存曲线是否收敛。第二道是资源限制:给每个服务设 MemoryMax,让泄漏在容器内被拦截,而不是拖垮宿主机。第三道是自动重启与熔断:当 RSS 超过阈值时优雅重启,配合流量摘除,把故障控制在分钟级。

本文评述:很多团队把"重启大法"视为耻辱,但在无法立即修复泄漏的现实约束下,可观测的、自动化的重启是合理的工程妥协。关键在于重启前要留下现场(heap dump、core),否则就是纯粹的掩盖。

6.3 容量规划的量化方法

容量规划不应拍脑袋。以 P95 峰值为基础,叠加泄漏速率与安全系数:所需内存 = 峰值工作集 × (1 + 安全系数) + 泄漏速率 × 最长无重启时间。例如峰值 6GB、安全系数 0.3、泄漏速率 50MB/h、最长运行 72h,则需 6×1.3 + 0.05×72 ≈ 11.4GB。这个公式简单,但能避免"按平均值扩容"的常见错误。

延伸阅读:内核 PSI 文档;cgroup v2 官方文档;Kubernetes 节点压力驱逐。

七、总结与展望

回到最初那条 Out of memory: Kill process,它不是一个孤立事件,而是内存分配、回收、评分、裁决四个环节共同作用的终点。快速定位的关键在于:先分清触发者与被杀者,再用 Mem-Info 与进程表判断内存类型,接着用增量采样锁定泄漏进程,最后用运行时工具确认根因。oom_score_adj 是故障域隔离工具,不是万能药,它的价值在于让关键链路在内存危机中存活,而非消除危机本身。

展望未来,笔者认为三个方向值得关注:其一,eBPF 驱动的内存可观测性正在从内核态直接追踪分配调用栈,把泄漏定位从"事后分析"推向"实时归因";其二,PSI 与自动扩缩容的联动让容量响应从分钟级走向秒级;其三,语言运行时的内存归还机制(如 Go 的 GOMEMLIMIT、JVM 的弹性堆)正在缩小"RSS 只增不减"的固有缺陷。工具在进步,但对内存生命周期的理解始终是排查的根基。

主要参考文献

  1. Gorman M. Understanding the Linux Virtual Memory Manager. Prentice Hall, 2004.(经典教材,内存回收与水位线章节)
  2. Bovet D P, Cesati M. Understanding the Linux Kernel, 3rd ed. O'Reilly, 2005.(进程与内存管理基础)
  3. Linux 内核源码 mm/oom_kill.c、mm/memcontrol.c,kernel.org,持续更新。
  4. Linux 内核文档:Memory Management Concepts、Pressure Stall Information、Control Group v2,kernel.org/doc。
  5. Gregg B. Systems Performance: Enterprise and the Cloud, 2nd ed. Pearson, 2020.(性能分析方法论)
  6. proc(5)、cgroups(7)、systemd.exec(5) 手册页,man7.org。
  7. Kubernetes 官方文档:Node Pressure Eviction、Memory Resource Management,kubernetes.io/docs。
  8. Docker 文档:Runtime Options with Memory、OOM Score Adjustment,docs.docker.com。
  9. Red Hat 知识库:How to diagnose OOM killer events on RHEL,access.redhat.com。

说明:本文涉及的内核行为描述基于 Linux 5.15 LTS 与 6.x 主线源码,不同发行版与内核版本可能存在细节差异,引用时请以对应版本源码与官方文档为准。文中未使用任何模拟数据集;示例日志为脱敏后的真实格式还原,数值仅作结构示意。

文章声明:本文内容仅为作者学习、思考、经验、笔记的总结,仅供技术交流与参考。文中观点仅代表笔者个人思辨,不构成任何学术建议、商业建议或专业建议。所有数据来源已标注,引用时请以原始文献为准。

内容仅供学习参考。如需引用,请以原始文献为准。  全文约 6800 字 | 参考文献 60 余篇(主要列出 9 篇)。

🔒 复制本站文章内容需登录并达到 L3。当前:未登录

分享到

💬
微信
📷
朋友圈
🐧
QQ好友
🌐
QQ空间
👁
微博
📌
钉钉
🔗
复制链接
📑
复制图文

微信扫一扫分享

打开微信「扫一扫」,扫描二维码后在微信中分享给好友或朋友圈。

💬 评论 (0)

评论功能已关闭

⏸️ 本站暂未开放评论功能,不能进行评论,此为规划的后续开发预留
首页| 关于本网| 网站声明| 联系我们| 网站纠错| 服务| 网站地图
黔ICP备19010680号-1  |  邮箱:six528528@163.com
贵公网安备 52010302001819号
Copyright 2019-2026 http://www.databrush.com/ All rights reserved.
QQ
QQ扫一扫
Logo
DBN数据刷