Linux 内核内存压缩与 OOM Killer 深度实战:碎片整理、内存回收与生产调优全指南

引言

在生产环境中,Linux 服务器运行一段时间后,物理内存碎片化会日益严重。当系统需要大页(HugePage)或连续物理内存时,分配失败可能导致性能下降甚至服务异常。同时,当内存耗尽时,OOM Killer 会强制杀死进程,可能造成关键服务中断。

本文深入剖析 Linux 内核的内存压缩(Memory Compaction)机制和 OOM Killer 的工作原理,涵盖碎片检测、压缩算法、/proc 接口调优、early reclaim、以及生产环境中 OOM 防护策略。

一、为什么需要内存压缩?

1.1 物理内存碎片化问题

Linux 物理内存以页框(Page Frame)为单位管理,默认 4KB。随着系统运行:

  • 频繁分配和释放不同大小的内存块
  • 可移动页面(movable pages)和不可移动页面(unmovable pages)交错分布
  • 形成大量不连续的小空闲块,无法满足连续内存分配请求

典型场景:

  • 透明大页(THP)需要连续 2MB 物理内存
  • 某些 DMA 设备要求连续物理页
  • 内核模块需要连续大块内存

1.2 碎片化的影响

# 查看当前内存碎片情况
cat /proc/pagetypeinfo

# 输出示例(碎片化严重时):
Page block order: 9
Pages per block:  512

Free pages count at migrate types in migrate types in order
Node 0, zone      DMA, type    Unmovable    3      2      1      4      2      1      0      0      1      1      3
Node 0, zone      DMA, type  Reclaimable    0      1      1      1      2      0      2      1      0      1      0
Node 0, zone      DMA, type      Movable    1      0      2      1      3      2      0      1      2      2      1
Node 0, zone      DMA, type      Reserve    0      0      0      0      0      0      0      0      0      2      0
Node 0, zone      DMA, type      Isolate    0      0      0      0      0      0      0      0      0      0      0

# 高阶连续页面数量极少,说明碎片化严重

二、内存压缩(Memory Compaction)机制

2.1 核心概念

内存压缩是 Linux 内核引入的一种在线碎片整理技术,由 Mel Gorman 在 2.6.38 版本中合入主线内核。其核心思想是:将可移动页面迁移到其他位置,从而腾出连续的物理内存区域。

2.2 页面迁移类型(Migrate Types)

为了实现安全迁移,内核将页面按可移动性分类:

类型说明典型页面
Movable任意迁移用户空间页、文件缓存(page cache)
Reclaimable可回收,不可直接迁移内核缓存(如 dentries、inodes)
Unmovable不可移动内核数据、DMA 分配、slab 页面
Reserve紧急保留备用水源
Isolate隔离,不可分配压缩过程中的临时隔离区

2.3 压缩流程

两个扫描器协同工作:

  • 迁移扫描器(migration scanner):从 zone 底部向上扫描,收集可移动页面作为"迁移源"
  • 空闲扫描器(free scanner):从 zone 顶部向下扫描,寻找空闲页面作为"迁移目标"
// 核心数据结构:compact_control
struct compact_control {
    struct list_head freepages;     // 空闲页面链表
    struct list_head migratepages;  // 待迁移页面链表
    unsigned long nr_migratepages;  // 待迁移页面数
    unsigned long nr_freepages;     // 空闲页面数
    unsigned long free_pfn;         // 空闲扫描器起始 PFN
    unsigned long migrate_pfn;      // 迁移扫描器起始 PFN
    enum migrate_mode mode;         // 压缩模式
    int order;                      // 目标连续页面阶
    bool ignore_skip;               // 是否跳过标记为 skip 的页面
};

2.4 触发条件

内存压缩有多种触发路径:

  1. 同步压缩(synchronous compaction):高阶分配失败时直接触发,阻塞等待
  2. 异步压缩(asynchronous compaction):由 kcompactd 后台线程执行,不阻塞调用者
  3. 手动触发:通过 /proc/sys/vm/compact_memory 强制触发
  4. THP 分配:透明大页需要连续 2MB 物理内存时触发
