商城首页欢迎来到中国正版软件门户

您的位置:首页 >Linux内存管理实战:OOMKiller触发机制与内存泄漏定位

Linux内存管理实战:OOMKiller触发机制与内存泄漏定位

  发布于2026-08-13 阅读(0)

扫一扫,手机访问

线上服务跑着跑着突然没了,dmesg 里再补上一句 Out of memory: Killed process xxxx,这种场面,运维和后端开发基本都不陌生。不过,OOM Killer 并不是“看谁不顺眼就杀谁”,它背后有一套明确的判定逻辑;内存泄漏也不是凭空发生、毫无线索,从 /proc/meminfo 到 eBPF,整条排查工具链其实是完整的。接下来这篇内容,会把内存管理的排查路径从内核机制一路讲到工程实践,尽量讲透、讲明白。

Linux内存管理实战:OOMKiller触发机制与内存泄漏定位

一、OOM Killer 的选择逻辑

OOM Killer 不是随机杀进程,而是通过 oom_score 给每个进程打分,分数最高的被杀。理解打分逻辑,才能预判哪个进程会被杀、怎么保护关键进程。

1.1 oom_score 的计算

# 查看进程的 oom_score
cat /proc//oom_score

# 查看 oom_score_adj(可手动调整,范围 -1000 到 1000)
cat /proc//oom_score_adj

oom_score 的主要构成:

因素权重说明RSS(常驻内存)高进程实际占用的物理内存页页表占用中Page table 占用的内存swap 占用中swap 中的页也会计入子进程内存中子进程的内存部分计入父进程oom_score_adj线性偏移-1000 表示永不被杀,1000 表示优先杀

核心公式简化理解:

oom_score ≈ (RSS + swap + 页表) 归一化到 0-1000
oom_score += oom_score_adj

实际内核实现比这复杂(涉及 oom_badness 函数),但工程上这样理解足够。

1.2 保护关键进程

# 保护 SSH 守护进程(防止 OOM 后无法登录)
echo -1000 > /proc/$(pidof sshd)/oom_score_adj
# 保护数据库主进程
echo -1000 > /proc/$(pidof mysqld)/oom_score_adj

持久化配置(systemd 服务):

# /etc/systemd/system/mysqld.service.d/override.conf
[Service]
OOMScoreAdjust=-1000
# 创建 override 后重载
systemctl daemon-reload
systemctl restart mysqld

注意:OOMScoreAdjust=-1000 不等于"绝对安全"——如果系统所有进程都被设为 -1000,内核仍会杀分数最高的。这个值是相对偏移,不是绝对保护。

二、三种 OOM 场景区分

线上 OOM 不只有一种,混淆场景会导致排查方向完全错误。

2.1 系统级 OOM

整个物理内存+swap 耗尽,内核触发全局 OOM Killer。

# 确认系统级 OOM
dmesg -T | grep -i "out of memory"
# 或
journalctl -k | grep -i "oom"

典型输出:

[2026-07-31 10:23:45] Out of memory: Killed process 5678 (ja va) total-vm:8G, anon-rss:6G, file-rss:200M

2.2 Cgroup 级 OOM

cgroup 内存限制触发,只杀该 cgroup 内的进程,不影响系统其他进程。

# 确认 cgroup OOM
dmesg -T | grep -i "memory cgroup out of memory"

典型输出:

[2026-07-31 10:24:01] memory cgroup out of memory: Killed process 9012 (python) total-vm:2G, anon-rss:1.5G

2.3 进程级 OOM(malloc 失败)

进程申请内存失败,自己崩溃。这不是内核杀的,是进程自己处理不了 ENOMEM

