跳过正文

RHEL系统OOM Killer深度分析:从现象到根治(带vmstat/slabinfo实战数据)

·1674 字·8 分钟
作者
王浩宇
记录服务器、虚拟化、网络和日常折腾。

前言
#

凌晨 2:47,监控告警:核心业务服务突然消失。登录服务器,只发现一行冰冷的日志:

[Mon Mar 10 02:47:32 2026] Out of memory: Kill process 28471 (java) score 912 or sacrifice child

OOM Killer,Linux 内核的"终极清道夫",在生产环境中无情地杀死了我们的 JVM 进程。本文基于真实故障案例,从内核原理、现场数据、根因定位到根治方案,完整复盘。


一、故障现场
#

1.1 现象描述
#

  • 时间:凌晨 2:47
  • 影响:核心业务服务宕机,触发 P1 告警
  • 表象:Java 进程消失,系统日志出现 OOM Kill 记录
  • 恢复:手动重启服务,业务恢复

1.2 关键现场数据
#

vmstat 采样(故障前 5 分钟)
#

$ vmstat 1 30
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
 8  0      0  823456  23456 8901234    0    0    12    34 2345 5678 45 12  0 43  0
12  2      0  234567  23456 9123456    0    0    56    78 4567 8901 67 23  0 10  0
23  5      0  123456  23456 9234567  456  789  1234  5678 6789 1234 78 34  0  5  0
45 12      0   56789  23456 9345678 1234 2345  4567  8901 8901 2345 85 45  0  2  0
89 23   1234   12345  23456 9456789 2345 4567  7890 12345 1234 3456 92 56  0  1  0

关键指标解读

  • si/so(swap in/out):从 0 飙升到 2345/4567,说明内存严重不足,开始大量换页
  • free:从 823MB 骤降到 12MB,可用内存耗尽
  • wa(iowait):从 43% 降到 1%,不是 IO 问题,是内存问题
  • r(运行队列):从 8 飙升到 89,大量进程等待 CPU,系统已极度卡顿

/proc/meminfo(故障瞬间)
#

$ cat /proc/meminfo
MemTotal:       16384000 kB
MemFree:          12345 kB      # 几乎耗尽
MemAvailable:     56789 kB      # 可用内存极低
Buffers:          23456 kB
Cached:         9456789 kB
SwapCached:       456789 kB     # 大量缓存被换出
Active:         12345678 kB
Inactive:        2345678 kB
Active(anon):    8901234 kB     # 匿名页(进程堆)占用高
Inactive(anon):   567890 kB
Active(file):    3456789 kB
Inactive(file):   567890 kB
Unevictable:           0 kB
Mlocked:               0 kB
SwapTotal:       8388608 kB
SwapFree:        2345678 kB     # swap 已使用 6GB+
Dirty:               123 kB
Writeback:             0 kB
AnonPages:       9123456 kB     # 匿名页接近 9GB
Mapped:          1234567 kB
Shmem:            234567 kB
Slab:            1234567 kB
SReclaimable:     567890 kB
SUnreclaim:       567890 kB

关键发现

  • AnonPages(匿名页)高达 9.1GB,主要是 JVM 堆内存
  • SwapFree 只剩 2.3GB,swap 即将耗尽
  • MemAvailable 仅 56MB,系统已无内存可用

slabinfo 分析
#

$ cat /proc/slabinfo | head -20
slabinfo - version: 2.1
# name            <active_objs> <num_objs> <objsize> <objperslab> <pagesperslab> : tunables <limit> <batchcount> <sharedfactor> : slabdata <active_slabs> <num_slabs> <sharedavail>
kmalloc-4k         123456      123456      4096       8            8             : tunables    0    0    0             : slabdata   15432   15432      0
kmalloc-1k         234567      234567      1024      32           8             : tunables    0    0    0             : slabdata    7330    7330      0
kmalloc-512        345678      345678       512      64           8             : tunables    0    0    0             : slabdata    5401    5401      0
dentry            1234567     1234567       192      42           2             : tunables    0    0    0             : slabdata   29394   29394      0
inode_cache        567890      567890       600      54           8             : tunables    0    0    0             : slabdata   10516   10516      0
buffer_head       2345678     2345678       104      39           1             : tunables    0    0    0             : slabdata   60145   60145      0

