Linux 内存直接压缩(Direct Compaction)延迟与页缓存治理
Linux 内存直接压缩(Direct Compaction)延迟与页缓存治理

在运行超低延迟核心服务(如金融交易结算、高频撮合引擎、分布式网关 Envoy)的 Linux 宿主机上,工程师在排查 P99/P999 响应延迟偶发毛刺时,经常会陷入一种令人极其困惑的“幽灵卡顿”境地:
- 监控大盘显示:CPU 利用率只有 30%,网络无丢包,数据库响应也在 1 毫秒以内;
- 查看宿主机内存,
free -m显示物理空闲内存(free+available)整整还有 25GB 以上的充裕空间! - 然而,运行在该节点上的 Java / Go 应用程序在执行普通的内存分配(如
new byte[]或malloc)时,偶发性地会出现长达 200ms 到 1200ms 的剧烈停顿!
在物理内存明明极其宽裕的情况下,为什么操作系统在分配内存时会发生长达整整一秒的“瞬间假死”?
本文深入剖析 Linux 内核内存子系统伙伴系统(Buddy System)、内存碎片化(Memory Fragmentation)与直接内存压缩(Direct Compaction,内核指标 compact_stall) 的深层机理,并给出生产级异步主动碎片整理与页缓存(Page Cache)水位的调优基准方案。
深入底层:为什么有 25GB 空闲内存依然会触发“直接压缩”?
要理解直接内存压缩的死锁机理,必须看清 Linux 内核物理内存分配的两个核心概念:
[ Linux 伙伴系统 (Buddy System: 阶数 Order 0 到 Order 10) ]
- Order 0: 4KB 单个物理页
- Order 1: 8KB (2 个连续页)
- Order 2: 16KB (4 个连续页)
...
- Order 9: 2MB (512 个物理连续页 - 生产大页与大数组必需!)
当系统长期运行并经历了海量网络 I/O、磁盘 Page Cache 读写与小对象创建后,物理内存中会产生极其严重的**“外部碎片化(External Fragmentation)”:
虽然把所有散落在各处的 4KB 小空闲页加起来足足有 25GB,但整台宿主机上却连一块完整的、连续的 2MB(Order 9)大物理页都找不到!**
[ 应用程序调用 malloc() 申请 2MB 连续物理内存 ]
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 1. 伙伴系统扫描: 发现 Order 9 连续物理页块为 0! │
├─────────────────────────────────────────────────────────────┤
│ 2. 触发内核直接内存压缩 (Direct Compaction / compact_stall)│
│ - 💥 致命停顿点: 内核强制挂起当前用户态申请内存的线程! │
│ - 内核在 CPU 上开始地毯式扫描物理内存: │
│ 将各个分散已分配的页逐个迁移、拷贝、拼接为连续大块 │
│ - 整个迁移过程耗时长达 200ms ~ 1000ms! │
│ - 应用程序被无情挂起,全网接口爆发严重的超时毛刺! │
└─────────────────────────────────────────────────────────────┘
此时如果开启了透明大页(THP)或者应用需要大块连续内存,内核无法从伙伴系统中直接分配,被迫唤醒 直接内存压缩(Direct Compaction) 机制。
申请内存的业务线程被内核强制转入不可中断的 D 状态,必须老老实实等待内核在物理内存中完成数万个小页的搬迁与拷贝。这就是那长达 1 秒时延毛刺的物理真相!
现场诊断与排查指令实战
第一步:检查宿主机伙伴系统内存碎片化分布(buddyinfo)
# 查看宿主机各 CPU NUMA 节点在不同阶数 (Order 0~10) 上的空闲页分布
cat /proc/buddyinfo
# 典型严重碎片化输出:
# Node 0, zone Normal 182400 42000 12000 140 2 0 0 0 0 0 0
- 深度诊断:
前几列(Order 02,4KB16KB 小页)有数十万个,但从 Order 5 开始数字几乎归零(Order 9 连续 2MB 大页数量直接为 0!)。这表明宿主机物理内存已经千疮百孔,外部碎片化率极高!
第二步:检查内核直接内存压缩停顿计数器(compact_stall)
# 查看内核时序统计中因直接压缩引发的停顿次数
grep -E "compact_stall|compact_fail|compact_success" /proc/vmstat
# 典型故障输出:
# compact_stall 148200 <-- 发生了整整 14 万次用户态线程被内核挂起等待内存压缩!
# compact_fail 94200 <-- 甚至很多次直接压缩耗费了 CPU 却依然失败!
若 compact_stall 正在随时间单调持续递增,证明系统正遭受着直接内存压缩的严重反噬。
生产级根治调优三步法
为了彻底消灭直接内存压缩引发的毛刺,我们必须将“被动、阻塞式的同步压缩”转变为**“后台平滑、主动式的异步预先整理(Proactive Compaction)”**。
步骤一:开启 Linux 内核主动碎片整理(compaction_proactiveness)
在 Linux 5.0+ / CentOS 8+ / RHEL 9 中,引入了主动内存整理参数:
# /etc/sysctl.d/99-memory-compaction.conf
# 开启内核主动后台碎片整理 (默认 20,调大至 50 保持低碎片率)
vm.compaction_proactiveness = 50
# 提高内存回收低水位线,给内核预留充裕的连续物理大页缓冲区 (默认 10 -> 调至 200)
vm.watermark_scale_factor = 200
# 优化内存分配低水位线计算
vm.min_free_kbytes = 2097152 # 锁定 2GB 作为内核物理应急底线
vm.watermark_scale_factor = 200:
将每个内存 Zone 的low水位线与min水位线之间的距离扩大 20 倍。
这使得后台内存回收守护进程kswapd能够在内存碎片刚刚出现苗头时,提前在后台异步平滑整理,坚决不让申请内存的业务前台线程遭遇哪怕 1 毫秒的compact_stall同步挂起!
步骤二:彻底关闭运行时透明大页(THP)
在宿主机初始化脚本中彻底封死透明大页:
# 运行时彻底关闭透明大页与碎片整理扫描
echo "never" > /sys/kernel/mm/transparent_hugepage/enabled
echo "never" > /sys/kernel/mm/transparent_hugepage/defrag
步骤三:治理 Page Cache 脏页占用,防止页缓存抢占大块内存
# 限制脏页后台刷盘阈值,保持内存轻盈
vm.dirty_background_bytes = 268435456 # 256MB 立即平滑回写
vm.dirty_bytes = 1073741824 # 1GB 触发硬限制
执行 sysctl -p /etc/sysctl.d/99-memory-compaction.conf 全局生效。
治理成效实测验证
在全集群推行主动碎片整理与水位线调优后:
- 宿主机在连续高并发运行 7 天后,
Order 9(2MB 大页)可用数量常年稳定维持在 400 个以上(彻底消除了碎片化赤字); /proc/vmstat中的compact_stall增长率彻底归零(0 次/天);- 核心微服务 P999 响应延迟毛刺从原本的 850ms 断崖式压降至 1.8ms 以内,彻底消除了由于内核内存压缩引发的“幽灵卡顿”。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)