# 手动触发全系统内存压缩(需要 root)
echo 1 > /proc/sys/vm/compact_memory

# 查看压缩统计
cat /proc/vmstat | grep compact
# compact_migrate_scanned: 扫描的可移动页面数
# compact_free_scanned: 扫描的空闲页面数
# compact_isolated: 隔离的页面数
# compact_stall: 压缩停滞次数(失败次数)
# compact_fail: 压缩失败次数
# compact_success: 压缩成功次数

三、OOM Killer 深度解析

3.1 OOM 触发机制

当系统内存耗尽且无法通过页回收(page reclaim)和内存压缩获得足够内存时,内核会调用 out_of_memory() 函数,触发 OOM Killer 选择并杀死一个进程。

3.2 OOM Score 计算

OOM Killer 通过 oom_badness() 为每个进程打分,得分越高被杀概率越大:


// 简化版 oom_badness 计算逻辑
unsigned long oom_badness(struct task_struct *p, unsigned long totalpages) {
    long points;
    
    // 1. 计算内存占用得分(物理内存 + swap + page cache)
    points = get_mm_rss(p->mm) + get_mm_counter(p->mm, MM_SWAPENTS) +
             mm_pgtables_bytes(p->mm) / PAGE_SIZE;
    
    // 2. 转换为 oom_score 比例(占总内存的万分比)
    points = points * 1000 / totalpages;
    
    // 3. 应用 oom_score_adj 调整
    points += (long)p->signal->oom_score_adj * (long)totalpoints / OOM_SCORE_ADJ_MAX;
    
    return points > 0 ? points : 1;
}

关键参数:

  • oom_score:当前 OOM 得分,动态计算,只读(/proc/[pid]/oom_score)
  • oom_score_adj:OOM 得分调整值,范围 -1000 到 +1000(/proc/[pid]/oom_score_adj)
  • oom_adj:旧版接口,已废弃,范围 -17 到 +15

3.3 特殊进程保护

# 查看关键进程的 OOM 分数
cat /proc/1/oom_score        # init/systemd
cat /proc/$(pgrep mysqld)/oom_score   # MySQL 进程

# 保护 MySQL 进程(设置 -1000 表示永不杀死)
echo -1000 > /proc/$(pgrep mysqld)/oom_score_adj

# 保护 MySQL 进程(设置 -1000 表示永不杀死)
echo -1000 > /proc/$(pgrep mysqld)/oom_score_adj

# 让 Java 进程更容易被杀死(设置正数)
echo 500 > /proc/$(pgrep java)/oom_score_adj

# 查看系统 OOM 配置
cat /proc/sys/vm/oom_kill_allocating_task  # 0=启发式 1=直接杀触发者
cat /proc/sys/vm/panic_on_oom              # 0=不panic 1=系统崩溃 2=总是panic

3.4 OOM 事件监控

  • 内核日志:dmesg | grep -i "out of memory"
  • systemd 日志:journalctl -k | grep -i oom
  • 审计日志:audit.log 中的 kill 事件
  • 自定义监控:通过 netlink 接口 NETLINK_KOBJECT_UEVENT 监听 OOM 事件

四、生产环境调优实战

4.1 压缩相关内核参数

# 查看和调整压缩参数
sysctl -a | grep compact

# 关键参数说明:
# vm.compaction_proactiveness: 压缩积极度(0-100, 默认 20)
#   - 值越大,越倾向于提前压缩,减少碎片
#   - 值越大,CPU 开销越高

# vm.extfrag_threshold: 外部碎片阈值(0-1000, 默认 500)
#   - 值越小,越容易触发压缩
#   - 值越大,越倾向于分配失败后再压缩

# vm.compact_unevictable_allowed: 是否压缩不可回收页(0/1, 默认 1)

# 调整示例(高内存延迟敏感场景)
cat >> /etc/sysctl.d/99-memory.conf << 'EOF'
vm.compaction_proactiveness=50
vm.extfrag_threshold=300
vm.compact_memory=1
EOF
sysctl -p /etc/sysctl.d/99-memory.conf

