文章

大页内存与透明大页详解

大页内存与透明大页详解

概述

现代 x86-64 CPU 的默认页大小是 4KB。一条 64GB 内存的服务器上有 16,777,216 个物理页,但 TLB(Translation Lookaside Buffer)只有约 1536 个条目。当工作集超过 TLB 覆盖范围(4KB × 1536 ≈ 6MB)时,每次内存访问都可能触发 page table walk,增加 1-5 次额外的内存访问。大页(HugePages)通过将页放大到 2MB 或 1GB,把 TLB 覆盖范围扩大 512-262144 倍。

一句话:4KB 页 → TLB 覆盖 ~6MB。2MB 页 → TLB 覆盖 ~3GB。1GB 页 → TLB 覆盖 ~1.5TB。

TLB 为什么重要

一次内存访问的代价

虚拟地址 → TLB 查询
  ↓ 命中(~0.5-1 cycle)
  直接得到物理地址 → 访问内存(~100ns)
  
  ↓ 未命中(TLB miss)
  四级页表遍历(4 × 内存访问)
    PGD → PUD → PMD → PTE  (~400-500ns 额外开销)
    然后 → 访问内存(~100ns)

每次 TLB miss 的代价是 命中时的 5 倍以上。工作集越大,miss 越多。大页直接减少页表级数(2MB 页只用 3 级,1GB 页只用 2 级),大幅降低 miss 率。

TLB 规格参考

CPUL1 DTLBL2 STLB
Intel Skylake64 × 4KB + 32 × 2MB/4MB1536 × 4KB + 相同 2MB/4MB
Intel Sapphire Rapids96 × 4KB + 48 × 2MB/4MB2048 × 4KB
AMD EPYC Genoa72 × 4KB + 72 × 2MB3072 × 和 L1 共享格式

注:大页条目数较少但每条覆盖的内存大得多。关键不是条目数,是覆盖总量。

显式大页(HugePages)

hugetlbfs 机制

显式大页不是自动的:系统预留一块连续物理内存作为大页池,应用通过 mmap hugetlbfs 文件系统申请。

# 1. 预留 2MB 大页
echo 1024 > /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages
# 预留 1024 × 2MB = 2GB

# 2. 查看预留情况
cat /proc/meminfo | grep -i huge
# HugePages_Total:    1024
# HugePages_Free:     1024
# HugePages_Rsvd:        0
# HugePages_Surp:        0
# Hugepagesize:       2048 kB
# Hugetlb:         2097152 kB   ← 2GB 总容量

# 3. 持久化配置
echo "vm.nr_hugepages = 1024" >> /etc/sysctl.d/99-hugepages.conf
sysctl -p /etc/sysctl.d/99-hugepages.conf

# 4. 应用程序使用(hugetlbfs mount)
mkdir -p /mnt/huge
mount -t hugetlbfs -o pagesize=2M none /mnt/huge
# 应用通过 mmap /mnt/huge 获取大页

1GB 大页

GPU 训练场景下,1GB 大页可以大幅减少 driver 和 NCCL 通信中的 TLB miss:

# 配置 16 个 1GB 大页
echo 16 > /sys/kernel/mm/hugepages/hugepages-1048576kB/nr_hugepages

# 持久化(grub)
# GRUB_CMDLINE_LINUX="... hugepagesz=1G hugepages=16 default_hugepagesz=1G"

# 验证
cat /proc/meminfo | grep Huge
# HugePages_Total:      16
# Hugepagesize:    1048576 kB

限制:1GB 大页需要在引导阶段分配(无法动态增减),且必须找到连续的 1GB 物理内存。长时间运行后内存碎片化会分配失败。

K8s 中的 HugePages

apiVersion: v1
kind: Pod
spec:
  containers:
  - name: db
    image: postgres:16
    resources:
      limits:
        hugepages-2Mi: 256Mi     # 128 个 2MB 页
        memory: 8Gi
      requests:
        hugepages-2Mi: 256Mi
    volumeMounts:
    - name: hugepages
      mountPath: /dev/hugepages
  volumes:
  - name: hugepages
    emptyDir:
      medium: HugePages-2Mi

K8s 自动在节点上为 Pod 预留大页,调度时确保目标节点有足够的空闲大页。

透明大页(THP)

THP 工作机制

THP 是内核自动管理的:后台线程 khugepaged 持续扫描内存,发现连续的 4KB 页时自动合并为 2MB 大页。用户应用无需任何代码修改。

khugepaged 线程:
  → 扫描匿名页
  → 发现 512 个连续的 4KB 页(对齐到 2MB 边界)
  → 将它们合并为一个 2MB 大页
  → 更新页表,减少 TLB miss

三个模式

模式/sys/kernel/mm/transparent_hugepage/enabled行为
always[always] madvise never内核积极合并所有符合条件的匿名页
madvisealways [madvise] never只有应用通过 madvise(MADV_HUGEPAGE) 请求时才合并
neveralways madvise [never]完全不使用 THP

THP 的开销:compaction

