引言

在现代数据中心与云计算环境中,内存管理子系统对系统整体性能有着举足轻重的影响。随着服务器内存容量突破 TB 级、核心数扩展至上百,传统的 4KB 小页(Base Page)管理方式在现代硬件上面临着 TLB(Translation Lookaside Buffer)命中率下降、页表层级加深带来的地址转换开销增大等挑战。透明大页(Transparent Huge Pages, THP)作为 Linux 内核提供的一种自动化大页机制,旨在通过将多个连续小页合并为大页(通常为 2MB 或 1GB),显著减少 TLB Miss 次数、降低页表遍历深度,从而提升内存密集型应用的访存性能。然而,THP 在生产环境中并非"银弹"——错误的使用场景会导致内存膨胀、延迟抖动、NUMA 失衡等问题。本文将从硬件基础出发,深入剖析 THP 在内核中的完整实现链路、khugepaged 线程行为、内存碎片整理策略,并结合数据库(MySQL/PostgreSQL/Redis)等典型场景给出生产调优实践。

一、大页的硬件基础与内核演进

1.1 TLB 与页表层级

现代 x86-64 处理器通过 MMU(Memory Management Unit)负责虚拟地址到物理地址的转换。每次地址转换都需要遍历多级页表,而 TLB 作为页表条目的硬件缓存,可以避免昂贵的内存访问。然而,主流服务器的 TLB 条目有限(通常 L1 D-TLB 64-96 条,L2 TLB 1536-2048 条),当工作集超过 TLB 覆盖范围时,频繁的 TLB Miss 会导致 Page Walk 开销占据 30% 以上的 CPU 时间。

大页的核心价值在于:一个 TLB 条目可覆盖更大的内存区域。以 4KB 小页与 2MB 大页对比,同样容量的 TLB 可覆盖 32KB-48KB vs 128MB-192MB 的内存范围,差距达 3000 倍以上。这对于内存密集数据库、虚拟机内存管理、HPC 计算等场景至关重要。

1.2 三种大页方案对比

Linux 历史上存在三种大页实现方式:

特性传统 HugePages(静态)THP(透明大页)libhugetlbfs
分配方式启动时预留,运行时不可变运行时按需动态合并/拆分基于hugetlbfs文件系统
对应用透明否,需修改代码调用mmap是,自动透明否,需修改代码
页大小2MB / 1GB2MD(x86-64)2MB / 1GB
灵活性低,需预估用量高,动态适应中等
适用场景数据库固定工作集通用云环境DPDK/特殊应用

1.3 THP 架构演进

THP 自 Linux 2.6.38(2011年)引入以来经历了多次重大改进:

  • v2.6.38:初始实现,仅支持匿名映射(anonymous memory),基于 khugepaged 内核线程扫描合并小页
  • v3.6.0:引入 madvise(MADV_HUGEPAGE) 机制,允许应用提示内核哪些区域应优先合并
  • v3.16.0:支持 file-backed 页面(tmpfs/shmem)的 THP 合并
  • v4.14.0:引入 deferred splitting(延迟拆分),避免大页拆分时阻塞进程
  • v5.4.0:增强 khugepaged 的 NUMA 感知能力,优先合并本地 NUMA 节点上的小页
  • v5.10.0:支持 configurable 页大小(multi-size THP / mTHP),向下兼容小工作集
  • v6.1.0:引入 per-size 的 THP 控制,针对不同页大小独立配置启用/禁用

二、内核页表管理与 THP 映射机制

2.1 x86-64 页表层级结构

x86-64 架构支持 48 位虚拟地址(256TB),使用 4 级页表(PGD→PUD→PMD→PTE),每级 9 位(512 个条目):

Virtual Address Layout (48-bit):
┌──────────┬──────────┬──────────┬──────────┬──────────┐
│ PGD[47:39]│ PUD[38:30]│ PMD[29:21]│ PTE[20:12]│ Offset[11:0]│
│  9 bits   │  9 bits  │  9 bits  │  9 bits  │  12 bits (4KB)│
└──────────┴──────────┴──────────┴──────────┴──────────┘

其中关键概念是大页标志位(PS - Page Size):当 PMD 条目的 PS=1 时,该条目直接指向一个 2MB 物理页(跳过 PTE 层级);当 PUD 条目的 PS=1 时,指向 1GB 物理页(跳过 PMD 层级)。THP 利用的就是 PMD 级别的 PS=1 来实现 2MB 大页映射。