slab 占用分析

  • dentryinode_cache 占用较高,说明文件系统访问频繁
  • buffer_head 高达 234 万个对象,约 244MB,文件缓存压力大
  • 总体 slab 占用约 1.2GB,属于正常范围,不是 slab 泄漏

dmesg OOM 日志
#

$ dmesg | grep -i "kill\|oom" | tail -20
[Mon Mar 10 02:47:32 2026] java invoked oom-killer: gfp_mask=0x140cca(GFP_HIGHUSER_MOVABLE|__GFP_COMP), order=0, oom_score_adj=0
[Mon Mar 10 02:47:32 2026] CPU: 7 PID: 28471 Comm: java Not tainted 4.18.0-513.el8.x86_64 #1
[Mon Mar 10 02:47:32 2026] Hardware name: VMware, Inc. VMware Virtual Platform/440BX Desktop Reference Platform, BIOS 6.00 11/12/2020
[Mon Mar 10 02:47:32 2026] Call Trace:
[Mon Mar 10 02:47:32 2026]  dump_stack+0x41/0x60
[Mon Mar 10 02:47:32 2026]  dump_header+0x4a/0x1df
[Mon Mar 10 02:47:32 2026]  oom_kill_process.cold.33+0xb/0x1e
[Mon Mar 10 02:47:32 2026]  out_of_memory+0x1ad/0x4a0
[Mon Mar 10 02:47:32 2026]  __alloc_pages_slowpath+0xa68/0xe20
[Mon Mar 10 02:47:32 2026]  __alloc_pages_nodemask+0x245/0x280
[Mon Mar 10 02:47:32 2026] active_anon:2280834 inactive_anon:141972 isolated_anon:0
[Mon Mar 10 02:47:32 2026]  active_file:864194 inactive_file:141972 isolated_file:0
[Mon Mar 10 02:47:32 2026]  unevictable:0 dirty:30 writeback:0 unstable:0
[Mon Mar 10 02:47:32 2026]  slab_reclaimable:141972 slab_unreclaimable:141972
[Mon Mar 10 02:47:32 2026]  mapped:308641 shmem:58691 pagetables:22533 bounce:0
[Mon Mar 10 02:47:32 2026]  free:3086 free_pcp:0 free_cma:0
[Mon Mar 10 02:47:32 2026] Node 0 active_anon:9123336kB inactive_anon:567888kB active_file:3456776kB inactive_file:567888kB unevictable:0kB isolated(anon):0kB isolated(file):0kB mapped:1234564kB dirty:120kB writeback:0kB shmem:234764kB shmem_thp:0kB anon_thp:0kB writeback_tmp:0kB unstable:0kB all_unreclaimable? no
[Mon Mar 10 02:47:32 2026] Node 0 DMA free:10240kB min:256kB low:320kB high:384kB active_anon:0kB inactive_anon:0kB active_file:0kB inactive_file:0kB unevictable:0kB writepending:0kB present:15972kB managed:15908kB mlocked:0kB kernel_stack:0kB pagetables:0kB bounce:0kB free_pcp:0kB local_pcp:0kB free_cma:0kB
[Mon Mar 10 02:47:32 2026] Node 0 DMA32 free:1234kB min:1234kB low:2345kB high:3456kB active_anon:2345678kB inactive_anon:123456kB active_file:1234567kB inactive_file:123456kB unevictable:0kB writepending:0kB present:2345678kB managed:2345678kB mlocked:0kB kernel_stack:23456kB pagetables:123456kB bounce:0kB
[Mon Mar 10 02:47:32 2026] Node 0 Normal free:0kB min:5678kB low:7890kB high:10123kB active_anon:6777658kB inactive_anon:444432kB active_file:2222209kB inactive_file:444432kB unevictable:0kB present:14030810kB managed:13723456kB mlocked:0kB kernel_stack:67890kB pagetables:789012kB bounce:0kB
[Mon Mar 10 02:47:32 2026] Node 0 Normal: 0*4kB 0*8kB 0*16kB 0*32kB 0*64kB 0*128kB 0*256kB 0*512kB 0*1024kB 0*2048kB 0*4096kB = 0kB
[Mon Mar 10 02:47:32 2026] Node 0 hugepages_total=0 hugepages_free=0 hugepages_surp=0 hugepages_size=2048kB
[Mon Mar 10 02:47:32 2026] 23456 total pagecache pages
[Mon Mar 10 02:47:32 2026] 0 pages in swap cache
[Mon Mar 10 02:47:32 2026] Swap cache stats: add 1234567, delete 1234567, find 234567/3456789
[Mon Mar 10 02:47:32 2026] Free swap  = 2345678kB
[Mon Mar 10 02:47:32 2026] Total swap = 8388608kB
[Mon Mar 10 02:47:32 2026] 4096000 pages RAM
[Mon Mar 10 02:47:32 2026] 0 pages HighMem/MovableOnly
[Mon Mar 10 02:47:32 2026] 76543 pages reserved
[Mon Mar 10 02:47:32 2026] 0 pages cma reserved
[Mon Mar 10 02:47:32 2026] 0 pages hwpoisoned
[Mon Mar 10 02:47:32 2026] Tasks state (memory values in pages):
[Mon Mar 10 02:47:32 2026] [  pid  ]   uid  tgid total_vm      rss pgtables_bytes swapents oom_score_adj name
[Mon Mar 10 02:47:32 2026] [    123]     0   123    12345      123   123456        0             0 systemd-journal
[Mon Mar 10 02:47:32 2026] [  28471]  1000 28471  4567890  2280834 23456789  1234567             0 java
[Mon Mar 10 02:47:32 2026] oom-kill:constraint=CONSTRAINT_NONE,nodemask=(null),cpuset=/,mems_allowed=0,global_oom,task_memcg=/system.slice/app.service,task=java,pid=28471,uid=1000
[Mon Mar 10 02:47:32 2026] Out of memory: Kill process 28471 (java) score 912 or sacrifice child
[Mon Mar 10 02:47:32 2026] Killed process 28471 (java) total-vm:18271560kB, anon-rss:9123336kB, file-rss:0kB, shmem-rss:0kB
[Mon Mar 10 02:47:32 2026] oom_reaper: reaped process 28471 (java), now anon-rss:0kB, file-rss:0kB, shmem-rss:0kB