THP 需要找到 2MB 连续物理内存。如果内存碎片化,khugepaged 会触发 compaction(内存压缩/整理),这会:

  • 消耗 CPU(khugepagedkcompactd 线程)
  • 暂停应用的内存分配(direct compaction)
  • 增加延迟抖动(p99 延迟井喷)

这就是 etcd、Redis 等延迟敏感应用必须关闭 THP 的原因。

# 检查 compaction 开销
cat /proc/vmstat | grep compact
# compact_migrate_scanned, compact_free_scanned 持续增长 = 碎片化严重

# 检查 khugepaged CPU 使用
top -p $(pgrep khugepaged)

THP 决策树

需要极低延迟(< 1ms p99)?
├── 是 → 关闭 THP,用显式 HugePages
│   场景:etcd、Redis、Kafka、金融交易
└── 否
    ├── 长时间运行的大内存应用?
    │   └── 是 → madvise(应用主动申请)
    │       场景:PostgreSQL、MySQL、JVM
    └── 短生命周期、频繁 fork?
        └── 是 → madvise 或 never
            场景:PHP-FPM、短 job

关闭 THP 的多种方式

# 方法 1:运行时关闭
echo never > /sys/kernel/mm/transparent_hugepage/enabled
echo never > /sys/kernel/mm/transparent_hugepage/defrag

# 方法 2:grub 启动参数(永久)
# /etc/default/grub
GRUB_CMDLINE_LINUX="... transparent_hugepage=never"

# 方法 3:systemd 服务(确保在应用启动前关闭)
# /etc/systemd/system/disable-thp.service
[Unit]
Description=Disable THP
Before=etcd.service kubelet.service

[Service]
Type=oneshot
ExecStart=/bin/sh -c 'echo never > /sys/kernel/mm/transparent_hugepage/enabled && echo never > /sys/kernel/mm/transparent_hugepage/defrag'
RemainAfterExit=yes

[Install]
WantedBy=multi-user.target

THP 与 K8s 的兼容性问题

Redis / etcd 的 THP 陷阱

# Redis 启动日志:
# WARNING you have Transparent Huge Pages (THP) support enabled in your kernel.
# This will create latency and memory usage issues with Redis.

# etcd 官方文档:
# "etcd 强烈建议在运行环境中关闭透明大页"

原因是 fork() 的 COW(Copy-on-Write):大页的 COW 粒度是 2MB 而非 4KB。Redis BGSAVE 或 etcd 快照时 fork(),即使只修改了子进程的 1 字节,也会复制整个 2MB 大页,导致:

  • 内存使用暴增
  • fork() 耗时大幅增加(阻塞主进程)

验证与监控

# 检查是否生效
cat /sys/kernel/mm/transparent_hugepage/enabled
# 期望:[never]

# 在容器内检查(取决于挂载方式)
# K8s 1.29+ 默认会挂载 hostPath /sys 到容器

# Prometheus 监控指标
node_memory_HugePages_Free
node_memory_HugePages_Rsvd
node_memory_HugePages_Total
rate(node_vmstat_compact_migrate_scanned[5m])

HugePages 在 GPU 训练中的应用

为什么 GPU 训练需要大页

GPU 训练的典型内存访问模式:

  1. CUDA driver + nvidia kernel module 需要 pin 住大段主机内存做 DMA(GPU Direct)
  2. NCCL 通信缓冲区(multi-GB)如果不使用大页,TLB miss 频繁
  3. 数据加载(DataLoader)的 pinned memory
# 大模型训练节点推荐设置(8×A100/H100,512GB RAM)
echo 131072 > /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages  # 256GB 2MB 大页
echo 8 > /sys/kernel/mm/hugepages/hugepages-1048576kB/nr_hugepages     # 8GB 1GB 大页

# 或 grub 一次性设
# GRUB_CMDLINE_LINUX="... hugepagesz=1G hugepages=8 hugepagesz=2M hugepages=131072 default_hugepagesz=2M"

性能收益参考

场景默认(4KB)2MB 大页1GB 大页
NCCL AllReduce (8×A100, 128MB buffer)基准+3-5%+5-8%
CUDA unified memory 迁移延迟基准-20%-35%
数据库 TPS(PostgreSQL)基准+10-15%
DPDK 包转发吞吐基准+15-25%

常见问题

问题原因解决
echo 1024 > nr_hugepages 返回错误没有 1024×2MB 连续物理内存在启动阶段预留,或降低数量
THP 设为 never 后系统重启又变回 always未通过 grub 持久化添加 transparent_hugepage=neverGRUB_CMDLINE_LINUX
K8s Pod 报 hugepages-2Mi 不足节点大页被其他 Pod 占用kubectl describe node 查看 hugepages-2Mi allocatable
khugepaged CPU 100%THP always + 内存碎片化,compaction 疯狂切换为 madvise 或 never
Pod 迁移后大页”消失”大页是 node-level 资源,重启后恢复确保节点启动脚本持久化了大页配置

关联知识

参考资源

学习时间

阶段时间备注
深度梳理2026-06-30TLB 原理、显式/透明大页对比、THP 陷阱、GPU 训练实践

状态: 🌱 学习中 下次复习日期: 2026-07-07