ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Linux内存碎片深度解析:从伙伴系统到页面迁移的实战整理指南

Linux内存碎片深度解析:从伙伴系统到页面迁移的实战整理指南 在服务器上跑了两年多的内存密集型业务free -h显示物理内存还剩二三十GB但一次echo 20 /proc/sys/vm/nr_hugepages就直接报“无法分配内存”操作系统日志里全是页分配失败的告警。这种“内存看起来够用但关键时刻总差一口气”的现象十有八九就是内存碎片在捣鬼。内存碎片整理这个主题很多人第一反应是“用工具整理一下就行”但实际生产环境远比这复杂。碎片问题不只是清缓存、重启进程那么简单它涉及伙伴系统的分配机制、页面迁移、迁移类型分组、大页预留策略等一系列内核底层的运作逻辑。这篇文章我会从一个长期跟服务器性能和内存调优打交道的视角把碎片产生的原因、诊断方法、主流整理方案、实操步骤和踩坑经验一次讲透。无论你是运维工程师、SRE还是做底层性能优化的开发都应该能在里面找到可以直接落地的思路。1. 碎片问题的根源内存“看似有线实则断线”1.1 伙伴系统与内存块的“秩序感”要理解碎片整理先得理解Linux内存分配的基本原则。内核伙伴系统把物理内存按order来组织order 0是4KB的单页order 1是两个连续页8KBorder n就是2的n次方个连续物理页即2^(n2) KB。当进程申请一段较大连续内存时内核会尝试从对应order或更高order的空闲块中切分。你可以把物理内存想象成一条长长的书架书页面一本一本放上去通常互相之间不需要紧挨着但如果你要找一本超厚的连续排列的书架层比如2MB的连续物理内存零散分布的书就会成为障碍。碎片化的本质就是空闲页的总量够多但它们在物理地址空间上“不连续”。这种不连续会让高order的分配请求失败即便总空闲内存很多。日常生活中常见的碎片化类比是停车场空车位很多但都是分散的单车位停不下一辆加长车。1.2 内碎片和外碎片是两码事做内存问题时最容易混淆两个概念内碎片internal fragmentation和外碎片external fragmentation。内碎片是分配单元内部的浪费比如分配了8KB但实际只用了5KB剩下3KB无法被利用。slab分配器、文件系统块分配都可能产生内碎片。这类问题通常靠调整分配粒度、改用更紧凑的数据结构解决。外碎片才是本文讨论的重头戏。外碎片指的是空闲内存总量充足但空闲的物理页块不足以满足某特定order大小的连续分配。这类问题在长时间运行的Linux服务器上尤其突出因为不同生命周期、不同迁移属性的页面会被反复分配释放逐渐交织在一起。1.3 不可移动页是碎片化的“钉子户”现代内核为了缓解碎片引入了一个重要机制按迁移类型migratetype分组管理页面。页面被划分为Unmovable不可移动、Movable可移动和Reclaimable可回收等类型。不可移动页通常由内核自身分配比如内核栈、页表、某些驱动申请的DMA内存。可移动页则主要指用户进程的匿名页和page cache它们的内容可以被重新映射、迁移到新的物理位置。理论上只要可移动页足够多内核就能通过页面迁移page migration把它们“搬走”腾出连续的物理空间给高order请求。但在实际运行中如果不可移动页散布在物理内存的各个区域就像装修时把承重墙建得到处都是再怎样搬动家具也很难腾出大空间。这种“钉死”的页面一旦比例过高compact内存压缩效果就会大打折扣。2. 不诊断就动手等于盲人摸象2.1 /proc/buddyinfo是碎片的“照妖镜”判断系统是否存在内存碎片第一件事就是看 /proc/buddyinfo。这个文件的每一行代表一个NUMA节点下的一个内存zone列出order 0到order 10的空闲块数量。举个例子Node 0, zone Normal, 12345 4321 876 234 67 21 8 4 2 1 0如果最后的order 9、order 10列是0而前面的order 0、order 1列数字很大就说明这个zone里小块内存很多但大块连续内存已经枯竭。即使free显示还有大量空闲高order申请一样会失败。这条信息非常关键因为它能直观告诉你“碎片化的严重程度”。诊断时建议对比长时间的数据变化。单看一次快照往往不够要连续记录几天看大块连续内存的数量是否持续下降。比如某台机器刚启动时order 10列可能有几十个块运行一周后变成个位数运行一个月后直接归零这就是碎片化的典型演进过程。2.2 /proc/pagetypeinfo揭示钉子户分布光看buddyinfo只能知道“有没有大块”但不知道“是谁挡住了大块的形成”。这时候要查 /proc/pagetypeinfo它会按迁移类型列出各order的空闲块数。重点关注Unmovable列在低order区域是否堆积了大量页面。如果Unmovable页面数很多并且分散在各个order区间那即使你触发compact效果也不会理想因为不可移动页是搬不动的。除了这两个文件还可以参考 /proc/vmstat里compact相关的计数。比如compact_stall表示因内存碎片导致的高order分配等待次数compact_fail表示压缩操作的失败次数。这些数字持续增长说明碎片已经影响到了实际分配路径。内核还有个extfrag索引位于debugfs的/sys/kernel/debug/extfrag/extfrag_index它用0到1之间的值表示每个order的碎片程度越接近1说明碎片越严重。2.3 确认需求你真的需要大块连续内存吗在我的经验里很多人看到buddyinfo一堆零就着急开整但忘记先问一个问题当前业务到底是否需要高order的连续物理内存如果业务主要是普通的小块内存分配order 3以下基本够用碎片再严重也不会有明显体感。相反如果业务涉及透明大页THP、HugeTLB大页、RDMA、DPDK、GPU显存映射或者虚拟机热迁移就会对连续物理内存有硬性要求。诊断的第一步应该是“按需诊断”先明确谁能消费大块连续内存再判断碎片是否已经拖累了它。否则你整理了半天业务该怎样还是怎样。3. 主流碎片整理方案逐个拆解3.1 内核内存压缩compact_memory手动触发Linux内核提供了一套内存压缩机制核心思路是把可移动页向zone的一端迁移把空闲页集中到另一端从而形成大的连续空闲区域。这套机制通常由内核在分配路径中自动触发直接压缩或后台kcompactd也支持手动触发echo 1 /proc/sys/vm/compact_memory手动触发后系统会对所有zone执行同步压缩。优点是立即见效适合在业务低峰期进行人工干预缺点是同步压缩过程可能消耗较多CPU而且如果不可移动页比例太高压缩会很快遇到“搬不动”的瓶颈。我实测过一台64GB内存的虚机触发compact时单核CPU短时间打满业务响应出现可感知的延迟抖动所以不建议在高峰期执行。3.2 缓存回收drop_caches只能解决一部分问题很多人把碎片整理等同于清缓存执行echo 3 /proc/sys/vm/drop_caches注意drop_caches的作用是回收page cache、dentry和inode缓存它释放的是Reclaimable页面。这些页面回收之后对应的物理块自然变成空闲块。如果这些空闲块恰好能拼接成大块碎片就会缓解但drop_caches本身并不具备“移动”页面的能力。如果空出来的块依然和不可移动页交织碎片并不会消失。也就是说drop_caches是清理战场不是重整队形。它有用但千万别把它当成碎片整理的银弹。它最大的副作用是缓存回收后短期内IO性能可能下降因为热数据需要重新从磁盘读取。3.3 透明大页与khugepaged主动折叠出大块THP透明大页的初衷是让用户进程的2MB大页透明化使用减少TLB miss。内核有一个khugepaged内核线程会在后台扫描可移动页面尝试把连续的512个4KB页面折叠成一个大页即物理连续的2MB这个过程可以视为一种“主动碎片整理”。THP更适合堆内存较大、且对延迟不敏感的应用场景。但对某些低延迟服务而言THP的khugepaged扫描和页面迁移反而可能引入额外开销甚至导致单次访问延迟飚升。数据库、缓存类应用比如Redis、MySQL在关闭THP时通常运行更稳定这是业界踩过很多坑后形成的共识。如果你的业务适合THP建议把/sys/kernel/mm/transparent_hugepage/enabled设为always或madvise并观察khugepaged的CPU占用。若发现碎片问题突出还可以手动触发echo 1 /sys/kernel/mm/transparent_hugepage/khugepaged/defrag但注意THP的折叠需要找到合适的连续物理页碎片严重时折叠失败率会很高khugepaged会反复尝试反而消耗CPU。这时候优先解决底层碎片问题而不是让THP硬扛。3.4 预留方案把大块连续内存圈出来有句话叫“最好的防守是进攻”对内存碎片来说最好的整理是在启动阶段就预留好连续内存。两个常见手段HugeTLB大页预留和CMA预留。HugeTLB预留是在内核启动参数或运行时设置vm.nr_hugepages预先分配固定数量的2MB或1GB大页。这些大页一旦预留就不再参与普通页面的流动不会被碎片化。但问题在于预留需要系统在启动早期或内存充足时找到足够的连续区域如果系统已经碎片化预留也可能失败。我遇到过一个真实案例一台64GB内存的机器业务需要80个2MB大页启动时预留没问题但运行一周后尝试热扩充大页就怎么都分配不出来原因就是运行期内存碎片已经形成。CMAContiguous Memory Allocator则是通过内核启动参数cma预留一块内存区域平时允许普通可移动页面使用在设备需要连续DMA内存时再临时迁移页面腾出空间。它更适合驱动、多媒体、显示等场景。启动参数示例cma128MCMA的好处是平时不浪费内存需要时又能保证连续内存缺点是页面迁移的实时开销可能影响性能而且如果CMA区域内的可移动页是活跃的迁移过程会有延迟。3.5 进阶玩法kernelcore/movablecore调节页面分配比例Linux内核提供了内存区域布局参数可以在一定程度上控制碎片化风险。通过向内核传递memmap参数和kernelcore/movablecore可以把一部分物理内存设计为可移动区域。简单来说movablecore会让指定比例的内存区域只分配给可移动页面不可移动页面不会散落到可移动区这样即使系统长期运行可移动区域内部也难以被“钉子户”破坏。不过这个方案需要重启服务器并且参数配置不好可能导致某些驱动无法分配到内存所以一般只在做容量规划时考虑。生产环境临时调优时用这个方案成本较高更适合新装机或大规模集群统一规划。3.6 方案对比速查表方案原理适用场景副作用执行成本compact_memory页面迁移整合空闲块运行期手动整理CPU尖峰、延迟抖动低drop_caches回收页缓存缓存过多且碎片严重短期内IO性能下降低THP/khugepaged后台折叠大页适合大内存服务扫描开销、延迟波动中HugeTLB预留启动期预留连续页需要稳定大页的场景内存无法按需弹性使用中CMA预留按需迁移腾连续区驱动/多媒体/DMA迁移时开销中kernelcore/movablecore内存区域类型划分新装机规划需重启、需谨慎评估高实战中我通常不单独依赖一种方式而是组合使用先诊断碎片来源再用compact_memory做短期整理同时启动期预留大页或CMA作为长期保障。这样既解燃眉之急又避免碎片问题周而复始。4. 完整实操从诊断到整理再到验证4.1 先做完整基线记录动手整理前先记录以下信息# 查看内存总量和空闲情况 free -h # 记录伙伴系统各order的空闲块分布 cat /proc/buddyinfo # 记录分迁移类型的页块分布 cat /proc/pagetypeinfo # 查看碎片相关事件计数 cat /proc/vmstat | grep -E compact|thp # 记录当前大页情况如果相关 cat /proc/meminfo | grep -i huge这些数据是后续对比的基准。没有基线你无法判断一次整理到底有没有效果。我习惯把这些输出重定向到一个日志文件命名带时间戳方便反复对比。尤其是buddyinfo的变化趋势比单次输出更有说服力。4.2 判断碎片是否影响业务如果机器上没有需要连续大页的业务我认为没必要主动做碎片整理。但如果业务确实在申请大页或高order内存通过日志或strace能看到分配失败的痕迹就需要系统性排查。例如某超融合平台上的虚拟机热迁移场景内存页迁移需要目标机有足够连续内存目标机碎片化会直接导致迁移失败或迁移性能下降。这时候整理本地内存碎片是很有价值的。确认业务受影响后再进入整理环节。注意所有高风险操作前建议先检查当前负载用uptime看load average用mpstat -P ALL 1看单核使用率在CPU相对空闲时进行整理。4.3 执行碎片整理操作对运行中的系统我推荐的分步操作如下第一步回收可回收的缓存给后续压缩腾出余地sync echo 1 /proc/sys/vm/drop_caches为什么先把drop_caches放前面因为回收page cache是一个相对“便宜”的操作许多空闲块是缓存的页面回收后空出的块可以被compact利用。如果缓存特别大比如上百GB这一步往往能让order 5以上的空闲块数量明显上升。第二步执行内存压缩echo 1 /proc/sys/vm/compact_memory这一步的目的是把分散的空闲块重新整合。压缩过程会遍历zone内的页面尝试把可移动页面迁移到一端。执行完后立即再看buddyinfo高order列的空闲块数通常会有所上升。第三步如果需要大量大页且系统内存充足再尝试设置大页预留sysctl -w vm.nr_hugepages80如果一次设置太多导致失败可以分步增加。比如要预留80个2MB大页可以先设40稳定后再设80。这样每次分配时系统找到连续2MB空间的概率更高失败率会降低。注意如果内存总空闲不多或者碎片过于顽固这一步仍然可能失败不要强行调大。4.4 验证整理效果与侧写操作完不要急着收工再看一次关键数据cat /proc/buddyinfo cat /proc/vmstat | grep compact重点看order 5以上即128KB以上连续块数量是否上升以及compact_stall、compact_fail这类事件是否停止增长。拿一次真实案例举例某虚拟化宿主机在整理前order 92MB的空闲块数为0order 81MB为3执行drop_caches compact后order 9变为18order 8变为45同时那段时间内hypervisor创建新虚拟机时分配2MB大页的失败日志再没出现。这就达到了整理目的。同时要注意观察整理后一段时间的内存变化趋势。碎片整理不是一次性的运行几周后碎片可能再次累积。这时可以加入周期性的cron任务在业务低峰期定期执行compact_memory。例如凌晨4点执行一次30 4 * * * sync echo 1 /proc/sys/vm/drop_caches echo 1 /proc/sys/vm/compact_memory但要留意如果drop_caches导致业务的热数据在白天频繁回源磁盘这种定时任务反而得不偿失。更稳妥的做法是只做compact不刻意清缓存。4.5 长期解决方案的系统化落地运行期整理只是“事后补救”长期来看应该从部署层面规避碎片化新服务器上线时评估业务是否需要大页。需要的话建议在装机阶段就预留给定数量的HugeTLB或cma而不是运行后再热分配。在虚拟化、容器平台中如果可以配置内存的movable分区应该尽量把客户机内存放到可移动区域避免内核态的不可移动页混入客户机内存区域。对高order分配频繁的业务如DPDK、SPDK强烈建议在系统启动命令中预留大页并且预留量要略高于业务峰值需求。定期监控buddyinfo和compact计数把碎片指标纳入监控大盘。这里说的长期方案有一定的部署期成本但对碎片问题反复出现的环境绝对是性价比最高的做法。5. 常见问题与实战避坑5.1 为什么我执行完compact碎片依旧严重这是出现频率最高的问题。原因通常有两个一是系统内不可移动页太多compact把能搬的都搬了但不可移动页像钉子一样把区域切开无法形成大块连续区二是内存本身已经极度紧张各order空闲块都不足这时候再压缩也腾不出空间。前者需要用pagetypeinfo确认不可移动页的比例和分布后者则需要先扩容或通过内核的zram、内存回收机制释放压力。我在一台高压力数据库服务器上就遇到过这种情况内存占用长期维持在90%以上drop_caches后空闲块才刚够满足order 5的需求但要order 10的2MB以上大块就非常困难。这本质上已经不是碎片问题而是内存总量瓶颈的伴生问题。5.2 drop_caches3真的把所有缓存都清干净了吗不是。echo 3只清理page cache、dentry和inode缓存对匿名内存进程堆栈无效对tmpfs、共享内存也不起作用。而且清理缓存后内核可能在很短时间内重新填充缓存尤其是文件服务器或数据库热点读场景缓存恢复速度惊人碎片状况并不会根本性改善。所以使用drop_caches要看清目标它解决的是“可回收页过多导致可用连续块少”的问题不是“物理布局杂乱”的问题。5.3 THP到底开还是不开这是个很经典的问题。THP的收益在大内存场景下很明显能减少TLB miss。但缺陷是khugepaged的折叠过程会引入瞬时延迟且需要连续物理页碎片严重时它会更“勤奋”地扫描重试白白消耗CPU。如果你的业务本身有比较明显的长尾延迟要求比如RPC服务、网关、数据库我倾向在系统层面把THP设为madvise业务自己按需开启。这样既能控制风险又能给确有需要的应用留出选择权。判断是否需要关闭THP最快的方法是看业务监控里的最大延迟和99分位延迟。如果开启THP期间P99涨了而P50没变大概率是khugepaged或页面迁移的贡献这时关闭THP再观察往往立竿见影。5.4 大页预留总是失败怎么办大页预留失败的最直接原因就是内存碎片化严重找不到指定大小的连续物理内存。遇到这种情况不要硬试一个很大的nr_hugepages改用“少量多次”的方式逐步加大。比如目标是1024个2MB大页每次增加128个等系统稳定后再次增加这样分配器每次只需要寻找少量连续2MB区域压力小很多。同时可以优先执行一次compact_memory再逐步预留。因为compact之后zone内连续大块的数量会增加预留成功率会显著提高。如果还是失败可能需要考虑重启服务并修改启动参数在boot阶段完成预留。5.5 容器/虚拟机里的碎片问题怎么处理容器和虚拟机场景存在一个附加问题你在容器里看到的buddyinfo是宿主机的视角。容器无法直接整理宿主机的内存宿主机碎片化问题需要由宿主机运维处理。虚拟机内部也是如此你只能在客户机内做有限的整理真正的物理内存连续性依赖宿主机层的策略。云上虚拟机如果确实需要连续的物理内存比如使用SR-IOV、RDMA务必要在创建时就规划好大页预留并在宿主机层面监测碎片的演进。不要把虚拟机内的内存整理当成全部宿主机层的NUMA亲和性和内存区域布局才是决定因素之一。5.6 应用层堆碎片也不容忽视最后提一个容易混淆的话题很多“内存碎片整理”的搜索其实是针对应用进程的堆碎片的。glibc的malloc在分配和释放不同大小的对象时堆内空闲块会变得零碎导致即使进程RSS很高实际有效使用内存不多。这类问题的解决思路和内核不一样通常需要使用jemalloc或tcmalloc替代glibc malloc通过更精细的分配分区降低碎片率。调整glibc的M_MMAP_THRESHOLD让大块分配直接走mmap释放后归还不连续的风险区域。对Java类应用关闭CMS、使用G1或ZGC等具备内存规整能力的GC算法。所以在动手“整理内存碎片”之前一定要区分清楚你说的是内核物理内存碎片还是进程堆碎片。两者症状有相似之处但手段完全不同搞错方向会浪费时间。写在最后的一些经验碎片整理这件事我个人的体会是“三分整理七分预防”。你可以在运行期用compact和drop_caches做一次漂亮的急救但如果业务的原始需求是频繁申请大块内存那我建议在规划阶段就把HugeTLB预留、CMA、movable参数这些长期手段想清楚。碎片问题最可怕的地方在于它的隐性——系统看起来一切正常一直到某次大页分配失败才暴露出来届时往往已经是业务受损的现场。实际维护中我会建议每台重要服务器都把buddyinfo和compact计数纳入日常巡检一周看一次趋势。有一次我就是从监控里发现一台数据库宿主机的order 8连续块数量两周内从64降到6提前把大页预留的计划做了调整避免了后续应用迁移时的分配失败。这种“看得见趋势”的习惯比任何一键整理的脚本都管用得多。另外工具层面我强烈建议优先使用系统自带的手段不要轻易引入第三方内核模块或者商业的“整理加速”服务。内核的内存管理机制已经相当成熟大部分问题用原生机制都能解决第三方工具反而可能引入稳定性风险。最后再加一句所有需要修改内核参数的操作尽量先在测试环境完整跑一遍流程尤其是drop_caches和compact同时触发的场景确认不影响业务再上生产。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进