# 检查应用日志
grep -i "cannot allocate memory|ENOMEM|malloc failed" /var/log/app/*.log

2.4 三种场景对照表

维度系统级 OOMCgroup 级 OOM进程级 OOM触发条件物理+swap 耗尽cgroup 内存超限单次 malloc 失败谁来杀内核 OOM Killer内核 OOM Killer进程自己崩溃影响范围全系统仅 cgroup 内单进程日志位置dmesg / journalctldmesg / journalctl应用日志排查重点谁在吃内存cgroup 限制是否合理单次申请大小、overcommit

三、内存泄漏定位工具链

OOM 发生后的第一件事是定位"谁在吃内存"。工具链从粗到细:

3.1 系统级:/proc/meminfo

# 关键字段
grep -E "MemTotal|MemFree|MemA vailable|Buffers|Cached|SwapTotal|SwapFree|Slab|SReclaimable|SUnreclaim" /proc/meminfo

字段解读:

字段含义排查价值MemA vailable系统可用内存(含可回收缓存)判断是否真的内存不足Buffers/Cached文件缓存高是正常的,可回收SReclaimable可回收 Slabdentry/inode 缓存,可回收SUnreclaim不可回收 Slab持续增长说明内核对象泄漏SwapFree剩余 swap频繁 swap in/out 说明内存压力大

注意:MemFree 低不代表内存不足。MemA vailable 才是真实可用量。很多人看到 MemFree 只有 200MB 就慌了,实际 Cached 有 10GB 可以随时回收。

3.2 进程级:smem 和 pmap

# 安装 smem
yum install -y smem # CentOS
apt install -y smem # Ubuntu

# 按PSS排序显示前20个进程
smem -rs pss | head -20

# PSS(Proportional Set Size)比RSS更准确
# 它把共享内存按比例分摊给每个进程

RSS vs PSS vs USS 对比:

指标含义适用场景RSS进程独占+共享库全部粗略看,会高估PSS独占+共享库按比例分摊准确看进程真实占用USS仅进程独占(不含共享)判断进程自己泄漏了多少
# pmap 看进程内存映射详情
pmap -x | sort -k3 -n -r | head -20

# 输出示例:
# Address Kbytes RSS Dirty Mode Mapping
# 00007f8a12340000 524288 524288 4096 rw--- [anon]
# 00007f8a34560000 32768 32768 0 r-x-- libc-2.17.so

[anon] 段持续增长通常就是泄漏点——匿名内存是 malloc 分配的堆内存。

3.3 内核级:slabtop

# 查看内核 Slab 缓存占用
slabtop -o -s c | head -20

# 输出示例:
# Active / Total Objects (% used) : 12345678 / 15000000 (82.3%)
# Active / Total Slabs (% used) : 234567 / 234567 (100.0%)
# Active / Total Caches (% used) : 89 / 120 (74.2%)
#
# OBJS ACTIVE USE OBJ SIZE SLABS OBJ/SLAB CACHE SIZE NAME
# 5678901 5678901 100% 0.19K 135212 42 540848K dentry
# 1234567 1234567 100% 0.66K 22222 55 355552K inode_cache

dentryinode_cache 高是正常的(文件系统缓存)。如果某个不认识的 cache 持续增长,可能是内核模块泄漏。

四、eBPF 追踪内存分配

传统工具只能看"现在占了多少",eBPF 能看"谁在分配、分配了多少次"。

4.1 bpftrace 追踪 malloc

# 安装 bpftrace
yum install -y bpftrace

# 追踪 libc malloc 调用,统计每个进程的分配次数和总大小
bpftrace -e '
uprobe:/lib/x86_64-linux-gnu/libc.so.6:malloc {
@bytes[comm] = @bytes[comm] + arg0;
@calls[comm] = @calls[comm] + 1;
}
interval:s:5 { print(@bytes); print(@calls); clear(@bytes); clear(@calls); }
'

4.2 追踪内核伙伴分配器

# 追踪 __alloc_pages,统计每个进程的页分配
bpftrace -e '
kprobe:__alloc_pages_nodemask {
@alloc[comm] = @alloc[comm] + 1;
}
kprobe:__free_pages {
@free[comm] = @free[comm] + 1;
}
interval:s:10 {
printf("=== Page alloc/free stats ===n");
print(@alloc);
print(@free);
}
'

如果某进程 @alloc 远大于 @free,就是泄漏点。

4.3 追踪 brk 和 mmap

# 堆扩展(brk)和大块分配(mmap)分别追踪
bpftrace -e '
tracepoint:syscalls:sys_enter_brk {
@brk[comm] = @brk[comm] + 1;
}
tracepoint:syscalls:sys_enter_mmap {
@mmap[comm] = @mmap[comm] + 1;
}
interval:s:10 { print(@brk); print(@mmap); }
'
调用场景增长特征brk小块堆分配(<128KB)持续增长=小块泄漏mmap大块分配(>128KB)或文件映射持续增长=大块泄漏或映射未释放

五、Cgroup v2 内存限制

线上服务应该用 cgroup v2 限制内存,避免单服务拖垮整机。

5.1 查看和设置内存限制

# 查看 cgroup 版本
mount | grep cgroup
# cgroup2 on /sys/fs/cgroup type cgroup2 ...

# 查看 service 的 cgroup 路径
systemctl show mysqld --property=ControlGroup

# 设置内存上限(2GB)
systemctl set-property mysqld MemoryMax=2G
interval:s:10 {
printf("=== Page alloc/free stats ===n");
print(@alloc);
print(@free);
}
'

如果某进程 @alloc 远大于 @free,就是泄漏点。

4.3 追踪 brk 和 mmap

# 堆扩展(brk)和大块分配(mmap)分别追踪
bpftrace -e '
tracepoint:syscalls:sys_enter_brk {
@brk[comm] = @brk[comm] + 1;
}
tracepoint:syscalls:sys_enter_mmap {
@mmap[comm] = @mmap[comm] + 1;
}
interval:s:10 { print(@brk); print(@mmap); }
'
调用场景增长特征brk小块堆分配(<128KB)持续增长=小块泄漏mmap大块分配(>128KB)或文件映射持续增长=大块泄漏或映射未释放

五、Cgroup v2 内存限制

线上服务应该用 cgroup v2 限制内存,避免单服务拖垮整机。

5.1 查看和设置内存限制

# 查看 cgroup 版本
mount | grep cgroup
# cgroup2 on /sys/fs/cgroup type cgroup2 ...

# 查看 service 的 cgroup 路径
systemctl show mysqld --property=ControlGroup

# 设置内存上限(2GB)
systemctl set-property mysqld MemoryMax=2G
# 设置内存软限制(超过时开始回收,1.5GB)
systemctl set-property mysqld MemoryHigh=1500M

5.2 查看内存使用

# cgroup v2 内存文件
cat /sys/fs/cgroup/system.slice/mysqld.service/memory.current # 当前使用
cat /sys/fs/cgroup/system.slice/mysqld.service/memory.max # 限制
cat /sys/fs/cgroup/system.slice/mysqld.service/memory.events # 事件计数器

memory.events 字段:

字段含义oomOOM 触发次数oom_kill实际杀掉进程的次数high超过 MemoryHigh 的次数max超过 MemoryMax 的次数
# 如果 oom_kill 持续增长,说明 cgroup 限制太低
cat /sys/fs/cgroup/system.slice/mysqld.service/memory.events
# oom 12
# oom_kill 3
# ...

5.3 告警脚本

#!/bin/bash
# /usr/local/bin/cgroup_mem_alert.sh
# 检查所有 service 的 cgroup 内存使用率

THRESHOLD=90 # 百分比

for cg_dir in /sys/fs/cgroup/system.slice/*/; do
service_name=$(basename "$cg_dir" .service)
current=$(cat "$cg_dir/memory.current" 2>/dev/null)
max=$(cat "$cg_dir/memory.max" 2>/dev/null)