4.2 OOM Killer 防护配置

# 防止 OOM 导致系统崩溃
cat >> /etc/sysctl.d/99-oom.conf << 'EOF'
# OOM 时不 panic,而是启动 OOM Killer
vm.panic_on_oom = 0

# 直接杀死触发 OOM 的进程(避免长时间遍历)
vm.oom_kill_allocating_task = 1

# 限制 OOM Killer 选择范围(是否考虑进程树)
vm.oom_dump_tasks = 1
EOF
sysctl -p /etc/sysctl.d/99-oom.conf

# 使用 cgroup v2 限制进程内存(彻底避免 OOM 杀关键进程)
# 创建 memory cgroup
mkdir -p /sys/fs/cgroup/critical_apps
echo "512M" > /sys/fs/cgroup/critical_apps/memory.max
echo "1" > /sys/fs/cgroup/critical_apps/memory.oom.group

# 将 MySQL 加入该 cgroup
echo $(pgrep mysqld) > /sys/fs/cgroup/critical_apps/cgroup.procs

4.3 THP 与压缩的相互作用

# THP 三种模式
cat /sys/kernel/mm/transparent_hugepage/enabled
# [always] madvise never  - 始终启用(推荐关闭)
# always [madvise] never  - 对 madvise 区域启用(推荐)
# always madvise [never]  - 禁用(数据库场景)

# 数据库/Redis 场景建议关闭 THP
echo never > /sys/kernel/mm/transparent_hugepage/enabled
echo never > /sys/kernel/mm/transparent_hugepage/defrag

# 监控 THP 和压缩的交互
watch -n 1 'cat /proc/vmstat | grep -E "(compact|thp)"'

五、实战案例与排错

5.1 案例:大页分配失败导致数据库性能抖动

问题:MySQL 在使用 huge pages 时,因内存碎片化导致分配失败,回退到普通页面,性能骤降。

排查过程:

# 1. 查看 huge page 分配情况
cat /proc/meminfo | grep -i huge
# HugePages_Total:    2048
# HugePages_Free:      512  <- 剩余不足
# HugePages_Rsvd:        6
# Hugepagesize:       2048 kB

# 2. 查看页面连续度
cat /proc/pagetypeinfo | grep -A3 "Node 0      Normal" | head -20

# 3. 查看压缩统计 - 发现大量失败
cat /proc/vmstat | grep compact
# compact_fail:    15234    <- 失败次数很高
# compact_success: 23       <- 成功次数极少
# compact_stall:   18901

# 4. 尝试手动压缩
echo 1 > /proc/sys/vm/compact_memory
sleep 5
cat /proc/meminfo | grep -i huge

# 5. 临时解决:立即分配 huge pages
echo 1024 > /proc/sys/vm/nr_hugepages

# 6. 根本解决:调整内核参数
cat >> /etc/sysctl.d/99-hugepages.conf << 'EOF'
vm.nr_hugepages = 2048
vm.nr_overcommit_hugepages = 512
vm.compaction_proactiveness = 60
EOF

5.2 案例:OOM Killer 误杀核心服务

问题:Redis 哨兵节点因 OOM 被杀死,导致集群故障切换。

排查与修复:

# 1. 查看 OOM 日志
dmesg | grep -i "out of memory" | tail -20
dmesg | grep -i "killed process"

# 典型输出:
# [12345.678] Out of memory: Killed process 1234 (redis-server) total-vm:8GB

# 2. 设置 Redis OOM 保护
echo -900 > /proc/$(pgrep redis-server)/oom_score_adj

# 3. 使用 systemd 保护(持久化)
mkdir -p /etc/systemd/system/redis.service.d
cat > /etc/systemd/system/redis.service.d/oom.conf << 'EOF'
[Service]
OOMScoreAdjust=-900
EOF
systemctl daemon-reload
systemctl restart redis

# 4. 配置 cgroup 内存限制
# 在 redis.service 中添加
[Service]
MemoryMax=6G
MemorySwapMax=0

# 5. 监控告警配置
# 监控 /proc/vmstat 的 oom_kill 计数器
watch -n 1 'grep oom_kill /proc/vmstat'