2.2 THP 的 struct page 与 compound page

THP 使用 compound page(复合页)数据结构来管理连续小页组。内核通过以下方式识别大页的元数据:

// 简化的 compound page 结构
struct page {
    unsigned long flags;        // PG_head 标记大页的 head page
    struct {
        unsigned long compound_head;  // 指向 head page 的指针(低位置 PG_head)
        unsigned long compound_dtor;  // 析构函数
        unsigned long compound_order; // 页阶(order),THP 2MB = order 9
    };
    struct list_head lru;       // LRU 链表用于回收
};

2MB THP 的 order=9(2^9 × 4KB = 2MB8192 字节),内核使用第一个 page 结构作为 head page,后续 page 为 tail page,compound_head 字段统一指向 head,实现大页的统一管理。

2.3 THP 分配流程详解

THP 分配的完整代码路径(以匿名 mmap 为例):

用户空间调用 mmap(NULL, size, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, ...)
          ↓
内核: sys_mmap() → do_mmap() → get_unmapped_area()
          ↓
访问时触发 Page Fault: do_page_fault() → handle_mm_fault()
          ↓
Anonymous Fault 路径:
  handle_pte_fault() → do_anonymous_page()
          ↓
  检查 vma->vm_flags & VM_HUGEPAGE (由 madvise 或全局设置控制)
          ↓
  调用 do_huge_pmd_anonymous_page()
          ↓
  1. alloc_hugepage_vma() - 从 buddy allocator 分配 order=9 的大页
  2. vma->vm_ops->huge_fault() - 建立映射
  3. set_pmd_at(..., pmd_mkhuge(...)) - 设置 PS=1 标志
          ↓
  成功: TLB 刷新完成,应用使用 2MB 大页运行
  失败: 回退分配 4KB 小页

其中 VM_HUGEPAGE 标志的触发条件为:全局设置 /sys/kernel/mm/transparent_hugepage/enabled 为 always 或应用显式调用 madvise(MADV_HUGEPAGE)。

三、khugepaged — THP 的内核守护线程

3.1 khugepaged 的工作原理

khugepaged 是 THP 的核心内核线程,负责在后台扫描进程的内存空间,将满足条件的连续小页合并为 2MB 大页。其工作流程为:

khugepaged (内核线程)
  │
  ├── 1. 从 kthreadd 启动,优先级为 kthread (nice = 0)
  │
  ├── 2. 进入主循环:
  │      等待 khugepaged_wait 信号量(超时或事件唤醒)
  │
  ├── 3. 扫描候选 VMA:
  │      遍历所有 VMAs,检查条件:
  │      └── vm_flags & VM_HUGEPAGE (合并目标)
  │      └── vm_flags & VM_NOHUGEPAGE (跳过)
  │      优先级队列:madvise 区域 > 全局 never 区域
  │
  ├── 4. 执行合并:
  │      对每个候选区域的 2MB 对齐虚拟地址:
  │      ├── collapse_huge_page()
  │      │   ├── 分配一个新的 2MB 大页
  │      │   ├── 检查目标区域内所有 PTEs 指向的物理页是否连续/可移动
  │      │   ├── 调用 isolate_lru_page() 将页面隔离出 LRU
  │      │   ├── 使用 rmap(reverse mapping)安全更换页表项
  │      │   ├── 设置 pmd_mkhuge(PMD) 完成映射
  │      │   └── 释放旧的小页组
  │      │
  │      └── 合并成功: 释放旧小页,更新统计
  │          合并失败: 释放新分配的大页,继续下一区域
  │
  └── 5. 睡眠等待:
          khugepaged_pages_to_scan 控制每次扫描页数
          khugepaged_scan_sleep_millisecs 控制扫描间隔(默认 10000ms = 10s)

3.2 khugepaged 关键参数