[ "$max" = "max" ] && continue # 未设置限制
[ -z "$current" ] && continue
usage=$((current * 100 / max))
if [ "$usage" -gt "$THRESHOLD" ]; then
echo "[ALERT] $service_name 内存使用 ${usage}% (${current}/${max})"
fi
done
# 加入 cron,每 5 分钟检查
echo "*/5 * * * * /usr/local/bin/cgroup_mem_alert.sh >> /var/log/cgroup_mem_alert.log" | crontab -

六、三种语言的内存泄漏排查

不同语言的泄漏特征不同,排查工具也不同。

6.1 Ja va(JVM 堆泄漏)

# 查看 JVM 堆使用
jmap -heap

# 导出堆 dump
jmap -dump:format=b,file=/tmp/heap.hprof

# 用 MAT (Memory Analyzer Tool) 分析
# 或用 jhat(命令行)
jhat -port 7000 /tmp/heap.hprof

Ja va 泄漏常见模式:

模式特征排查重点集合不清理HashMap 持续增长MAT 看 Dominator TreeThreadLocal 泄漏线程池线程不销毁看 ThreadLocalMap静态集合ClassLoader 泄漏看静态字段引用链连接未关闭ResultSet/Statement看 Finalizer 队列

6.2 Python(引用计数+GC)