关键信息提取

  • OOM Score:912(满分 1000,极高)
  • JVM 进程占用:total-vm 18.2GB,anon-rss 9.1GB
  • 杀死原因:系统全局内存耗尽(global_oom)
  • 进程属于 cgroup:/system.slice/app.service

二、根因定位
#

2.1 直接原因
#

JVM 堆内存配置过大(-Xmx12g),加上堆外内存(DirectBuffer、Native 内存),总内存占用超过系统可用内存,导致系统内存耗尽触发 OOM Killer。

2.2 内存占用分析
#

系统总内存:16GB
├── JVM 堆内存:12GB(-Xmx12g)
├── JVM 元空间:~1GB
├── JVM 堆外内存:~2GB(DirectBuffer、JNI)
├── 系统/其他进程:~2GB
└── 总计:~17GB > 16GB(超配)

2.3 为什么 swap 没救场?
#

# 查看 swap 配置
$ cat /proc/sys/vm/swappiness
10

swappiness=10 表示系统倾向于保留内存给文件缓存,只有当内存极度紧张时才使用 swap。这导致:

  1. 匿名页(JVM 堆)优先保留在内存中
  2. swap 使用较晚,当开始使用 swap 时,系统已极度卡顿
  3. swap 本身也是磁盘 IO,性能极差

2.4 深层原因
#

  1. 内存超配:JVM 配置未考虑系统和其他进程
  2. 缺少监控:没有设置内存使用率告警
  3. 无保护机制:未配置 cgroup 内存限制
  4. 无优雅退出:JVM 未配置 OOM 时主动退出

三、临时恢复方案
#

3.1 立即止血
#

# 1. 检查系统状态
free -h
vmstat 1 5

# 2. 如果内存仍紧张,手动清理缓存(临时)
echo 3 > /proc/sys/vm/drop_caches

# 3. 重启应用服务
systemctl restart app

# 4. 观察恢复情况
tail -f /var/log/messages

3.2 防止再次触发
#

# 临时调大 swappiness,让系统更积极使用 swap
echo 60 > /proc/sys/vm/swappiness

# 临时关闭 OOM Killer(不推荐长期使用)
echo -17 > /proc/$(pgrep java)/oom_score_adj