参数路径默认值说明
pages_to_scan/sys/kernel/mm/transparent_hugepage/khugepaged/pages_to_scan4096(16MB每次扫描的小页数
scan_sleep_millisecs/sys/kernel/mm/transparent_hugepage/khugepaged/scan_sleep_millisecs10000两次扫描间隔(毫秒)
alloc_sleep_millisecs/sys/kernel/mm/transparent_hugepage/khugepaged/alloc_sleep_millisecs60000分配失败重试前等待(毫秒)
max_ptes_none/sys/kernel/mm/transparent_hugepage/khugepaged/max_ptes_none511允许的最大未映射 PTE 数(512-1)
max_pte_misses/sys/kernel/mm/transparent_hugepage/khugepaged/max_pte_misses-跳过区域前最大 PTE 错失次数

其中 max_ptes_none 是关键参数:它控制合并阈值。默认 511 表示在一个 2MB 窗口(512 个 PTE)中,只要未映射的 PTE 不超过 511 个,即至少分配了 1 个小页。降低此值(如 0 或 64)可提高合并率,但增加 khugepaged 的 CPU 开销;设为 511(默认)则表示仅分配 1 页时也可合并,但非强制。

3.3 khugepaged 的代价与优化

khugepaged 在合并密集内存时会产生显著副作用:

  • CPU 开销:频繁扫描在大内存(512GB+)服务器上可能消耗 1-3% CPU
  • 分配延迟:大页分配失败时触发直接回收(direct reclaim),导致应用停顿
  • NUMA 不感知:早期版本 khugepaged 不考虑 NUMA 拓扑,可能跨节点分配大页,导致远程访存

自 5.4+ 版本起,khugepaged 引入 NUMA 感知分配策略:优先在触发 Page Fault 的 CPU 所在 NUMA 节点分配大页。更进一步建议将 khugepaged 的扫描间隔在大内存系统上调大至 60000ms 以上,或使用 taskset 绑定其 CPU 到特定核心。

四、THP 的拆分与内存碎片整理

4.1 大页拆分(Split / Collapse Failed)

当大页中某个小页被写时复制(COW)、迁移、或 madvise(MADV_DONTNEED) 时,内核需要拆分 2MB 大页回 4KB 小页。Linux 4.14 之前,拆分是同步阻塞操作;4.14+ 引入 Deferred Splitting:

Deferred Split Queue(延迟拆分队列)
  │
  ├── 触发条件:
  │    ├── munmap/mprotect/madvise 操作中检测到 compound page
  │    ├── try_to_unmap() 发现大页中部分 PTE 被 PTE-unmap
  │
  ├── 处理流程:
  │   split_queue_lock 保护的双向链表
  │   ├── 将待拆分大页加入 split_queue_list
  │   ├── split_queue_len 记录待处理大页数
  │   ├── 在适当的时机(exit_mmap/munmap_batch)异步执行 split_huge_page()
  │   └── 释放 split 出的空闲小页返回 buddy allocator
  │
  └── 优势:减少持有 mmap_lock 的时间,提升并发 munmap 性能

4.2 内存碎片与 CMA(Contiguous Memory Allocator)

THP 依赖 buddy allocator 提供连续的物理内存。长时间运行后,内存碎片化可能导致 THP 分配失败。Linux 内核提供两种碎片整理机制:

机制触发方式说明
compact_memory手动触发:echo 1 > /proc/sys/vm/compact_memory同步整理所有 zone 的碎片
kcompactd 内核线程自动触发(碎片严重时)后台异步整理碎片,避免分配失败
CMA启动时预留连续内存区域专为 DMA/大页设计,但容量通常较小

碎片整理的核心算法(由 compaction.c 实现)使用 migration scanner:

free scanner(从 zone 头部开始):扫描寻找可分配的空闲页块
migration scanner(从 zone 尾部开始):扫描可迁移页,腾出连续空间

两者相向而行,当 free scanner 到达 migration scanner 位置时,
合并找到的空闲页形成更大的连续块(order=9)

五、NUMA 架构下的 THP 优化

5.1 NUMA 访存模型

在 NUMA(Non-Uniform Memory Access)架构下,处理器访问本地内存节点的延迟通常为 80-100ns,远低于远程节点的 140-200ns。THP 的 NUMA 敏感性比 4KB 小页更强,因为:

  • 2MB 大页只能属于一个 NUMA 节点(不可分割)
  • 错配的 2MB 大页会导致整个区域的远程访问
  • 小页可以灵活分布到多个 NUMA 节点

5.2 NUMA 感知分配策略

从 Linux 5.7 开始,THP 分配引入 NUMA 策略控制:

/sys/kernel/mm/transparent_hugepage/numa_stat    # NUMA 分配统计
/sys/devices/system/node/node*/meminfo            # 各节点内存使用

thp_numa_local_alloc    # 本地节点成功分配的大页数
thp_numa_local_fail     # 本地节点分配失败数  
thp_numa_remote_alloc   # 远程节点分配的大页数(应尽量避免)

5.3 生产环境 NUMA 优化策略

对于运行在大 NUMA 系统上的数据库服务,推荐以下配置:

# 查看当前 NUMA 拓扑
numactl --hardware

# 将数据库进程绑定到特定 NUMA 节点,确保 THP 本地分配
numactl --membind=0 --cpunodebind=0 mysqld ...

# 或设置 zone_reclaim_mode=1 促进本地分配
sysctl -w vm.zone_reclaim_mode=1

# /etc/sysctl.d/99-thp-tuning.conf
# 启用 NUMA 均衡 + THP 按需分配
kernel.numa_balancing = 1
vm.zone_reclaim_mode = 1

六、生产场景实战:数据库的 THP 配置

6.1 MySQL / MariaDB

MySQL 官方博客曾明确建议禁用 THP(在 MySQL 5.7 和 8.0 文档中均有提及),原因如下:

  • InnoDB 使用自己的 Buffer Pool 管理,可能产生大量 madvise 调用
  • THP 拆分时使用 Global Lock(mmap_lock 写锁),导致并发写入暂停
  • 长时间运行后碎片化导致内存浪费和延迟抖动

然而,对于以读为主的场景或只读副本,THP 可带来 5-15% 的 TLB Miss 降低。折中方案是:

# /etc/rc.local(MySQL 建议禁用 THP)
echo never > /sys/kernel/mm/transparent_hugepage/enabled
echo never > /sys/kernel/mm/transparent_hugepage/defrag

# 或者使用 madvise 模式,让应用自己决定
echo madvise > /sys/kernel/mm/transparent_hugepage/enabled

6.2 PostgreSQL

PostgreSQL 自 9.4+ 版本支持在共享内存中使用大页(huge_pages = on),但使用的是静态 HugePages 而非 THP。对于私有内存(如 work_mem),PostgreSQL 通常建议使用 THP 的 madvise 模式:

# postgresql.conf
huge_pages = try    # 尝试使用静态 HugePages(需预先配置)

# 系统参数
# 设置为 madvise,避免全局 THP 干扰
echo madvise > /sys/kernel/mm/transparent_hugepage/enabled

6.3 Redis

Redis 官方强烈推荐禁用 THP(THP Frankenstein):

  • Redis 使用 fork() 创建子进程做持久化(RDB 或 AOF rewrite)
  • fork() 后子进程继承父进程的 COW 页面
  • 父进程的写入触发 COW,若页面是 2MB THP,拆分后变为 512 个 4KB 页
  • 每个 4KB 页的 COW 会触发额外的 page fault,导致 fork 时间暴涨(从 ms 级→秒级)
  • 这会导致主进程在 THP 启用时的内存使用量(RSS)远超实际数据量(因无法重复利用写过的 THP 区域)

因此 Redis 在启动时会检测 THP 状态,若启用则打印警告。生产环境必须在系统层面禁用 THP:

/etc/rc.local
echo never > /sys/kernel/mm/transparent_hugepage/enabled
echo never > /sys/kernel/mm/transparent_hugepage/defrag

6.4 基础设施与中间件小结

软件推荐配置原因
Redis (持久化模式)disabled(禁用)fork 后 COW 触发 THP 拆分导致延迟
MySQL (写密集)disabled 或 madvise拆分时全局锁影响并发
PostgreSQLmadvise共享内存用静态大页
Java (JVM)madvise 或 always-XX:+UseLargePages 配合效果更佳
QEMU/KVMalways减少虚拟机内存 virtual→physical 开销
DPDK静态 1GB HugePages固定大页避免运行时分配延迟
HPC 计算always最大化 TLB 覆盖率

七、THP 与虚拟化:KVM 场景分析

在 KVM 虚拟化场景中,THP 通过减少 EPT(Extended Page Table)层级的 TLB Miss 来提升虚拟机性能。当 VM 内存大小超过 256GB 时,TLB 未命中导致的性能损失可达 15-20%。配置建议:

# 宿主机启t always
echo always > /sys/kernel/mm/transparent_hugepage/enabled

# QEMU 启动参数
-m 64G \
-object memory-backend-file,id=mem,size=64G,mem-path=/dev/shmem,share=on,numa-node=0 \
-nodevices

# 配合 QEMU 的 prealloc 可进一步提升 THP 合并率
-prealloc

八、监控与可观测性

8.1 THP 状态查询

# 查看 THP 全局状态
cat /sys/kernel/mm/transparent_hugepage/enabled    # 通常为 [madvise] always never

# 查看 THP 合并/拆分统计
grep -i huge /proc/meminfo
AnonHugePages:   204800 kB    # 当前系统 THP 使用量(匿名)
ShmemHugePages:      12 kB    # 共享内存中 THP 使用量
HugePages_Total:     80    # 静态 HugePages 总数
HugePages_Free:      75    # 空闲静态 HugePages
Hugepagesize:       2048 kB

# 各 NUMA 节点 THP 分配统计
cat /sys/kernel/mm/transparent_hugepage/numa_stat

# 大页数拆分统计(/proc/vmstat)
grep thp /proc/vmstat
thp_fault_alloc          # 成功 THP 分配次数
thp_fault_fallback       # THP 分配失败回退次数
thp_collapse_alloc       # khugepaged 合并成功次数
thp_collapse_alloc_failed # khugepaged 合并失败次数
thp_split                # 大页被拆分次数
thp_split_page           # 拆分的大页数
thp_zero_page_alloc      # 分配 THP 并初始化(清零)次数

8.2 eBPF 监控 THP 行为

可以使用 BCC / bpftrace 实时跟踪 THP 相关事件:

# 跟踪 THP 分配事件
bpftrace -e 'kprobe:__do_huge_pmd_anonymous_page { printf("THP 2MB fault at PID %d, addr 0x%lx\n", pid, arg1); }'

# 跟踪大页拆分次数
bpftrace -e 'kprobe:split_huge_page { @traces[comm] = count(); }'

# 监控 khugepaged CPU 使用
perf top -p $(pgrep khugepaged) -d 10

# 使用 perf stat 追踪 TLB 性能
perf stat -e dtlb_load_misses.stlb_hit,dTLB-loads,dTLB-load-misses ./your_application

8.3 TLB Miss 分析工具

推荐使用 perf 配合 mem 子命令进行采样:

# 监控应用运行时的远端 TLB Miss
perf mem -t load record -a sleep 30
perf mem report --sort=mem

# 输出示例:
#  85.39%  [kernel]  [k] copy_page_to_iter  # 内核态热路径 TLB Miss
#   4.22%  [mysqld]  [.] ???                 # 应用态 TLB Miss
#   ...

九、未来趋势与新特性

9.1 multi-size THP (mTHP)

Linux 5.15 引入的 multi-size THP 允许内核使用不同大小的大页:64KB、32KB 等中间粒度。这对 ARM64(原生支持 64KB 页)系统意义重大,可解决 2MB THP 对小工作集(如几 MB 的热数据)过度分配问题。用户可通过 /sys/kernel/mm/transparent_hugepage/hpage_pmd_size 配置。

9.2 Large folio 与文件映射

正推动中的 folio patchset (Matthew Wilcox) 用 folio 取代 compound page 概念,提供统一的页组抽象。文件-backed 页面的大页化(file THP)将在 6.3+ 内核中增强,允许 ext4/xfs 文件系统上的大页文件缓存。

9.3 硬件支持趋势

  • Intel:5-level page table(57位虚拟地址)、1GB 大页原生支持
  • AMD:支持 1GB 大页,SEV-SNP 加密场景下大页优势明显
  • ARM64:4KB-64KB 页大小可配置,原生支持 Contiguous PTE 标志(连续 16/64 个 PTE 构成大页)

总结

透明大页作为 Linux 内核内存管理调优的关键杠杆,其效果高度依赖于应用场景与工作负载特征。对于内存密集型、顺序访存为主的大数据集场景(虚拟机内存、HPC 分析),THP 可带来显著的性能提升;而对于 fork()-密集型、随机分配/释放模式的应用(Redis、频繁创建短进程的服务),THP 反而可能造成延迟恶化与内存浪费。生产环境调优的核心原则是:理解你的工作负载,使用 madvise 或 per-application 控制,而非依赖全局 always 设置。结合 khugepaged 参数调优、NUMA 感知分配与 eBPF 运行时监控,才能充分发挥 THP 的潜力同时避免副作用。

参考资料

  • Documentation/admin-guide/mm/transhuge.rst — Linux 内核文档
  • MySQL 8.0 Reference Manual: 8.5.7 Linux Memory Allocation
  • Redis Documentation: THP on Redis latency problems
  • Understanding the Linux Virtual Memory Manager — Mel Gorman
  • Linux Kernel Source: mm/huge_memory.c, mm/compaction.c, mm/khugepaged.c
点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部