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 触发条件
内存压缩有多种触发路径:
- 同步压缩(synchronous compaction):高阶分配失败时直接触发,阻塞等待
- 异步压缩(asynchronous compaction):由 kcompactd 后台线程执行,不阻塞调用者
- 手动触发:通过 /proc/sys/vm/compact_memory 强制触发
- 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 导致的业务中断。

发表评论 取消回复