Linux 内存碎片化(Memory Fragmentation)与透明大页(THP)排坑

封面信息图

在长期稳定运行数月之久的 Kubernetes 物理节点、Redis 缓存服务器或大型数据库宿主机上,运维工程师经常会遭遇这样一种让人匪夷所思的“假死性性能劣化”:

查看 free -m 监控指标,系统的物理内存还有 40GB 以上的充足空闲(Free/Available)
然而,当某个业务进程尝试申请一块稍微大一点的连续内存、或者 Redis 执行 BGSAVE 创建子进程时,系统突然发生严重的性能停顿:

  • 接口响应延迟从 2ms 骤增至数百毫秒;
  • 内核守护线程 kcompactd0kswapd0 瞬间将 CPU 单核算力拉满到 100%;
  • 查看系统日志,充斥着大量的 page allocation stalls(内存分配停顿)告警,极端情况下甚至直接触发了非预期的 OOM-Killer 误杀。

在内存明明非常充裕的情况下,为什么操作系统会卡死在内存分配上?
问题的根源在于 Linux 底层内存管理子系统的两大隐形暗礁:外部内存碎片化(External Memory Fragmentation)透明大页(THP, Transparent Huge Pages)机制的严重反噬

深入物理底层:Buddy System 与内存碎片化

Linux 内核采用经典的 伙伴系统(Buddy System) 来管理物理内存页(Page,标准大小为 4KB)。伙伴系统将连续的物理页按照 $2^n$ 的大小划分为 11 个阶梯等级(Order 0 到 Order 10):

  • Order 0:单个 4KB 页面;
  • Order 1:连续 2 个页面(8KB);
  • $\dots$
  • Order 9:连续 512 个页面(2MB,对应大页 HugePage);
  • Order 10:连续 1024 个页面(4MB)。

当系统长期运行后,由于大量进程频繁地申请和释放大小不一的小内存块,原本连续的大块物理内存被逐渐切得支离破碎。
此时,虽然零碎的 4KB 小页面的总和依然高达 40GB,但在 Order 3(32KB)或 Order 9(2MB)的高阶链表上,可能连一个可用的连续大块物理内存都找不出来了

[ 物理内存碎片化示意图 (总空闲 16KB,但全被切碎) ]
  [ 4KB空闲 ] [ 已占用 ] [ 4KB空闲 ] [ 已占用 ] [ 4KB空闲 ] [ 已占用 ] [ 4KB空闲 ]
  ──► 此时若应用程序申请一块 16KB 连续内存:
      虽然总空闲 = 16KB,但没有一块连续的 Order 2 (16KB) 块可用!
      直接触发 Linux 内核【同步内存压缩 (Direct Compaction)】,全系统陷入死锁卡顿!

为什么透明大页(THP)会成为数据库的灾难?

为了降低 CPU 页表(TLB, Translation Lookaside Buffer)的未命中率,现代 Linux 默认开启了 THP(Transparent Huge Pages,透明大页) 机制。THP 试图在后台自动将 512 个连续的 4KB 小页面合并为一个 2MB 的大页

在面对 Redis、MySQL、Elasticsearch、JVM 等内存密集型组件时,THP 会带来灾难性的后果:

  1. 内存写放大(Copy-On-Write 放大 512 倍)
    Redis 在执行持久化(Fork BGSAVE)时,采用操作系统 Copy-On-Write 机制。当开启 THP 时,子进程和父进程共享 2MB 大页。客户端只要修改了某个 String Key 中的 1 个字节,Linux 内核就会被迫在内存中完整拷贝整整 2MB 的全新大页!内存写入吞吐瞬间暴增 512 倍,导致 Redis 发生长达数百毫秒的严重阻塞。
  2. 直接内存压缩(Direct Compaction)死锁
    当应用程序申请内存时,THP 执着地要在内存中寻找连续 2MB 的 Order 9 大块。如果内存已经碎片化,内核进程 khugepaged 会触发同步直接压缩(Direct Compaction):强行锁住多个物理内存页并在内存中来回搬移数据以拼凑出连续大页。在此期间,所有业务线程的内存申请被强制挂起,引发全站大面积超时。

现场诊断:如何确认内存碎片化程度?

可以通过查看 /proc/buddyinfo 查看系统当前各个阶梯(Order 0~10)的空闲块分布:

# 查看各个 NUMA 节点下的伙伴系统阶梯分布
cat /proc/buddyinfo

# 典型严重碎片化输出示例:
# Node 0, zone Normal  182400   1200     40      0      0      0      0      0      0      0      0
# 分析:
# Order 0 (4KB) 有 182,400 个空闲块 (约 730MB);
# 而 Order 3 (32KB) 及以上高阶全部为 0!说明系统已极度碎片化,无法分配任何连续大块内存!

同时查看系统由于直接内存压缩导致的卡顿计数:

grep -E "compact_stall|compact_fail" /proc/vmstat

生产级根治调优方案

1. 彻底禁用透明大页(THP)——数据库与容器节点的铁律

在所有生产宿主机启动脚本与 GRUB 中,强制将 THP 设为 never

# 1. 临时立即生效 (需同时修改 thp 与 defrag)
echo never > /sys/kernel/mm/transparent_hugepage/enabled
echo never > /sys/kernel/mm/transparent_hugepage/defrag

# 2. 在 /etc/default/grub 的 GRUB_CMDLINE_LINUX 中永久固化
transparent_hugepage=never

执行 grub2-mkconfig -o /boot/grub2/grub.cfg 永久生效。

2. 配置内核主动内存反碎片化基线

编辑 /etc/sysctl.d/99-memory-defrag.conf

# 提高内存保留水印 (Watermark Scale Factor),让 kswapd 提前在后台平滑回收,避免触发直接压缩
vm.watermark_scale_factor = 200

# 控制内核在内存紧张时主动进行内存压缩的激进度 (默认 1)
vm.compaction_proactiveness = 50

# 降低 Swap 倾向,避免将匿名页换出
vm.swappiness = 1

# 控制虚拟内存分配策略 (0: 启发式过度分配; 1: 永远允许超卖; 2: 严格禁止超卖)
# 针对 Redis 宿主机必须设为 1,确保 Fork BGSAVE 时能够顺利申请虚拟内存空间
vm.overcommit_memory = 1

执行 sysctl -p /etc/sysctl.d/99-memory-defrag.conf

3. 编写非高峰期主动内存整理 Cron 任务

在业务凌晨低谷期(如 04:00),通过向内核写入触发一次非阻塞的主动内存连续性整理:

# 在低峰期主动触发内存碎片合并整理 (释放零散小页,拼凑出高阶连续大块)
echo 1 > /proc/sys/vm/compact_memory

收益复盘

在全集群推行“彻底禁用 THP + 调大 watermark_scale_factor + 凌晨主动内存压缩”后:

  • Redis 实例在执行 BGSAVE 时的最大阻塞耗时从原本的 420ms 断崖式下跌至 1.8ms
  • 宿主机 /proc/buddyinfo 中的高阶(Order 3~Order 7)可用连续块数量长期稳定在 1000 以上;
  • 彻底消除了因直接内存压缩(Direct Compaction)引发的系统级间歇性假死故障。
Logo

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

更多推荐