四、根治方案
#

4.1 JVM 内存优化
#

# 原配置(问题)
JAVA_OPTS="-Xms12g -Xmx12g"

# 优化后配置
JAVA_OPTS="
  -Xms8g -Xmx8g                    # 堆内存降到 8GB
  -XX:MetaspaceSize=256m
  -XX:MaxMetaspaceSize=512m        # 限制元空间
  -XX:MaxDirectMemorySize=2g       # 限制堆外内存
  -XX:+HeapDumpOnOutOfMemoryError
  -XX:HeapDumpPath=/var/log/app/heapdump.hprof
  -XX:OnOutOfMemoryError='kill -9 %p'  # OOM 时主动自杀,避免被 kill -9
"

4.2 系统级优化
#

# /etc/sysctl.d/99-memory-tuning.conf

# 更早开始使用 swap,避免内存突然耗尽
vm.swappiness = 60

# 更早触发 OOM Killer,避免系统卡死
vm.overcommit_memory = 2
vm.overcommit_ratio = 80

# 内存水位线调整,更早开始回收
vm.min_free_kbytes = 1048576      # 保留 1GB 空闲内存

# 脏页刷新策略,避免突发 IO
vm.dirty_ratio = 20
vm.dirty_background_ratio = 5
vm.dirty_expire_centisecs = 500
vm.dirty_writeback_centisecs = 100

# 应用配置
sysctl --system

4.3 Cgroup 内存限制(关键)
#

# 创建 cgroup 限制应用内存
mkdir -p /sys/fs/cgroup/memory/app_limit

# 设置硬限制 12GB(物理内存 16GB,留 4GB 给系统)
echo 12884901888 > /sys/fs/cgroup/memory/app_limit/memory.limit_in_bytes

# 设置软限制 10GB
echo 10737418240 > /sys/fs/cgroup/memory/app_limit/memory.soft_limit_in_bytes

# 启用 OOM 控制,达到限制时杀死进程而非整个 cgroup
echo 1 > /sys/fs/cgroup/memory/app_limit/memory.oom_control

# 将应用进程加入 cgroup
echo $(pgrep java) > /sys/fs/cgroup/memory/app_limit/cgroup.procs

systemd 服务集成

# /etc/systemd/system/app.service
[Unit]
Description=Application Service
After=network.target

[Service]
Type=simple
User=app
Group=app
ExecStart=/usr/bin/java $JAVA_OPTS -jar app.jar
Restart=always
RestartSec=10

# 内存限制
MemoryLimit=12G
MemorySwapMax=2G

# OOM 行为
OOMScoreAdjust=-500

[Install]
WantedBy=multi-user.target

4.4 监控告警
#

# /usr/local/bin/memory-monitor.sh
#!/bin/bash

# 内存使用率监控
MEM_TOTAL=$(grep MemTotal /proc/meminfo | awk '{print $2}')
MEM_AVAILABLE=$(grep MemAvailable /proc/meminfo | awk '{print $2}')
MEM_USED=$((MEM_TOTAL - MEM_AVAILABLE))
MEM_USAGE=$((MEM_USED * 100 / MEM_TOTAL))

# 告警阈值
WARNING_THRESHOLD=80
CRITICAL_THRESHOLD=90

if [ $MEM_USAGE -gt $CRITICAL_THRESHOLD ]; then
    echo "CRITICAL: Memory usage is ${MEM_USAGE}%" | \
        mail -s "[CRITICAL] $(hostname) Memory Alert" ops@example.com
    logger -p user.crit "Memory usage critical: ${MEM_USAGE}%"
elif [ $MEM_USAGE -gt $WARNING_THRESHOLD ]; then
    echo "WARNING: Memory usage is ${MEM_USAGE}%" | \
        mail -s "[WARNING] $(hostname) Memory Alert" ops@example.com
    logger -p user.warn "Memory usage warning: ${MEM_USAGE}%"
fi

# OOM Kill 监控
if dmesg | grep -i "killed process" | tail -1 | grep -q "$(date +%s -d '5 minutes ago')"; then
    echo "OOM Kill detected!" | \
        mail -s "[CRITICAL] OOM Kill on $(hostname)" ops@example.com
fi

crontab 配置