# 启用 tracemalloc
python3 -c "
import tracemalloc
tracemalloc.start()
# ... 业务代码 ...

snapshot = tracemalloc.take_snapshot()
top_stats = snapshot.statistics('lineno')
for stat in top_stats[:10]:
print(stat)
"

Python 泄漏常见模式:

模式特征排查重点循环引用GC 回收不了gc.get_objects()全局列表list 只 append 不 poptracemalloc 按行号闭包捕获变量被闭包持有objgraph.show_backrefsC 扩展泄漏引用计数管不到valgrind --tool=memcheck

6.3 Go(GC 语言仍有泄漏)

Go 有 GC 但仍会泄漏——goroutine 泄漏和 channel 阻塞是主要类型。

# 查看 goroutine 数量
curl http://localhost:6060/debug/pprof/goroutine?debug=1 | head -50

# 查看 heap profile
curl http://localhost:6060/debug/pprof/heap?debug=1 > heap.out
go tool pprof heap.out
# (pprof) top 10
# (pprof) list

Go 泄漏常见模式:

模式特征排查重点goroutine 泄漏goroutine 数持续增长pprof goroutinechannel 阻塞发送/接收永远阻塞pprof goroutine stackslice 底层数组大 slice 切片后未释放看 slice 引用time.Ticker 未 Stop定时器泄漏pprof goroutine

七、监控告警配置

Prometheus 告警规则:

groups:
- name: memory
rules:
- alert: HostMemoryUsageHigh
expr: (1 - node_memory_MemA vailable_bytes / node_memory_MemTotal_bytes) * 100 > 85
for: 10m
labels:
severity: warning
annotations:
summary: "内存使用率超过 85%"

- alert: HostSwapUsageHigh
expr: (1 - node_memory_SwapFree_bytes / node_memory_SwapTotal_bytes) * 100 > 50
for: 5m
labels:
severity: warning
annotations:
summary: "Swap 使用率超过 50%"

- alert: ProcessOomKilled
expr: increase(node_vmstat_oom_kill[5m]) > 0
labels:
severity: critical
annotations:
summary: "系统在 5 分钟内发生了 OOM Kill"

八、常见误区

误区一:MemFree 低就加内存

MemFree 低但 MemA vailable 高,说明内存被缓存占了,这是正常行为。加内存解决不了问题,应该看业务是否真的需要那么多内存。

误区二:OOM 杀进程就是内存泄漏

单次 OOM 可能是突发流量、批量任务、配置错误(如 JVM 堆设太大)。只有 OOM 反复发生且进程重启后又快速被杀,才是泄漏。

误区三:关闭 swap 提升性能

关闭 swap 会让 OOM 更激进——内核没有 swap 作为缓冲区,一旦物理内存满就立即杀进程。线上服务建议保留 swap(哪怕很小),作为 OOM 的缓冲。

误区四:cgroup 限制设得太紧
cgroup MemoryMax 设得太紧会导致频繁 OOM Kill,设得太松失去隔离效果。建议初始设为业务峰值 P99 的 1.5 倍,观察一周后调整。

九、排查清单

发现内存问题,按这个顺序排查:

  1. dmesg -T | grep -i oom 确认是否 OOM,区分系统级/cgroup级
  2. /proc/meminfo 看系统级内存分布
  3. smem -rs pss 定位占内存最多的进程
  4. pmap -x 看进程内存映射,找 [anon] 增长
  5. slabtop 看内核 Slab 是否异常
  6. bpftrace 追踪分配热点
  7. 语言级工具(jmap/tracemalloc/pprof)定位代码
  8. cgroup v2 限制 + 告警配置
  9. Prometheus 告警长期监控

写在最后

内存问题排查,核心就两步:先把层级定位清楚,再选择合适的工具。系统级先看 /proc/meminfo,进程级看 smem/pmap,内核级看 slabtop,代码级就交给语言 profiler。cgroup v2 是线上服务做内存隔离的基础设施,每条服务都应该配上。至于 OOM Killer,它的行为其实是可预测、可保护的,千万别把它理解成随机事件。

本文转载于:https://segmentfault.com/a/1190000048101203 如有侵犯,请联系zhengruancom@outlook.com删除。
免责声明:正软商城发布此文仅为传递信息,不代表正软商城认同其观点或证实其描述。

热门关注