OOM 与内存泄漏的定位:从 dmesg 到真凶

一、现象与影响面:进程无声消失

OOM(Out of Memory,内存耗尽)在 Linux 上几乎从不敲门。典型症状挑几个:服务跑着跑着突然 502、过一会儿又好了;容器状态变成 Exited (137);某个进程无声消失,日志戛然而止;整台机器卡死几分钟又缓过来——缓过来的那一刻,总有一个进程不见了。共同点是没有应用层报错,因为杀死它的是内核,被杀者没有机会说遗言。

反模式一:发现容器死了就重启,接着干别的。重启掩盖现场,OOM 是慢性病还是急性伤、凶手是泄漏还是配置,重启之后一个都回答不了。正确姿势是先取证再恢复:现场全在内核日志和容器状态里,两分钟就能看完。

这套排查走法在本站这台 1.8G 小机上属于常备工具。小机的经验恰恰最有迁移价值:内存越紧张的环境,OOM 排查越是要快而准。

排查主线三层:内核层(确认是 OOM、谁被杀)→ cgroup 层(是限额杀的还是物理内存杀的)→ 进程层(泄漏还是配置)。一层看正常就往下走。

二、第一层:内核日志,确认”是 OOM”

第一站在宿主机。内核的 OOM killer 每次动手都会留记录:

# 宿主机上;-T 把时间戳转成人类可读
dmesg -T | grep -iE 'out of memory|oom|killed process'
1. [某日 14:02:11] Out of memory: Killed process 4837 (node) total-vm:2900000kB ...
1.   anon-rss:1380000kB ...    ← rss 是实占物理内存,这个数才是内核杀它的依据

1. dmesg 是环形缓冲区,会被新消息冲掉;查历史用 journalctl
journalctl -k --since "-6 hours" | grep -i oom      # 本次开机内的内核日志
journalctl -k -b -1 | grep -i oom                   # 上一次开机的(机器被 OOM 拖重启过就用它)

1. 顺手看当前水位,判断是"还在流血"还是"已经止住"
free -h
1. available 列才是还能真分出去的内存,看它而不是看 free 列

日志里认准两类行:Killed process 是宿主级全局 OOM,内核按 oom_score 挑了牺牲者;带 Memory cgroup out of memory 字样的是 cgroup 限额 OOM,死的是某个容器/服务组内的进程。这一眼决定了下一层往哪走。

这层查无记录,不代表没发生过——cgroup OOM 在部分运行时配置下只在容器事件里留痕。往下走,用容器状态补证。

三、第二层:cgroup,确认”谁被杀、被谁杀”

容器世界里 OOM 分两种,处理方向完全不同,必须先分清:

# 宿主机上;137 = 128 + 9(SIGKILL),OOM 杀出来的退出码
docker ps -a --filter "status=exited" --format '{{.Names}}  {{.Status}}'
1. redis  Exited (137) 3 minutes ago     ← 高度可疑

1. 铁证:OOMKilled 标记是 docker 替你从 cgroup 事件里读好的
docker inspect redis --format '{{.Name}} OOMKilled={{.State.OOMKilled}} Code={{.State.ExitCode}}'
1. /redis OOMKilled=true Code=137        ← 实锤:限额 OOM

1. 全栈内存水位一眼看齐
docker stats --no-stream
1. NAME     MEM USAGE / LIMIT   MEM %
1. redis    199.6MiB / 200MiB   99.80%  ← 贴着限额顶,就是它
维度 宿主全局 OOM cgroup 限额 OOM
触发条件 物理内存 + swap 全线耗尽 单个容器吃破自己的 --memory 限额
日志特征 dmesg/journalctl 有 Killed process,无 memcg 字样 OOMKilled=true;journalctl 里带 Memory cgroup out of memory
受害者 内核挑 rss 最大的进程,无辜邻居常遭殃 只死限额内的进程,别人无恙
处理方向 找泄漏大户或砍服务,实在不行加内存 调限额,或调该进程的内存配置(见第五节根因表)

反模式二:一见 OOMKill=true 就加 --memory。限额是护栏,不是病因。MySQL 把 buffer pool 配成了限额的九成、Java 老镜像按宿主内存定堆、Node 没限 V8 堆——这些加多少限额都会再顶破。先去第五节对号入座,再谈动限额。

这层结论出来(谁被杀、属于哪种 OOM),往下走最后一步:看进程内存的形态。

四、第三层:进程级,”泄漏还是配置”

这层在宿主机看最全——容器进程本质上就是宿主机进程,docker exec 进容器再看反而只能看到 PID namespace 内的一小截:

