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 以内,彻底消除了由于内核内存压缩引发的“幽灵卡顿”。
Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