ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

内存碎片导致大块分配失败?Linux内核反碎片机制与实战排查方案

2026/10/4 13:43:45 拓冰建站 浏览量
内存碎片导致大块分配失败?Linux内核反碎片机制与实战排查方案 线上服务跑了两三个月突然收到告警应用层申请2MB连续内存失败磁盘IO队列跟着飙高。查了一圈内存总量充足剩余几百MB但就是拿不出一块干净的连续区域。如果你也遇到过这种诡异现象多半就是内存碎片在捣乱。碎片整理不是新概念现代操作系统内核里早就有整套机制在背后兜底但很多做应用开发的同学对它只有一个模糊印象真出问题时不知道从哪开始排查更不知道出手方案该怎么定。这篇内容我会从碎片问题的本质讲起把底层反碎片机制的工作原理拆开再给出一套可以落地的碎片整理方案设计思路最后整理实际过程中最容易踩的坑。适合正在处理长期运行服务内存问题的人也适合想真正理解内核内存管理这块拼图的开发者。就算你暂时不需要亲自改内核策略弄懂这套逻辑之后再遇到类似问题至少知道该去哪里看、该怎么动手。1. 先搞清楚你要打的是哪种碎片1.1 内部碎片与外部碎片别混为一谈很多讨论把内存碎片当成一个词来讲但实际处理问题时必须区分两类。内部碎片是分配器给了你一块比你实际需求更大的内存多出来的那一截用不上造成浪费。比如你只想存100字节但分配器按照128字节的粒度给你那28字节就是内部碎片。这种碎片通常是固定大小分配器比如slab的产物特点是单块浪费少但累积起来也很可观。外部碎片是更棘手的那一种。内存被拆成很多小块每一块单独看都有人占用但这些小块之间留下的空隙太小容纳不下一个大块请求。打个比方停车场还剩10个车位但分散在四个角落每处只有两三个连续空位这时候来了一辆需要占领整个角落的大巴车停不进去。外部碎片才是导致内存明明有余量却分配不出大块连续空间的元凶。两者应对思路完全不同。内部碎片靠调整分配粒度、合并对象来治理外部碎片靠内存迁移和重排来解决。做方案之前先把现象定位清楚是大量小对象累积导致slab膨胀还是连续页块被拆得七零八落方向搞错了后面全白搭。1.2 外部碎片什么时候才真正致命外部碎片不是时刻都有害。短暂的内存交错分配很快会被释放系统在运行中自发恢复。但有几类场景会持续恶化需要主动介入第一是长期运行的服务型进程。分配释放的路径常年不重合有的对象从启动活到退出比如连接池、全局配置缓存它们占据的内存把可回收区域切成了一块块孤岛。第二是有大块连续内存需求的工作负载。比如大页HugeTLB分配、某些网卡驱动的DMA描述符、虚拟机内存热插拔动辄要几MB甚至几GB的物理连续区域。这类申请对碎片最敏感系统里剩下的内存再多凑不齐连续的照样宣告失败。第三是NUMA架构下某个节点的局部需求。就算整机内存充裕如果某个NUMA节点上碎片严重绑定在该节点的进程照样分配困难。判断是否有碎片问题最直观的手段是读内核为开发者准备的接口。看/proc/buddyinfo能观察到每个内存节点各个order上还有多少空闲块。我看过最典型的碎片机器order-0的空闲页很多但order-3以上全部是0这就是典型的小块很多、大块没有外部碎片已经相当严重了。再配合/proc/pagetypeinfo看不同迁移类型页面的分布能进一步定位碎片主要是被哪种页面切碎的。这两份数据是判断要不要做整理的起点建议先学会看再谈方案。2. 碎片整理的底层思路移动内存而不是物理压缩2.1 Buddy分配器的合并机制与被打破的条件要理解碎片整理绕不开buddy分配器。内核把物理内存按照2的幂次拆成不同大小的块每一层叫一个order。释放内存时内核会尝试把相邻的兄弟块合并成更大的块。这个设计本身自带一定的反碎片效果理想情况下空闲块可以从小合并到大满足各种尺寸需求。但合并有一个前提相邻的两个块必须都处于空闲状态。只要有一边还被人占着另外一边再空也合并不了。想象两扇房门对开想拼成一个大房间前提是两边都腾空只要一边堆满杂物另一边再空也没戏。长年累月的随机分配释放之后各个order的空闲块被大量占用中的页面隔开合并路径被切断这就是碎片产生的根因。注意到没有问题是占用中的页面导致的。所以解法自然而然就出来了如果我们能把这些占用中的页面挪走让空闲块凑到一块合并的条件就能重新满足。2.2 页面迁移机制碎片整理的引擎移动一个页面的物理位置最核心的机制是页迁移page migration。整体迁移流程分四步先把目标页面的内容复制到一个空闲页上然后修改页表和反向映射让所有访问旧页面的虚拟地址都指向新页面最后释放旧页面。这套机制早期是为了NUMA内存均衡设计的后来被顺理成章地复用到了碎片整理memory compaction上。迁移不是所有页面都能做的。内核把页面按迁移类型分成三类可移动movable、可回收reclaimable、不可移动unmovable。用户态进程的匿名页和页缓存属于可移动因为只要改页表就能重定位内核自身的数据结构、某些驱动分配的内存属于不可移动因为它们直接依赖物理地址。还有一类可回收页面可以先写回再释放也能帮助腾出空间。碎片整理的核心思路就是把不可移动的页面尽量集中到内存区域的一端把可移动页面逐个迁走让另一端腾出连续的大块空闲区。这个过程不需要任何物理压缩靠的是逻辑层面的地址重映射。2.3 为什么不能用内存压缩的思路来理解我见过不少同学把碎片整理想象成类似磁盘碎片整理的做法——把数据搬来搬去在物理上重新排列。磁盘上搬数据是合理的因为磁盘扇区寻址没有额外的转换开销。内存不同CPU访问的永远是虚拟地址页表把虚拟地址映射到物理地址。所以对内存来说只要页表改一下数据的物理位置怎么变都无所谓这才是迁移可行性的根基。理论上也可以靠压缩比如把多个小对象的内容复制拼接省出整块区域。但这对内核来说成本太高压缩意味着大量数据拷贝还要处理对象内部的指针引用关系一旦碰上对象内部有自引用指针压缩逻辑就会变得极其复杂。页迁移只用改页表和反向映射不需要理解数据内容成本低且通用性强。所以现实里所有的碎片整理方案都是迁移导向的不是压缩导向的。3. 实操设计一套能落地的碎片整理方案3.1 方案选型与目标定义先行动手之前先把目标量化。碎片严重程度没有统一的国家标准但对你的业务来说可以定义一个具体的成功指标。例如order-464KB以上连续空闲块至少恢复到N个连续运行30天后仍能成功分配2MB连续内存。没有这个目标整理做得再热闹也无法判断效果。接下来做场景选型。如果是操作系统内核态的问题可以选择现有的comaction机制调整触发参数如果问题出在应用层更适合从内存分配模式上做文章实现内存池或对象复用。一个完整的方案往往是自上而下的组合拳内核侧保证底层可移动性应用侧减少碎片产生的频率。区分一个关键点你的方案是预防性的还是补救性的。预防性方案通过控制页面迁移类型分布让内存区域天然不易碎补救性方案在碎片已经形成后主动触发整理。成熟系统一般两条腿走路因为单纯预防挡不住极端负载。3.2 核心流程设计与关键参数以下是一套参考性的碎片整理实操流程已在x86_64 Linux环境验证过第一步采集基线数据。先跑一段时间的性能压测同时记录/proc/buddyinfo、/proc/pagetypeinfo、/proc/pressure/memory数据观察碎片增长曲线。这一步能确定碎片增长速率和高峰期避免后续方案被偶发现象误导。第二步评估页面迁移类型分布。重点看不可移动页面是否过度分散。如果大量不可移动页面散布在各处那么仅仅靠整理可移动页面并不够你需要从源头减少不可移动页面的分配或者更加激进地规整内核分配。第三步触发整理。内核的memory compaction设备接口可以通过向/proc/sys/vm/compact_memory写入1触发全内存整理。生产环境更常用的是设置/proc/sys/vm/compact_memory配合watermark和extfrag_threshold参数让系统在分配失败风险升高时自动处理。需要注意的是触发方式的选择直接影响整理代价。同步整理发生在进程分配页面的关键路径上如果碎片严重一次整理可能耗时几十毫秒甚至更多对延迟敏感的服务是灾难。异步整理比如通过内核线程在后台执行对业务影响小得多但效果来得慢。我建议设计成混合模式常规负载下靠后台异步整理保持水位内存分配失败时再降级为同步整理兜底。第四步验证与回归。整理完成后重新测一次大块分配通过buddyinfo确认order分布改善同时观察业务延迟是否出现毛刺。整理操作本身会消耗CPU和迁移页面的额外内存开销如果这类开销超过收益就要调整整理频率或触发条件。3.3 移动哪些页面冷热分离与代价评估做页面迁移时不是所有可移动页面都值得搬。热页面频繁访问的页面迁一次会产生大量TLB冲刷后续访问还可能产生额外的NUMA跳变。冷页面长时间不被访问的迁移代价相对低得多。所以实际方案里应该引入冷热判断逻辑。最简单的做法是看页表里的访问位Accessed bit定期清理并重新标记一段时间后仍未被访问的页面就是冷页。把冷页作为优先迁移目标大幅降低迁移的副作用。另一个代价点是目标页面的来源。整理过程中需要一个中转站——把原页面内容复制过去的新空闲页。如果系统本身空闲内存不足迁移就会卡住。有些实现里会把可回收页面先回收一部分腾出迁移空间这就是先回收再迁移的流程设计。从数据看迁移100个4KB页面大概产生400KB的拷贝量这在GB级内存面前微不足道但TLB刷新的影响不能只看数据量。所以高核心数、大内存机器上我习惯把迁移批次限制在256到512个页面之间分多批完成避免一次性冲刷大量TLB。3.4 应用层辅助手段内存池与大页预留光靠内核整理等于总是事后救火。长期运行的服务应该在前端降低碎片概率。最有效的做法是内存池化把频繁分配释放的对象集中管理减少对内核分配器的压力。连接缓冲、请求上下文这些对象本来就是复用式生命周期用内存池之后对象数量稳定内核页面的分配释放模式也趋于稳定碎片生成率会显著下降。还有一些场景适合直接使用大页。比如数据库的共享内存、虚拟机的Guest内存本身就需要大段连续空间。对这种场景与其等碎片形成后再整理不如在系统启动早期就预留大页内存。启动时内存还很干净预留成本最低。HugeTLB和透明大页THP在路径上有区别HugeTLB是显式预留可靠性高THP是后台透明处理有分裂风险。如果业务可以接受我建议优先考虑HugeTLB预留。有个容易被忽略的经验应用启动时不要一次性申请大量内存再慢慢释放这会在低地址区留下大片不可移动页。相反可以模仿内核的批量申请、分批使用、整块释放模式让峰值内存需求以拍平曲线的方式出现。4. 常见问题与排查技巧实录4.1 碎片整理中的典型问题速查下面这些情况是我在真实环境和社区案例里反复见到的整理成一个速查表方便对照。现象可能原因排查路径常用解法内存总量充足但大块分配失败外部碎片严重看/proc/buddyinfo各order计数触发compaction调整迁移类型分布整理后效果短暂很快恢复碎片产生碎片的源头未治理监控分配释放频率定位高频热点应用层内存池化调整分配策略同步整理导致延迟尖刺关键路径上触发同步compaction检查dmesg和延迟监控调异步整理提高触发阈值迁移大量页面后CPU飙升热页被反复迁移检查页访问位确认迁移对象冷热冷热分离策略只迁移冷页整理过程中临时内存不足没有预留中转页观察nr_free_pages与watermark先回收可回收页预留迁移缓冲freepages高但大块仍不足低order空闲页过多统计各order空闲块数量调整min_free_kbytes和extfrag_threshold4.2 我踩过的几个坑提前帮你避开第一个坑是盲目调低extfrag_threshold。这个参数用来控制当空闲块碎片程度达到多少时允许直接申请更高order页面。把它调低确实能增加大块分配成功率但会让内核把小于目标块大小的空闲块视为不可用导致部分内存实际被搁置可用内存下降。建议按百分比逐步调整每次5%观察业务指标而不是只看分配成功率。第二个坑是忽略了cgroup的内存管理。很多容器环境里碎片问题不止发生在全局更发生在cgroup的内部。你看到宿主机buddyinfo还好好的但容器里面分配不了大块内存。这种情况要先确认cgroup的memory.high/memory.max限制以及cgroup内页面迁移的约束。只盯着全局做整理方案可能没法解决容器内问题。第三个坑是不做页面迁移率监控。整理做完了你只知道内存块数变了但不知道期间迁移了多少页、失败了多少次。这些数据直接反映方案的代价和有效性。统计迁移成功率可以用/sys/kernel/debug/compaction/下的接口或自定义内核探针。没有这套监控你的方案就是盲人摸象。第四个坑是内核分配不可移动页导致的碎片顽固。用户态的匿名页能迁走但内核态页面比如页表本身、内核栈、驱动分配的DMA缓冲区迁不动。这类页面如果散布在低地址区域整理效果直接减半。我用过的办法是给关键驱动提前预留内存或写简单的内核模块跟踪不可移动页面的物理分布在分配时尽量往固定区域引导。第五个坑是忽略THP的碎化效应。透明大页在内存压力下会分裂成小页这些小页在不同时间点被部分释放会留下很碎的空隙。如果业务确实需要THP可以用madvise在关键路径上显式提示让内核只在可控场景下使用大页避免全局统一开启的策略带来难以预料的碎片行为。4.3 一个小技巧碎片程度的计算方式很多时候需要把碎片数字化方便监控和告警。业界常用一个简单指标碎片指数fragmentation index公式是1 - (可用连续大块数 * 大块大小) / 总空闲内存。Linux的/proc/pagetypeinfo里可以读到block_unusable和block_pfn的计数通过它们也能计算某个order级别的碎片程度。一个更朴素的判断方式是看buddyinfo的分布形态。如果order-0到order-3有大量空闲页而order-6以上几乎为0一定存在碎片问题。如果所有order都有分布说明碎片还没有严重影响大块分配。我个人习惯把这一形态做成可视化曲线随着时间轴滚动碎片趋势一目了然。线上稳定运行之后可以配上简单的告警规则当系统连续N次order-4分配失败或compact_failed计数增长超过阈值时触发告警。有了这个监控网就不需要靠用户报障才知道碎片问题了。结尾收个尾说点掏心窝的话我经手过的内存碎片问题有一半其实不是内核的锅而是应用层的分配模式太杂乱。碎片整理这个方案从机制上很强内核能帮你移动页面、规整空间但它阻止不了业务代码每秒钟创建上千个生命周期错乱的临时对象。真正的治理不是把宝全押在整理动作上而是让整理成为兜底从源头削减碎片才是长久之计。如果你现在正被内存碎片搞得头疼我的建议是先别急着改参数。花一周时间把/proc/buddyinfo和/proc/pagetypeinfo的数据收集起来把业务分配模式摸清楚理解碎片从哪来、何时最严重。等数据出来了答案往往自己就浮出表面。这份方案里的思路和参数不是万能药但它可以帮你少走弯路给你的排查路径提供一个可靠的起点。