# 宿主机上;按实占物理内存(RSS)排序,前几名就是嫌疑人
ps aux --sort=-rss | head -8

1. top 里按 M 键同理;连续观察形态用 pidstat 每 5 秒采一次
pidstat -r 5 12 | awk '$NF ~ /node|java|mysqld|redis/ || NR<4'
1. minflt/s 持续为正且 RSS 一路爬升 = 还在持续分配,没有回落迹象

1. 需要看容器内部视角时(比如确认容器里就一个主进程):
docker exec app ps aux --sort=-rss | head
1. 注意:容器内的 ps 只显示同 namespace 的进程,不是全貌

RSS 与 VSZ 的区别要分清:VSZ 是虚拟地址空间(预留),RSS 是实占物理内存——OOM 只关心 RSS。Java 进程 VSZ 动辄几十 G 属正常,别被吓到。看曲线形态,比看瞬时值有用得多:

曲线形态 含义 下一步
平稳高位(长期贴着限额 80–90%) 配置问题:内存池/堆给了那么大,它就真的用那么大 去根因表调配置,这不是泄漏
锯齿上涨、每次回落点比上一次高 泄漏嫌疑:GC 在扫,但总有一批对象释放不掉 保留趋势采样,转代码层(全局缓存、定时器、监听器)
平时正常、定点尖峰 峰值预算问题:fork 写时复制、批量任务、大文件加载 把尖峰时刻和 crontab / RDB 保存点对齐

五、根因清单:容器世界的高频五类

按小内存机器上的出现频率排序,每条给一行验证:

根因 特征 验证 修法
Node 未显式限堆 node 进程贴限额,日志无异常;较新的 Node 能读容器限额定默认堆,旧版本按宿主内存算,且堆外(Buffer、线程栈)不在 V8 堆统计里 node -e "console.log(v8.getHeapStatistics().heap_size_limit)"(应用容器内,对比预期) node --max-old-space-size=384 显式给出,并给堆外留量
MySQL buffer pool 配置失当 mysqld 平稳吃掉限额大头;常见错不是默认 128M,而是小机上配太大不留余量 SHOW VARIABLES LIKE 'innodb_buffer_pool_size';(mysql 容器内) 池设为容器限额一半上下,给连接与临时表留 20–30%
Java 堆与限额脱节 稳定吃满后被 OOMKill;老基础镜像按宿主内存定堆,8u191 起才默认感知容器 启动日志里的 heap 大小,或 -XX:+PrintFlagsFinal -version | grep MaxHeapSize -Xmx 小于容器限额,或显式 -XX:MaxRAMPercentage
redis fork 写时复制 BGSAVE 瞬间 RSS 接近翻倍、定点尖峰被杀 尖峰时刻对照 INFO persistence 的 bgsave 记录(redis 容器内) 容器限额大于 maxmemory、给数据量留约一倍峰值余量
真·泄漏(无上限缓存、未清理定时器、对象堆积) 锯齿上涨不回落,重启后缓慢复现 趋势采样 + 二分注释可疑模块 代码修:缓存必须有上限或 TTL

六、预防:限额、swap、告警

措施 做什么 边界与代价
全员限额 compose 里每个服务 mem_limit;redis 另设 maxmemory 限额不是免死金牌,配置错的进程照样在限额内撞墙
小 swap 当缓冲 给宿主机 1–2G swap,用卡顿换不被杀 swap 会让问题从”死”变成”慢”,定位窗口变长但体验变差;容器侧可配 memory-swap 管住
告警 监控 MemAvailable 与 cgroup 的 oom_kill 计数;最朴素的版本是 cron 里巡检 docker inspect 各容器的 OOMKilled 告警的价值在第二次 OOM 前叫醒你,第一次永远靠人肉发现

这套走法收拢成一句:先问内核(是不是 OOM、杀的谁),再问 cgroup(谁的责任田),最后问进程(泄漏还是配置)——顺序反了,就会在应用日志里翻半天什么也翻不到。

延展阅读:分不清是内存打满还是 CPU 打满时,先看《CPU 100% 的定位路径》第一节做分诊;动手前的命令基本功见《Linux 常用命令》,容器化部署的容量背景见《多节点集群架构规划》。

版本核实(以官方文档为准):Node 12 起可读容器内存限额定默认堆、Node 20+ 默认堆约为容器限额一半(4G 内封顶,超出上限 2G);JDK 8u191 起 UseContainerSupport 默认开启、MaxRAMPercentage 默认 25%。以上行为随版本演进,以 Node 与 OpenJDK 官方文档为准。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