1. 内存碎片问题的本质与影响
在长时间运行的系统里,内存碎片化就像书房里随意堆放的书籍——虽然总空间足够,但当需要连续摆放多本成套书籍时,却找不到足够大的连续空间。这种现象在操作系统中表现为:尽管系统显示有足够空闲内存,但当需要分配连续物理页面时(如分配2MB大页),却频繁遭遇分配失败。
1.1 内存碎片的两种形态
内存碎片主要分为两种类型:
- 外部碎片:空闲内存被已分配的内存块分隔成多个不连续的小块。就像停车场被随机停放的车辆分割,虽然总剩余车位足够,但无法容纳一辆加长轿车。
- 内部碎片:分配的内存块内部未被使用的部分。例如分配了4KB但只使用3KB,剩余的1KB就是内部碎片。这类似于购买整箱饮料却只喝掉其中几罐。
在Linux内核中,外部碎片对系统性能影响更为显著。当DMA设备、GPU驱动或透明大页(THP)需要连续物理内存时,外部碎片会直接导致分配失败。现代服务器通常配置128GB甚至更大内存,但可能因为几十MB的碎片导致关键服务异常。
1.2 碎片化的量化评估
内核通过"碎片指数"(fragmentation index)量化内存碎片程度,计算公式为:
code复制碎片指数 = (1 - (最大连续空闲块大小 / 总空闲内存)) × 迁移成本系数
这个0到1000的数值(实际代码中除以1000变为0-1浮点数)综合考虑了:
- 最大可用连续块与总空闲内存的比值
- 页面迁移的预期成本(移动不可移动页面的代价)
- 当前zone的水位线压力
当指数超过阈值(默认值600)时,内核会触发主动碎片整理。我们可以通过/proc/buddyinfo观察各阶空闲页面分布:
code复制# cat /proc/buddyinfo
Node 0, zone Normal 73 54 38 29 15 8 4 2 1 1 0
每列数字表示对应阶数(2^0, 2^1,..., 2^10页)的连续空闲块数量。上例显示高阶(右侧)空闲块较少,存在一定碎片。
2. Linux内存碎片整理机制剖析
2.1 迁移类型与页面分类
Linux内核将内存页面按迁移特性分为五类(定义于include/linux/mmzone.h):
| 迁移类型 | 说明 | 典型内存 |
|---|---|---|
| MIGRATE_UNMOVABLE | 不可移动 | 内核栈、DMA缓冲区 |
| MIGRATE_MOVABLE | 可移动 | 用户进程内存 |
| MIGRATE_RECLAIMABLE | 可回收 | 文件缓存 |
| MIGRATE_CMA | 连续内存分配区 | 预留大块连续内存 |
| MIGRATE_HIGHATOMIC | 高优先级原子分配 | 中断上下文 |
这种分类使碎片整理器能智能决策:
- 只移动MOVABLE页面,避免破坏内核关键结构
- 优先整理RECLAIMABLE区域,通过回收释放更多连续空间
- 保护CMA区域不被普通分配占用
2.2 核心算法实现
碎片整理的核心流程在mm/compaction.c中实现,主要分为三个阶段:
2.2.1 扫描阶段(compact_zone)
- 从zone的起始位置开始扫描
- 创建两个游标:
- 迁移扫描器:查找可移动页面
- 空闲扫描器:查找合适的目标位置
- 使用位图记录可移动页面位置
2.2.2 迁移阶段(migrate_pages)
- 对每个候选页面:
- 检查是否真的可移动(通过page->mapping和页标志)
- 分配目标页面(优先从相同NUMA节点)
- 调用特定迁移函数复制内容
- 处理特殊情况:
- 大页(THP)需要整体迁移
- 正在回写的页面需要等待完成
2.2.3 释放阶段
- 原页面被释放回伙伴系统
- 合并新释放的块形成更高阶空闲块
整个过程类似于整理凌乱的衣柜:
- 先标记哪些衣服可以移动(夏季衣物)
- 找到空余的抽屉空间
- 将衣服移动到新位置
- 腾出的空间可以放入更大的物品(如冬季外套)
2.3 kcompactd守护进程
内核线程kcompactd负责后台碎片整理,其工作逻辑如下:
c复制// mm/compaction.c
static int kcompactd(void *p)
{
while (!kthread_should_stop()) {
for_each_zone(zone) {
if (fragmentation_index(zone) > sysctl_compaction_threshold) {
compact_zone(zone);
}
}
schedule_timeout_interruptible(msecs_to_jiffies(500));
}
return 0;
}
关键参数可通过sysctl调整:
bash复制# 设置触发整理的碎片指数阈值(默认600)
echo 700 > /proc/sys/vm/compaction_threshold
# 调整检查间隔(毫秒)
echo 1000 > /proc/sys/vm/compaction_interval
3. 实战:优化大页分配场景
3.1 透明大页(THP)的碎片挑战
当启用透明大页时,常规的2MB大页分配极易受碎片影响。以下是一个生产环境优化案例:
-
问题现象:
- MySQL频繁出现"cannot allocate THP"警告
grep thp /proc/vmstat显示thp_fault_fallback持续增加cat /proc/buddyinfo显示高阶空闲块为零
-
解决方案:
bash复制# 1. 预留CMA区域(占内存10%)
echo 10240 > /sys/kernel/mm/hugepages/hugepages-2048kB/nr_overcommit_hugepages
# 2. 调整碎片整理策略
echo 1 > /sys/kernel/mm/transparent_hugepage/defrag
echo 500 > /proc/sys/vm/compaction_threshold
# 3. 监控指标
watch -n 1 'cat /proc/buddyinfo /proc/vmstat | grep -e thp -e compact'
- 效果验证:
- THP分配成功率从63%提升至98%
- 查询延迟P99降低40%
- 系统整体内存利用率提高15%
3.2 容器环境特别处理
在Kubernetes集群中,内存碎片会导致Pod启动失败。推荐配置:
- 为每个节点预留CMA内存:
yaml复制# kubelet配置
--kube-reserved=cpu=500m,memory=1Gi,hugepages-2Mi=512Mi
--system-reserved=memory=2Gi
- 在容器运行时启用内存整理:
bash复制# containerd配置
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options]
MemoryCompact = true
MemoryDefrag = 500ms
4. 高级调试与性能优化
4.1 动态追踪碎片整理
使用ftrace观察整理过程:
bash复制# 1. 启用跟踪点
echo 1 > /sys/kernel/debug/tracing/events/compaction/enable
# 2. 过滤特定zone
echo "zone_normal" > /sys/kernel/debug/tracing/events/compaction/compact_zone/filter
# 3. 查看实时数据
cat /sys/kernel/debug/tracing/trace_pipe
典型输出示例:
code复制kcompactd0-120 [000] ...1 3145.125678: compact_zone: zone=Normal frag_index=723 free=143736KB contig=2048KB
kcompactd0-120 [000] ...1 3145.129812: migrate_pages: nr_succeeded=342 nr_failed=0
4.2 性能调优参数
关键可调参数及其影响:
| 参数路径 | 默认值 | 建议范围 | 作用 |
|---|---|---|---|
| /proc/sys/vm/compaction_threshold | 600 | 500-800 | 触发整理的碎片指数 |
| /proc/sys/vm/compact_memory | 0 | 手动触发 | 立即执行全系统整理 |
| /sys/kernel/mm/transparent_hugepage/defrag | always | madvise | THP整理策略 |
| /proc/sys/vm/extfrag_threshold | 500 | 300-700 | 外部碎片告警阈值 |
调整原则:
- 对于数据库等需要大页的服务:降低阈值(500-600),更积极整理
- 对于内存敏感的实时系统:提高阈值(700+),减少整理开销
- 在内存紧张的系统中:禁用THP (
never) 避免整理风暴
4.3 常见问题排查
问题现象:系统日志频繁出现"page allocation failure"错误,但free命令显示有足够内存。
诊断步骤:
-
检查伙伴系统状态:
bash复制cat /proc/buddyinfo | awk '{print $1,$2,$(NF-2),$(NF-1),$NF}'关注高阶(最右侧几列)是否接近零。
-
查看碎片指数:
bash复制grep -A10 "Normal" /proc/zoneinfo | grep -E "frag|free" -
分析最近整理记录:
bash复制
grep compact /proc/vmstat如果compact_stall值持续增加但compact_success不增长,表明整理无效。
解决方案:
-
临时缓解:
bash复制echo 1 > /proc/sys/vm/compact_memory -
长期优化:
- 调整应用内存分配模式,减少高阶分配需求
- 预留CMA区域供关键服务使用
- 考虑使用SLUB分配器的
CONFIG_SLUB_DEBUG定位内存泄漏
内存碎片整理是系统长时间稳定运行的重要保障,理解其工作原理并根据实际负载特性进行调优,能有效避免因内存分配失败导致的性能下降和服务中断。在实际运维中,建议结合监控系统对/proc/buddyinfo和/proc/vmstat中的关键指标进行持续观测,建立碎片化预警机制。