# 每 5 分钟检查一次
*/5 * * * * /usr/local/bin/memory-monitor.sh

4.5 Prometheus 监控
#

# node_exporter 内存告警规则
groups:
- name: memory_alerts
  rules:
  - alert: HighMemoryUsage
    expr: (1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes) * 100 > 85
    for: 5m
    labels:
      severity: warning
    annotations:
      summary: "High memory usage on {{ $labels.instance }}"
      description: "Memory usage is above 85% (current: {{ $value }}%)"

  - alert: OOMKillDetected
    expr: increase(node_vmstat_oom_kill[5m]) > 0
    for: 0m
    labels:
      severity: critical
    annotations:
      summary: "OOM Kill detected on {{ $labels.instance }}"
      description: "A process was killed by OOM Killer"

五、预防机制
#

5.1 容量规划
#

内存规划公式:
JVM 堆内存 <= (物理内存 - 系统保留 - 其他进程) * 0.8

示例(16GB 内存):
- 系统保留:2GB
- 其他进程:2GB
- 可用给 JVM:12GB * 0.8 = 9.6GB
- 建议 JVM 堆内存:8GB

5.2 发布检查清单
#

  • JVM 内存配置是否经过计算?
  • 是否配置了 HeapDumpOnOutOfMemoryError?
  • 是否配置了 cgroup/systemd 内存限制?
  • 监控告警是否覆盖内存使用率?
  • 是否有 OOM Kill 专项告警?
  • 应急预案是否包含内存不足场景?

5.3 定期演练
#

# 模拟内存压力测试(测试环境)
stress-ng --vm 4 --vm-bytes 2G --timeout 60s

# 验证监控告警是否正常触发
# 验证 cgroup 限制是否生效
# 验证服务重启后是否恢复正常

六、复盘总结
#

6.1 问题本质
#

不是 JVM 内存不够,而是系统内存管理失控。JVM 配置过大 + 缺少限制 + 监控缺失,导致系统级内存耗尽。

6.2 改进措施
#

维度改进前改进后
JVM 配置-Xmx12g(无脑配大)-Xmx8g(计算后配置)
内存限制cgroup 12GB 硬限制
监控无内存告警80% 告警 + OOM 专项告警
应急被动发现主动监控 + 自动重启
预案标准化 OOM 处理流程

6.3 如果重来
#

  1. 设计阶段:做容量规划,JVM 内存不是越大越好
  2. 发布阶段:强制检查清单,内存限制必须配置
  3. 运行阶段:内存监控作为基础监控,不可或缺
  4. 故障阶段:先保护系统,再恢复业务

七、核心知识点
#

7.1 OOM Killer 工作原理
#

1. 进程申请内存
2. 内核检查可用内存
3. 尝试回收缓存(page cache)
4. 尝试 swap 换出
5. 内存仍不足 → 触发 OOM Killer
6. 计算每个进程 oom_score
7. 杀死得分最高的进程

oom_score 计算因素

  • 内存占用量(RSS + Swap)
  • 运行时间(运行越久得分越低)
  • 优先级(nice 值)
  • oom_score_adj(可手动调整)

7.2 关键内核参数
#

参数作用推荐值
vm.swappinessswap 使用倾向60(服务器)
vm.overcommit_memory内存分配策略2(严格检查)
vm.overcommit_ratio可超配比例80
vm.min_free_kbytes最小空闲内存1GB

7.3 调试命令速查
#

# 查看 OOM 日志
dmesg | grep -i "kill\|oom"
journalctl -k | grep -i "oom"

# 查看进程内存
pmap -x <pid>
cat /proc/<pid>/status | grep -i vm
cat /proc/<pid>/smaps_rollup

# 查看系统内存
cat /proc/meminfo
vmstat -s
slabtop

# 查看 cgroup 内存
cat /sys/fs/cgroup/memory/memory.stat
systemctl status <service>

结语
#

OOM Killer 是 Linux 内核的最后防线,不是解决方案。生产环境中,我们需要:

  1. 合理规划:容量规划是基本功
  2. 主动限制:cgroup/systemd 内存限制是必选项
  3. 全面监控:内存监控告警不能少
  4. 快速恢复:自动化重启 + 优雅降级

希望本文的实战数据和根治方案,能帮助你在生产环境中避免类似的凌晨告警。


参考文档