内存碎片这东西Linux运维同学大概都碰到过但多数时候它是个“隐形刺客”。你可能不会天天盯着 /proc/buddyinfo 看直到某天服务突然出现几十毫秒的分配抖动或者 htop 里 kcompactd 莫名其妙吃掉一个 CPU 核心才发现碎片已经悄悄影响了整个系统的“手感”。而主动内存规整proactive compaction正是内核为缓解这类问题专门设计的一剂“预防针”与其等分配大块内存时临时抱佛脚不如让后台线程提前把物理页整理好等真正需要时直接拿出来用。这篇文章想把主动内存规整从原理到调优完整过一遍包括它和传统内存规整的关系、碎片指数如何参与决策、相关内核参数该怎么调以及我在实际服务器上排查和调整时踩过的坑。无论你是做系统调优的运维还是想搞懂内核内存管理源码的开发者读完后应该都能对“什么时候整理内存、整理到什么程度、如何观察效果”有一个清晰的判断依据。1. 先说清楚内存碎片为什么会让系统“卡一下”1.1 高阶分配失败的本质是“连续物理空间稀缺”Linux 物理内存分配用的是伙伴系统buddy allocator空闲页按 2 的幂次组织成不同的 order 链表order-0 是单个物理页x86_64 下 4KBorder-1 是 2 页order-2 是 4 页依此类推。一次内存分配可能只需要一页也可能要一大块连续物理页透明大页THP在 x86_64 上要 order-9也就是连续的 2MB某些设备驱动做 DMA 可能要求更高的 orderCMA 区域更是从名字上就写明了它要的是“连续内存分配”。问题在于物理内存不像我们想象的那么整齐。一个长期运行的服务器上内核、应用程序、页缓存、匿名页、各种临时映射会不断分配和释放。假设系统总共有 4GB 内存空闲页总量还有 1GB但如果这 1GB 被拆成了成百上千个散布在不同 order 块里的小碎片你就可能找不到一个连续的 order-9 块。这就是外部碎片内存总量够但“看得见、用不了”。打个比方停车场里有 1000 个空车位但每个区域都稀稀拉拉剩一两个你想找一个能并排停两辆车的连续空位都难。操作系统也是这样空闲页数量不等于空闲连续性这是外部碎片问题的核心也是内存规整存在的根本原因。1.2 被动规整是“亡羊补牢”主动规整是“未雨绸缪”在主动内存规整出现之前内核面对高阶分配失败主要靠两种手段直接回收direct reclaim和被动内存规整direct compaction。前者是去压榨页缓存、回收可回收页来腾空间后者是把已经分配的页尽量往一边迁移、挤出一块连续空闲区。问题是这两件事都发生在“分配请求已经来了、实在凑不出内存”的那一刻执行路径上带着明显的延迟代价。我见过不少数据库主机出现过大页分配延迟飙升的情况一次 THP 分配在最坏情况下可能触发几万页的页面迁移整个分配线程被阻塞住业务 RT 直接拉出一条尖刺。这种“平时不整理、关键时候才扫厕所”的模式对延迟敏感的应用非常不友好。主动内存规整的思路就是反转这个顺序让内核里一个常驻的后台线程定期检查内存碎片程度如果发现某个内存节点的碎片指数已经高到值得整理就提前执行一部分 compaction 工作。这样当真正的 order-9、order-10 分配请求到来时内存里已经有现成的连续块可用分配路径可以快速返回。用一句大白话说就是“把账还到平时别在双十一当天崩溃”。2. 主动内存规整的工作机制与关键参数2.1 干活的线程kcompactd 和它的周期性检查执行主动规整的主角是 kcompactd 内核线程。它和 kswapd 一样是每个内存节点一个的后台线程。kswapd 负责把内存水位维持在合理范围kcompactd 则专门负责整理内存碎片。平时 kcompactd 会进入睡眠状态等两类信号把它唤醒一是内存分配路径在某个 zone 里发现碎片严重、需要后台整理时主动唤醒它二是周期性的主动检查也就是我们说的 proactive compaction。在较新的内核版本里kcompactd 醒来之后会先去判断系统是否开启了主动规整也就是看 vm.compaction_proactiveness 这个参数是否大于 0。如果大于 0它会检查当前节点各 zone 的碎片指数一旦发现碎片指数超过 vm.extfrag_threshold 设定的阈值就会调用 compact_node 相关逻辑对节点内的 zone 执行一轮限定规模的规整。之所以强调“限定规模”是因为主动规整不能像手动触发那样一口气把整个节点翻一遍那样 CPU 开销太大而是每次只迁移一定数量的页做完一批再睡避免长时间霸占 CPU。需要注意的是主动规整做的事情本身和被动 compaction 没有本质区别都是 isolate 页面、migrate 页面、然后释放合并出更大的连续块。区别只在于“谁在什么时候发起”。主动路径由后台线程在碎片变严重之前发起被动路径则是分配线程在分配失败之后发起两者共享同一套底层实现。2.2 碎片指数内核怎么判断“该不该整理”谈到主动规整绕不开一个概念叫碎片指数fragmentation index。这是一个 0 到 1000 之间的整数用来描述某个 zone 在给定 order 下碎片化的严重程度。数值越低说明现有空闲页里凑出目标阶连续块的难度越小数值越高说明明明有空闲页但就是凑不出大块连续内存这时候就值得动用 compaction。碎片指数的计算并不只看空闲页总量还会考虑多少空闲页已经聚成了目标阶的大块。我用自己的理解给你一个直觉如果系统里空闲页总数很多但特定 order 的空闲块数量很少那碎片指数就会往 1000 方向走如果空闲页本身就很紧张连 order-0 都缺那碎片指数通常也会非常高因为这时候根本没有余力做迁移。内核在实际实现里做了很多精细处理还会结合迁移类型、页块分配状态等维度但大方向就是这样一个“需求和供应之间的错配程度”度量。内核引入碎片指数的目的是为了给“到底该先回收还是先整理”提供一个量化依据。默认情况下vm.extfrag_threshold 的值是 500意思就是当碎片指数超过 500 时倾向于执行 compaction低于 500 时则更多依赖正常回收和分配逻辑。2.3 三个你需要记住的控制项既然要调优就必须先搞清楚哪些参数在起作用。主动内存规整相关的主要有三个参数接口默认值作用备注/proc/sys/vm/compaction_proactiveness20部分内核默认启用控制主动规整的积极程度0 表示关闭主动规整值越大后台整理越积极范围 0-100单位是百分比式的“力度”/proc/sys/vm/extfrag_threshold500碎片指数阈值超过这个值才触发整理范围 0-1000调低更激进调高更保守/proc/sys/vm/compact_memory无写入 1 触发手动触发一次全系统内存规整适合离线维护或大版本变更前手动整理这里要特别提醒一点很多文章会把 compact_memory 和主动规整混在一起讲实际上它不是主动规整的开关而是手动强制工具。往 /proc/sys/vm/compact_memory 里写 1会同步触发所有节点的完整内存规整这个过程可能持续较长时间对在线业务有潜在影响千万不要在业务高峰随便执行。3. 怎么观察主动内存规整到底干了什么3.1 用 /proc/buddyinfo 看碎片分布想判断系统是不是有碎片问题最直观的就是看 /proc/buddyinfo。它按内存节点和 zone 列出每个 order 下当前空闲块的个数。我拿一个典型的服务器输出做个示意Node 0, zone DMA 1 0 1 0 2 1 1 0 1 1 3 Node 0, zone DMA32 3186 1791 543 179 32 8 2 0 0 0 0 Node 0, zone Normal 55832 12511 9381 2945 612 91 12 3 0 0 0从左到右依次是 order-0 到 order-10 的空闲块数量。如果你关注 THP那要重点看 order-9 那一列。上面这个 Normal zone 里 order-9 只有 0 个order-8 有 3 个说明 2MB 大块非常稀缺后续 THP 分配大概率要触发直接规整。反过来如果 order-9、order-10 都有不少空闲块说明之前主动规整或被动规整起了作用内存连续性不错。看 buddyinfo 不需要额外工具只需要你有 root 权限。我一般会连续采样几次观察空闲块数量是否持续下降。如果持续下降说明系统处于碎片加剧的状态这时候调高 compaction_proactiveness 是合理的。3.2 用 /proc/vmstat 里的 compact 字段评估效果光看碎片分布还不够你还需要知道规整动作发生得勤不勤、成没成功。内核在 /proc/vmstat 里维护了一组 compact 相关的统计计数常见的有compact_stall分配路径因内存规整而被阻塞的次数。compact_fail规整尝试后仍无法满足高阶分配的次数。compact_success规整成功满足高阶分配的次数。compact_pages_migrated / compact_pages_isolated迁移和隔离的页数。compact_migrate_scanned / compact_free_scanned迁移扫描和空闲扫描的页数。主动规整和被动规整都会累加到这些计数里所以没有办法直接从 vmstat 里拆出“主动完成了多少工作”。但如果某个时段内 compact_success 在增加同时你看到 buddyinfo 里大块数量在变多基本可以判断规整在起作用。如果 compact_fail 一直增长说明规整一直在做但总是失败这通常不是阈值不够低而是内存整体不足、没有足够空闲页可以作为迁移目标这时候应该考虑扩容或者减少内存占用而不是继续加大整理力度。3.3 用 ftrace 跟踪 kcompactd 的实际行为如果你想更精确地观察 kcompactd 是不是在执行主动规整可以用 ftrace 挂上内存规整相关的事件点。比较常用的事件是 mm_compaction_kcompactd_sleep 和 mm_compaction_kcompactd_wakeup还有 mm_compaction_begin 和 mm_compaction_end。大致操作如下# 确保 tracefs 已挂载一般都在 /sys/kernel/debug/tracing echo 0 /sys/kernel/debug/tracing/tracing_on echo mm_compaction_kcompactd_sleep /sys/kernel/debug/tracing/set_event echo mm_compaction_kcompactd_wakeup /sys/kernel/debug/tracing/set_event echo mm_compaction_begin /sys/kernel/debug/tracing/set_event echo mm_compaction_end /sys/kernel/debug/tracing/set_event echo 1 /sys/kernel/debug/tracing/tracing_on sleep 10 cat /sys/kernel/debug/tracing/trace这样做的好处是能看到 kcompactd 被唤醒的频率、每次 compaction 持续的时间以及是哪个 node 触发了整理。要注意tracefs 的路径在不同发行版上可能略有差异有些系统需要先挂载 debugfs。这个方法适合做短时间的问题定位长时间开着会把 trace ring buffer 撑爆。4. 调优经验不同场景怎么做、怎么验证4.1 常见场景的推荐配置主动规整不是参数越大越好它本质上是“拿后台 CPU 和迁移开销换分配延迟”。不同业务对延迟和 CPU 的敏感度不一样配置思路差别很大。场景建议配置原因高负载数据库、Java 服务开 THPcompaction_proactiveness20-50extfrag_threshold500这类服务对 THP 分配延迟敏感需要保证连续大块随时可用容器宿主机、多租户混部compaction_proactiveness0-10后台迁移可能引起小抖动容器场景更看重 CPU 隔离和稳定纯计算、无大页需求compaction_proactiveness0不需要高阶连续内存主动规整纯属浪费 CPU离线批量任务、内存压力大保守策略proactiveness0必要时手动 compact_memory内存已经不够用了规整效果有限还可能加剧竞争表格里这些值并不是拍脑袋定的而是综合了内核文档和社区讨论后的保守建议。实际生产环境一定要结合你自己的业务特征去压测不要直接照抄。4.2 一套可复现的调优验证流程我在调整主动规整参数时通常会按下面的流程走一遍避免改完参数后只是“感觉变好了”但拿不出数据。第一步记录基线。先收集调整前的 buddyinfo、vmstat 和相关的延迟指标cat /proc/buddyinfo grep compact /proc/vmstat cat /sys/kernel/debug/tracing/trace /tmp/compact_before_trace.txt如果业务有现成的延迟监控同时记录下来。第二步修改参数。比如把主动规整力度调高echo 30 /proc/sys/vm/compaction_proactiveness echo 500 /proc/sys/vm/extfrag_threshold第三步观察一段时间。至少运行一两个小时让 kcompactd 有足够多的周期去执行主动规整。第四步再次采集 buddyinfo 和 vmstat对比 order-9、order-10 空闲块数量是否有提升compact_success 是否增长同时确认 kcompactd 的 CPU 占用没有失控。如果在压测环境下我还会额外做一轮 THP 分配压力测试。简单的方式是开启 THP 后频繁分配大内存观察分配延迟的 P99 变化。当然这只是一种直观验证手段严谨的性能评估还是需要结合你的实际业务流量。4.3 我在实际项目中踩过的几个坑第一个坑是盲目调高 compaction_proactiveness。我曾经在一台机器上把 proactiveness 从默认的 20 改到 80结果 kcompactd 的 CPU 占用直接冲到 30% 以上业务线程虽然没有出现明显的大页分配尖刺但整机 CPU 余量被蚕食了不少。主动规整本质上是周期性的后台劳动调得越激进后台劳动就越频繁、量越大所以必须找到一个“够用就好”的平衡点。第二个坑是手动触发 compact_memory。有一次我为了快速解决碎片问题在业务高峰期执行了 echo 1 /proc/sys/vm/compact_memory结果系统出现了明显的全局卡顿因为这是对所有节点、所有 zone 的完整规整页面迁移量大得惊人。从那以后我对手动compact_memory的态度就是离线维护和压测环境随便用线上绝对要先评估。第三个坑是把碎片指数阈值调得过低。有些人觉得 extfrag_threshold 从 500 调到 200 就能让系统更积极整理但实际上碎片指数本身就带有噪声阈值太低会导致 kcompactd 频繁被唤醒大部分规整工作其实是在扫不需要整理的 zone。真正碎片严重的时候阈值 500 已经足够触发主动规整没必要激进下调。第四个坑是忽略了内存压力对主动规整效果的影响。前面说过compaction 需要空闲页作为迁移目的地。如果系统长期处于内存水位很低的状态主动规整会陷入“隔离了页面却找不到地方迁移”的窘境表现为 compact_fail 持续增长、页面迁移数却上不去。遇到这种情况不要指望调参能解决问题先看看是不是内存真的不够了。5. 常见问题速查表5.1 高频问题问答问题原因与处理思路kcompactd CPU 占用过高多半是主动规整过于激进把 compaction_proactiveness 调低或置 0 观察也有可能是碎片指数长期偏高导致的周期性整理需要结合 buddyinfo 判断关闭 THP 后还需要主动规整吗如果业务不再有 order-9 及以上的分配需求主动规整意义不大可以设 0 省 CPU但有些驱动和文件系统仍可能触发高阶分配建议留一个低水位值碎片指数很低但大页分配还是失败碎片指数针对特定 order 计算你可能关注的是 order-9但当前 zone 整体空闲页不足另外 cgroup 内存限制也可能导致明明节点有内存、进程却分配不到主动规整和被动直接规整会同时发生吗会。主动规整只是提前整理不能保证后续一定不缺连续块分配路径上该触发直接规整还是会触发两者不互斥参数修改后需要重启吗不需要/proc/sys/vm 下的参数是运行时可调的立即生效如果想要持久化写到 /etc/sysctl.conf 或 sysctl.d 里5.2 如何快速判断当前系统的碎片状态不需要看一大坨源码最快的方法是三步走。先看 /proc/buddyinfo 里相关 order 的空闲块数量再看 /proc/vmstat 里的 compact_stall 和 compact_fail最后用 top 或 pidstat 看一眼 kcompactd 的 CPU 占用。这三个数据组合起来基本能判断大块空闲数量充足、kcompactd 低 CPU状态健康不用管。大块空闲数量少、compact_stall 在涨碎片问题正在影响业务优先检查主动规整是否开启再考虑调高 proactiveness。大块空闲数量少、compact_fail 也在涨规整做了但没用通常是内存总容量紧张先扩容或优化内存占用调参不是第一选择。kcompactd 高 CPU 但大块空闲数量没有改善主动规整做得太频繁但效果不好先降低 proactiveness再排查 memory cgroup 和迁移类型设置。我在实际操作中的体会是主动内存规整是一个非常典型的“预防优于治疗”的工程思想它不会让你的内存凭空变多也不会让碎片彻底消失但它能把最危险的延迟从分配关键路径上挪走。每次遇到大页分配抖动与其一股脑关掉 THP不如先看一眼碎片指数和 kcompactd 的行为很多时候用参数微调就能解决而不是粗暴地禁用一个本来好用的特性。