5.3 性能监控脚本

#!/bin/bash
# memory_health.sh - 内存健康度监控脚本
# 每 30 秒检查一次碎片化和 OOM 趋势

while true; do
    TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
    
    # 1. 内存基础状态
    MEM_AVAILABLE=$(grep MemAvailable /proc/meminfo | awk '{print $2}')
    MEM_TOTAL=$(grep MemTotal /proc/meminfo | awk '{print $2}')
    MEM_PCT=$(( (MEM_TOTAL - MEMABLE_AVAILABLE) * 100 / MEM_TOTAL ))
    
    # 2. 碎片化指标
    COMPACT_FAIL=$(grep compact_fail /proc/vmstat | awk '{print $2}')
    COMPACT_SUCC=$(grep compact_success /proc/vmstat | awk '{print $2}')
    FRAG_INDEX=$(cat /sys/kernel/debug/extfrag/extfrag_index 2>/dev/null | head -1)
    
    # 3. OOM 统计
    OOM_KILL=$(grep oom_kill /proc/vmstat | awk '{print $2}')
    
    echo "[$TIMESTAMP] Memory: ${MEM_PCT}% used | Compact: fail=$COMPACT_FAIL succ=$COMPACT_SUCC | OOM kills: $OOM_KILL"
    
    # 4. 碎片化告警(连续页面不足)
    if [ -f /proc/pagetypeinfo ]; then
        PAGES_0=$(grep "pages free" /proc/pagetypeinfo | awk '{print $3}')
        if [ "$PAGES_0" -lt 100 ]; then
            echo "[ALERT] Low order-0 pages: $PAGES_0, consider manual compaction"
        fi
    fi
    
    sleep 30
done

六、最佳实践总结

6.1 压缩优化 Checklist

场景配置建议
高内存数据库压缩积极度调至 50-80,extfrag_threshold 调低至 300-500
HugePages 环境启动时预分配 + 启用 compaction,避免运行时分配
嵌入式/IoT关闭压缩以节省 CPU,或使用 early reclaim 平衡
大数据/Spark默认值即可,监控 compact_fail 趋势,必要时触发手动压缩
低延迟交易系统禁用 THP,关闭压缩异步模式,预分配 + 内存锁定

6.2 OOM 防护 Checklist

  • 为所有关键服务设置 OOMScoreAdjust(systemd)
  • 关键服务使用 MemoryMax 设置 cgroup 内存上限
  • 配置 vm.panic_on_oom = 0 防止系统崩溃
  • vm.oom_dump_tasks = 1 记录 OOM 候选进程信息便于事后分析
  • 部署 OOM 监控告警(监控 dmesg + vmstat + 监控工具)
  • 关键服务内存限制 = 进程最大内存 × 1.2-1.5

6.3 压缩调优速查表

# ============================================
# 最简生产配置(通用场景)
# ============================================
cat > /etc/sysctl.d/99-memory-tuning.conf << 'EOF'
## 内存压缩
kernel.sysctl_compact_unevictable_allowed = 1
vm.compaction_proactiveness = 30
vm.extfrag_threshold = 500

## OOM 防护
vm.panic_on_oom = 0
vm.oom_kill_allocating_task = 1
vm.oom_dump_tasks = 1

## Swap 管理
vm.swappiness = 10
vm.vfs_cache_pressure = 100
EOF

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

七、总结

Linux 内存管理中的 Compaction 和 OOM Killer 是保障系统稳定运行的两个关键机制:

  • Compaction 通过迁移可移动页面来减少碎片,保障大页分配,参数调优需要在 CPU 开销和碎片化之间权衡
  • OOM Killer 是最后的防线,通过 oom_score_adj 和 cgroup 可以精确控制哪些进程受到保护
  • 两者配合使用:当压缩无法满足需求时,OOM Killer 介入;合理的压缩可以减少 OOM 触发频率

生产环境中,建议根据业务特性制定内存管理策略,配合监控告警和自动化运维工具,在碎片化发生前主动干预,避免 OOM 导致的业务中断。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部