本文合并整合 :系统介绍 Linux OOM 机制原理、排查方法论与 Envoy 实战案例
一、OOM Killer 原理机制
1.1 什么是 OOM Killer
当系统物理内存和交换空间都被用完时,如果进程继续申请内存,Linux 内核会触发 OOM (Out Of Memory) killer 机制来终止进程释放内存。
1.2 OOM 触发流程
graph TD
A[进程申请内存] --> B{物理内存+Swap是否耗尽?}
B -->|否| C[分配内存成功]
B -->|是| D{panic_on_oom?}
D -->|=2| E[系统 Panic]
D -->|=1| F{符合条件?}
F -->|是| E
F -->|否| G{oom_kill_allocating_task?}
G -->|=1| H[Kill 当前进程]
G -->|=0| I[计算各进程 OOM 分数]
I --> J[Kill 分数最高进程]
J --> K{oom_dump_tasks=1?}
K -->|是| L[生成 core dump]
K -->|否| M[记录日志]
E --> N[panic 秒后重启]
H --> O[内存释放]
L --> O
M --> O graph TD
A[进程申请内存] --> B{物理内存+Swap是否耗尽?}
B -->|否| C[分配内存成功]
B -->|是| D{panic_on_oom?}
D -->|=2| E[系统 Panic]
D -->|=1| F{符合条件?}
F -->|是| E
F -->|否| G{oom_kill_allocating_task?}
G -->|=1| H[Kill 当前进程]
G -->|=0| I[计算各进程 OOM 分数]
I --> J[Kill 分数最高进程]
J --> K{oom_dump_tasks=1?}
K -->|是| L[生成 core dump]
K -->|否| M[记录日志]
E --> N[panic 秒后重启]
H --> O[内存释放]
L --> O
M --> O graph TD
A[进程申请内存] --> B{物理内存+Swap是否耗尽?}
B -->|否| C[分配内存成功]
B -->|是| D{panic_on_oom?}
D -->|=2| E[系统 Panic]
D -->|=1| F{符合条件?}
F -->|是| E
F -->|否| G{oom_kill_allocating_task?}
G -->|=1| H[Kill 当前进程]
G -->|=0| I[计算各进程 OOM 分数]
I --> J[Kill 分数最高进程]
J --> K{oom_dump_tasks=1?}
K -->|是| L[生成 core dump]
K -->|否| M[记录日志]
E --> N[panic 秒后重启]
H --> O[内存释放]
L --> O
M --> O
graph TD
A[进程申请内存] --> B{物理内存+Swap是否耗尽?}
B -->|否| C[分配内存成功]
B -->|是| D{panic_on_oom?}
D -->|=2| E[系统 Panic]
D -->|=1| F{符合条件?}
F -->|是| E
F -->|否| G{oom_kill_allocating_task?}
G -->|=1| H[Kill 当前进程]
G -->|=0| I[计算各进程 OOM 分数]
I --> J[Kill 分数最高进程]
J --> K{oom_dump_tasks=1?}
K -->|是| L[生成 core dump]
K -->|否| M[记录日志]
E --> N[panic 秒后重启]
H --> O[内存释放]
L --> O
M --> O 1.3 关键配置参数
参数 默认值 说明 /proc/sys/vm/panic_on_oom0 0=不panic, 1=可能panic, 2=一定panic /proc/sys/vm/oom_kill_allocating_task0 0=kill最高分进程, 1=kill当前进程 /proc/sys/vm/oom_dump_tasks1 是否在 OOM 时 dump 任务信息 /proc/sys/kernel/panic0 panic 后多少秒重启系统(0=不重启)
1.4 OOM 分数计算
每个进程的 OOM 分数由内核自动计算,存储在 /proc/<pid>/oom_score。
影响因素 :
进程运行时间(越长越重要,分数越低) 进程已分配内存(越多分数越高) 进程 nice 值 子进程数量 CPU 时间 手动调整分数 :
通过 /proc/<pid>/oom_score_adj 调整(范围 -1000 到 1000):
1
2
3
4
5
# -1000 表示永远不会被 OOM killer 杀掉
echo -1000 > /proc/<pid>/oom_score_adj
# 500 表示提高被 kill 的优先级
echo 500 > /proc/<pid>/oom_score_adj
注意 :旧版本使用 oom_adj(-17 到 15),-17 等同于现在的 -1000
1.5 查看系统配置
1
2
3
4
5
6
7
8
# 查看 OOM 配置
cat /proc/sys/vm/panic_on_oom
cat /proc/sys/vm/oom_kill_allocating_task
cat /proc/sys/vm/oom_dump_tasks
# 查看进程 OOM 分数
cat /proc/$( pidof envoy) /oom_score
cat /proc/$( pidof envoy) /oom_score_adj
1.6 修改配置(三种方式)
1
2
3
4
5
6
7
8
9
10
# 方式1:直接写文件(重启失效)
sudo sh -c "echo 0 > /proc/sys/vm/panic_on_oom"
# 方式2:sysctl 命令(重启失效)
sudo sysctl vm.panic_on_oom= 0
# 方式3:配置文件(重启生效)
sudo vim /etc/sysctl.conf
# 添加:vm.panic_on_oom=0
sudo sysctl -p # 立即生效
二、OOM 排查方法论
2.1 快速诊断流程
graph LR
A[系统异常] --> B{进程被Kill?}
B -->|是| C[dmesg 查看日志]
B -->|否| D{内存持续增长?}
C --> E[确认 OOM]
D -->|是| F[主动分析]
D -->|否| G[其他问题]
E --> H[定位被Kill进程]
F --> H
H --> I[分析内存占用]
I --> J[定位根因] graph LR
A[系统异常] --> B{进程被Kill?}
B -->|是| C[dmesg 查看日志]
B -->|否| D{内存持续增长?}
C --> E[确认 OOM]
D -->|是| F[主动分析]
D -->|否| G[其他问题]
E --> H[定位被Kill进程]
F --> H
H --> I[分析内存占用]
I --> J[定位根因] graph LR
A[系统异常] --> B{进程被Kill?}
B -->|是| C[dmesg 查看日志]
B -->|否| D{内存持续增长?}
C --> E[确认 OOM]
D -->|是| F[主动分析]
D -->|否| G[其他问题]
E --> H[定位被Kill进程]
F --> H
H --> I[分析内存占用]
I --> J[定位根因]
graph LR
A[系统异常] --> B{进程被Kill?}
B -->|是| C[dmesg 查看日志]
B -->|否| D{内存持续增长?}
C --> E[确认 OOM]
D -->|是| F[主动分析]
D -->|否| G[其他问题]
E --> H[定位被Kill进程]
F --> H
H --> I[分析内存占用]
I --> J[定位根因] 2.2 常用排查命令
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 1. 检查系统内存状态
free -h
cat /proc/meminfo
# 2. 查看 OOM 日志
dmesg | grep -i "killed process"
dmesg | grep -i "out of memory"
grep -i "oom" /var/log/messages
journalctl -k | grep -i oom
# 3. 查看进程内存占用(按内存排序)
ps aux --sort= -%mem | head -20
top -o %MEM
# 4. 查看进程详细内存信息
cat /proc/<pid>/status
pmap -x <pid>
smem -s pss
# 5. 查看系统整体内存分布
smem --pie name
slabtop
2.3 OOM 日志分析
典型的 OOM 日志示例:
1
2
3
[ 490.006836] eat_memory invoked oom-killer: gfp_mask=0x24280ca, order=0, oom_score_adj=0
[ 490.006871] [<ffffffff81191442>] oom_kill_process+0x202/0x3c0
[ 490.007123] Out of memory: Killed process 12345 (envoy) total-vm:1234567kB, anon-rss:987654kB, file-rss:12345kB, shmem-rss:0kB
关键信息 :
gfp_mask:内存分配标志oom_score_adj:进程的 OOM 分数调整值total-vm:总虚拟内存anon-rss:匿名物理内存file-rss:文件映射物理内存三、深度内存分析
3.1 进程内存布局
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
┌─────────────────────────────────────────────────────────┐
│ 进程虚拟地址空间 │
├─────────────────────────────────────────────────────────┤
│ Kernel Space (高地址) │
├─────────────────────────────────────────────────────────┤
│ Stack (栈) │
├─────────────────────────────────────────────────────────┤
│ ↓ │
│ ↓ (增长方向) │
│ ↓ │
├─────────────────────────────────────────────────────────┤
│ Memory Mapping Segment (mmap 区域) │
│ - 动态库加载 │
│ - 大内存分配 │
│ - 共享内存 │
├─────────────────────────────────────────────────────────┤
│ ↑ │
│ ↑ (增长方向) │
│ ↑ │
├─────────────────────────────────────────────────────────┤
│ Heap (堆) │
├─────────────────────────────────────────────────────────┤
│ BSS Segment (未初始化数据) │
├─────────────────────────────────────────────────────────┤
│ Data Segment (已初始化数据) │
├─────────────────────────────────────────────────────────┤
│ Text Segment (代码段) │
├─────────────────────────────────────────────────────────┤
│ (低地址) │
└─────────────────────────────────────────────────────────┘
3.2 /proc//maps 解读
输出格式:
1
2
3
4
5
地址范围 权限 偏移 设备 inode 文件路径
55c9acb52000-55c9adbd1000 r--p 00000000 103:03 11953407 /usr/local/bin/envoy
55c9adbd1000-55c9afed8000 r-xp 0107e000 103:03 11953407 /usr/local/bin/envoy
55c9b1f0d000-55c9b500e000 rw-p 00000000 00:00 0 [heap]
7f3fcf214000-7f3fd12f4000 rw-p 00000000 00:00 0 [anon]
权限说明 :
r = read, w = write, x = execute, p = private, s = shared内存类型 :
[heap]:堆内存[stack]:栈内存[anon]:匿名内存映射(malloc 大块内存)文件路径:动态库或可执行文件映射 3.3 pmap 详细分析
输出示例:
1
2
Address Kbytes RSS Dirty Mode Mapping
000055c9b1f0d000 50180 43952 43952 rw--- [ anon ]
字段说明 :
Kbytes:虚拟内存大小RSS (Resident Set Size):实际物理内存占用Dirty:脏页数量(已修改但未写回磁盘)快速定位大内存块 :
1
2
# 按排序查看最大 RSS 内存块
pmap -x <pid> | sort -nk 3 | tail -10
3.4 smem 高级分析
1
2
3
4
5
6
7
8
9
10
11
# 安装 smem
sudo apt install smem
# 按 PSS (Proportional Set Size) 排序
smem -s pss -k
# 查看用户内存占用
smem -u -k
# 查看进程内存映射详情
smem -m <pid>
内存指标说明 :
VSS (Virtual Set Size):虚拟内存大小RSS (Resident Set Size):物理内存占用PSS (Proportional Set Size):共享内存按比例分配后的实际占用USS (Unique Set Size):进程独占内存四、实战案例:Envoy OOM 排查
4.1 问题背景
Envoy 进程持续占用内存增长,最终被 OOM killer 杀死。
4.2 排查步骤
步骤1:确认进程 PID
1
2
ps -ef | grep envoy
# 1337 42947 42820 0 Jan19 ? 00:15:19 /usr/local/bin/envoy ...
步骤2:查看内存布局
1
2
# 快速定位最大内存块
pmap -x 42947 | sort -nk 3 | tail -5
输出:
1
2
3
4
5
Address Kbytes RSS Dirty Mode Mapping
000055c9b1f0d000 50180 43952 43952 rw--- [ anon ]
00007f3fcf214000 33664 33560 33560 rw--- [ anon ]
---------------- ------- ------- -------
total kB 243060 101508 82704
发现 0x55c9b1f0d000 内存块占用约 50MB,RSS 占总内存近 50%。
步骤3:确认内存块范围
1
2
# 在 maps 中查找该内存块的起止地址
cat /proc/42947/maps | grep 55c9b1f0d000
输出:
1
55c9b1f0d000-55c9b500e000 rw-p 00000000 00:00 0 [heap]
内存块范围:0x55c9b1f0d000 到 0x55c9b500e000
步骤4:Dump 内存块内容
1
gdb --batch --pid 42947 -ex "dump memory stack.dump 0x55c9b1f0d000 0x55c9b500e000"
步骤5:分析内存内容
1
2
3
4
5
# 查看可读字符串
strings stack.dump | less
# 十六进制查看
hexdump -C stack.dump | less
步骤6:完整内存 Dump(可选)
1
2
3
4
5
6
# 生成完整 core dump
gcore 42947
# 或使用 gdb 附加分析
gdb -p 42947
( gdb) generate-core-file envoy.core
4.3 Envoy 专用分析工具
开启 heap profiler
1
2
3
4
5
6
7
8
9
10
11
12
13
# 开启 heap profiler
kubectl exec -n " $ns " " $pod " -c istio-proxy -- \
curl -X POST "http://localhost:15000/heapprofiler?enable=y"
# 等待 15 秒采集数据
sleep 15
# 关闭 profiler
kubectl exec -n " $ns " " $pod " -c istio-proxy -- \
curl -X POST "http://localhost:15000/heapprofiler?enable=n"
# 复制 profile 数据
kubectl cp -n " $ns " " $pod " :/var/lib/istio/data /tmp/envoy -c istio-proxy
获取 Envoy 配置和状态
1
2
3
4
5
6
7
8
9
10
11
# 配置 dump
kubectl exec -n " $ns " " $pod " -c istio-proxy -- \
curl http://127.0.0.1:15000/config_dump
# Cluster 信息
kubectl exec -n " $ns " " $pod " -c istio-proxy -- \
curl http://127.0.0.1:15000/clusters?format= json
# 内存统计
kubectl exec -n " $ns " " $pod " -c istio-proxy -- \
curl http://127.0.0.1:15000/memory
4.4 自动化排查脚本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
#!/bin/bash
# OOM 排查自动化脚本
POD_NAME = " $1 "
NAMESPACE = " $2 "
if [ -z " $POD_NAME " ] || [ -z " $NAMESPACE " ] ; then
echo "Usage: $0 <pod-name> <namespace>"
exit 1
fi
OUTPUT = " ${ POD_NAME } -oom-analysis.log"
echo "=== Envoy OOM Analysis: ${ POD_NAME } ===" > ${ OUTPUT }
echo "Time: $( date) " >> ${ OUTPUT }
echo "" >> ${ OUTPUT }
# 1. Envoy config dump
echo "### Config Dump ###" >> ${ OUTPUT }
kubectl exec -n ${ NAMESPACE } ${ POD_NAME } -c istio-proxy -- \
curl -s http://127.0.0.1:15000/config_dump >> ${ OUTPUT } 2>& 1
# 2. 获取容器 PID
CONTAINER_ID = $( kubectl get pod -n ${ NAMESPACE } ${ POD_NAME } \
-o jsonpath = '{.status.containerStatuses[0].containerID}' | awk -F '//' '{print $NF}' )
if [ -z " $CONTAINER_ID " ] ; then
echo "Error: Cannot get container ID" >> ${ OUTPUT }
exit 1
fi
PPID = $( docker inspect -f '{{.State.Pid}}' ${ CONTAINER_ID } )
PID = $( ps -ef | grep ${ PPID } | grep envoy | awk '{print $2}' )
echo "" >> ${ OUTPUT }
echo "### Process Info ###" >> ${ OUTPUT }
echo "Container ID: ${ CONTAINER_ID } " >> ${ OUTPUT }
echo "Pilot-agent PID: ${ PPID } " >> ${ OUTPUT }
echo "Envoy PID: ${ PID } " >> ${ OUTPUT }
# 3. 内存映射
echo "" >> ${ OUTPUT }
echo "### /proc/ ${ PID } /maps ###" >> ${ OUTPUT }
cat /proc/${ PID } /maps >> ${ OUTPUT } 2>& 1
# 4. pmap
echo "" >> ${ OUTPUT }
echo "### pmap -x ${ PID } ###" >> ${ OUTPUT }
pmap -x ${ PID } >> ${ OUTPUT } 2>& 1
# 5. 最大内存块 dump
echo "" >> ${ OUTPUT }
echo "### Dumping largest memory block ###" >> ${ OUTPUT }
STACK_START = $( pmap -x ${ PID } | sort -nk 3 | grep -v total | tail -1 | awk '{print $1}' )
STACK_END = $( cat /proc/${ PID } /maps | grep ${ STACK_START } | awk -F '-' '{print $2}' )
echo "Dumping 0x ${ STACK_START } - 0x ${ STACK_END } " >> ${ OUTPUT }
gdb --batch -p ${ PID } \
-ex "dump memory ${ POD_NAME } -memory.dump 0x ${ STACK_START } 0x ${ STACK_END } " \
>> ${ OUTPUT } 2>& 1
# 6. 分析 dump
echo "" >> ${ OUTPUT }
echo "### Memory content preview ###" >> ${ OUTPUT }
strings ${ POD_NAME } -memory.dump | head -100 >> ${ OUTPUT }
echo "" >> ${ OUTPUT }
echo "=== Analysis Complete ===" >> ${ OUTPUT }
echo "Output: ${ OUTPUT } "
echo "Memory dump: ${ POD_NAME } -memory.dump"
使用方法:
1
2
chmod +x oom-analysis.sh
./oom-analysis.sh my-pod-xxx my-namespace
五、内存泄漏分析进阶
5.1 使用 gdb 分析 C++ 对象
对于 C++ 程序(如 Envoy),可以统计内存中的对象类型:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 获取最大内存块地址
STACK_START = $( pmap -x ${ PID } | sort -nk 3 | grep -v total | tail -1 | awk '{print $1}' )
# 使用 gdb 输出内存内容
gdb -c core.${ PID } ${ BINARY }
( gdb) set height 0
( gdb) set logging file gdb.txt
( gdb) set logging on
( gdb) x/612205568a 0x${ STACK_START }
( gdb) set logging off
# 解析 C++ 符号
cat gdb.txt | c++filt | \
awk 'BEGIN{stat[""]=0}{stat[$5]++}END{for(i in stat) print i" "stat[i]}' | \
sort -nk 2
5.2 使用 Valgrind
1
2
3
4
5
6
7
8
9
# 内存泄漏检测
valgrind --leak-check= full --show-leak-kinds= all \
--log-file= valgrind.log ./your_program
# Massif 堆分析
valgrind --tool= massif --massif-out-file= massif.out ./your_program
# 分析结果
ms_print massif.out
5.3 使用 AddressSanitizer
编译时添加 ASan:
1
2
gcc -fsanitize= address -g your_program.c -o your_program
./your_program
六、容器环境 OOM
6.1 Kubernetes 中的 OOM
两类 OOM :
系统级 OOM :节点内存耗尽,kubelet 触发容器级 OOM :超过容器 memory limit,内核直接 kill6.2 Kubelet OOM 机制
1
2
3
4
5
6
# Pod 资源限制
resources :
limits :
memory : "512Mi"
requests :
memory : "256Mi"
当容器超过 limit 时:
1
Memory cgroup out of memory: Kill process 12345 (java) score 900 or sacrifice child
6.3 容器 OOM 排查
1
2
3
4
5
6
7
8
9
# 查看 Pod 事件
kubectl describe pod <pod-name>
# 查看容器日志
kubectl logs <pod-name> --previous
# 进入容器查看
kubectl exec -it <pod-name> -- cat /sys/fs/cgroup/memory/memory.limit_in_bytes
kubectl exec -it <pod-name> -- cat /sys/fs/cgroup/memory/memory.usage_in_bytes
6.4 cgroup v2 OOM
1
2
3
4
5
6
7
# cgroup v2 路径
cat /sys/fs/cgroup/<cgroup-path>/memory.max
cat /sys/fs/cgroup/<cgroup-path>/memory.current
cat /sys/fs/cgroup/<cgroup-path>/memory.oom.group
# 查看 OOM 事件
cat /sys/fs/cgroup/<cgroup-path>/memory.events
七、预防措施与最佳实践
7.1 监控指标
指标 告警阈值 说明 节点内存使用率 >85% 预警 容器内存使用率 >limit*0.9 即将 OOM OOM Kill 事件 >0 已发生 OOM 内存持续增长 10%/5min 可能泄漏
7.2 Prometheus 监控规则
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 节点内存告警
- alert : NodeMemoryHigh
expr : (1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes) > 0.85
for : 5m
annotations :
summary : "Node {{ $labels.instance }} memory usage > 85%"
# 容器即将 OOM
- alert : ContainerMemoryNearLimit
expr : container_memory_usage_bytes{container!="",container!="POD"} > container_spec_memory_limit_bytes * 0.9
for : 2m
annotations :
summary : "Container {{ $labels.container }} near OOM"
# OOM Kill 检测
- alert : OOMKilled
expr : increase(kube_pod_container_status_terminated_reason{reason="OOMKilled"}[5m]) > 0
annotations :
summary : "Pod {{ $labels.pod }} was OOM killed"
7.3 资源配置建议
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# Java 应用
resources :
limits :
memory : "2Gi"
requests :
memory : "1Gi"
env :
- name : JAVA_OPTS
value : "-Xms1g -Xmx1536m -XX:MaxRAMPercentage=75"
# Go 应用
resources :
limits :
memory : "512Mi"
requests :
memory : "256Mi"
# Go 运行时会自动根据 cgroup limit 限制内存
# Node.js 应用
resources :
limits :
memory : "512Mi"
requests :
memory : "256Mi"
env :
- name : NODE_OPTIONS
value : "--max-old-space-size=384"
7.4 系统调优
1
2
3
4
5
6
7
8
# /etc/sysctl.conf
vm.overcommit_memory = 0 # 严格 overcommit 检查
vm.overcommit_ratio = 50 # overcommit 比例
vm.panic_on_oom = 0 # 不 panic
vm.swappiness = 10 # 降低 swap 使用倾向
# 应用配置
sudo sysctl -p
7.5 常见陷阱
陷阱 说明 解决方案 Java 堆外内存 堆外内存不受 -Xmx 限制 设置 -XX:MaxDirectMemorySize Go goroutine 泄漏 每个 goroutine 占用 2KB 栈 使用 pprof 检测 内存碎片化 长时间运行导致内存碎片 定期重启或使用 jemalloc Shared libraries 多进程共享库重复计算 正确理解 PSS Page Cache 文件缓存占用大量内存 区分 cache/dirty 内存
八、快速参考
常用命令速查
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# === 系统级检查 ===
free -h # 内存概览
cat /proc/meminfo | grep -E "MemTotal|MemFree|Cached|Swap" # 详细内存
vmstat 1 10 # 内存/swap 统计
# === OOM 日志 ===
dmesg | grep -i "killed process" # OOM kill 记录
journalctl -k -b -1 | grep -i oom # 上次启动的 OOM 日志
grep "Out of memory" /var/log/syslog
# === 进程内存 ===
ps aux --sort= -%mem | head -20 # 按内存排序
smem -s pss -k # 按 PSS 排序
top -o %MEM # 实时监控
# === 进程详情 ===
cat /proc/<pid>/status # 进程状态(含VmRSS/VmSize)
pmap -x <pid> # 内存映射详情
cat /proc/<pid>/maps # 虚拟地址空间
# === 内存分析 ===
gcore <pid> # 生成 core dump
gdb --batch --pid <pid> -ex "dump memory mem.dump 0xSTART 0xEND"
strings mem.dump | head # 查看内存内容
# === Envoy 专用 ===
curl http://localhost:15000/memory # Envoy 内存统计
curl http://localhost:15000/heapprofiler?enable= y # 开启 heap profiler
/proc//status 关键字段
1
2
3
4
5
6
7
8
9
10
Name: 进程名
Pid: PID
VmPeak: 峰值虚拟内存
VmSize: 当前虚拟内存
VmRSS: 物理内存占用
VmData: 数据段大小
VmStk: 栈大小
VmExe: 代码段大小
VmLib: 动态库大小
Threads: 线程数
参考资源