上一篇 下一篇 分享链接 返回 返回顶部

如何在香港服务器的 Linux 上,用 HugeTLB 与 Transparent HugePages 把“大内存数据库”压到稳定高性能

发布人:Minchunlin 发布时间:2025-08-31 09:23 阅读量:821


午夜 2:30,告警信息把我从宿舍拉回机房,客户的业务是跨境的资金清算,数据库实例内存 384 GB,正常 P99 延迟在 18–25 ms,过去一小时抬到了 120 ms 且有尖刺。监控显示 CPU 并不打满,但系统层面有周期性软中断与内存碎片回收。那晚我最先做的不是“加机器”,而是把矛头指向了内存页:THP(Transparent HugePages)与 HugeTLB 的组合使用不当,是一把看起来锋利却会“割手指”的刀。本文就按我当晚的排障—落地—回收的顺序,把细节、坑点和可复现实操完整讲清楚。

现场环境(香港机房的那台主机)

项目 规格
机房/线路 香港柴湾机房,双上联(国际/内地互联)
服务器 2× AMD EPYC 7543(32C64T×2,基准 2.8 GHz)
内存 512 GB DDR4-3200(8 通道×2 CPU,NUMA=2)
系统盘 2× 960 GB SATA SSD(RAID1)
数据盘 2× 3.84 TB NVMe(RAID1/软件 mdadm,ext4)
网卡 2× 10 GbE(bonding,LACP)
OS CentOS 7.9(3.10 内核),systemd,cgroup v1
DB PostgreSQL 14(主从×2,流复制),共享缓冲 256 GB(后改为 HugeTLB)

你若用 MySQL/Redis/RocksDB,后文都有“按此类推”段落;但主线用 PostgreSQL 演示,因为它对 HugeTLB 的支持更直观(huge_pages=on)。

1)原理速写:THP vs HugeTLB,别把两个概念搅在一起

Transparent HugePages(THP)

内核把 4 KB 小页“透明地”合并成 2 MB 大页(x86_64)。优点是省 TLB miss,对吞吐类负载友好;缺点是需要整理内存(compaction/khugepaged),在内存碎片化或内存压力时会产生不可预知的抖动(延迟尖刺)。

模式:always | madvise | never;另有 defrag 子开关。

HugeTLB(静态大页)

开机或运行时提前预留固定数量的大页(2 MB 或 1 GB),用户态通过 hugetlbfs/系统调用显式使用。优点是性能与延迟最稳定;缺点是预留即占用,分配不当会浪费内存。

简要结论:对大内存数据库(长寿命、大块共享内存),典型策略是禁用 THP(或设 madvise 仅在明确要求时使用),配合 HugeTLB 把大池子(如 PostgreSQL shared_buffers、MySQL InnoDB buffer pool)放在静态大页上,获得稳定的 P99。

2)动作前的最小安全网

两件事先做:

开一个本地控制台(IPMI/iKVM)与下个窗口的 SSH,以防 GRUB 改坏了进不了系统。

存一份回滚脚本(能一键把 THP、GRUB、sysctl、systemd 改回原状)。

# /root/rollback-hugepages.sh
set -e
# THP 回退
echo always > /sys/kernel/mm/transparent_hugepage/enabled || true
echo always > /sys/kernel/mm/transparent_hugepage/defrag || true

# 取消运行时 2MB hugepages
sysctl -w vm.nr_hugepages=0 || true
sed -i '/^vm.nr_hugepages=/d' /etc/sysctl.d/99-hugepages.conf || true

# 还原 GRUB(去掉 hugepages 与 transparent_hugepage 参数)
sed -ri 's/\s*(default_hugepagesz|hugepagesz|hugepages|transparent_hugepage)=[^ ]+//g' /etc/default/grub
grub2-mkconfig -o /boot/grub2/grub.cfg || true
[ -d /boot/efi/EFI/centos ] && grub2-mkconfig -o /boot/efi/EFI/centos/grub.cfg || true

# 还原系统服务
systemctl disable --now disable-thp.service || true
rm -f /etc/systemd/system/disable-thp.service
systemctl daemon-reload
echo "Rollback done."

3)第一步:把 THP 调整到对“延迟敏感”友好的状态

我当晚的症状是 periodic spike,结合 khugepaged 扫描与碎片整理,基本指向 THP。

3.1 临时关闭(立即生效,重启失效)

cat /sys/kernel/mm/transparent_hugepage/enabled
# [always] madvise never  ← 方括号为当前
echo never > /sys/kernel/mm/transparent_hugepage/enabled
echo never > /sys/kernel/mm/transparent_hugepage/defrag

3.2 永久关闭(或改为 madvise,更保守)

方式 A:systemd 开机脚本(不动内核参数)

# /etc/systemd/system/disable-thp.service
[Unit]
Description=Disable Transparent Huge Pages
After=sysinit.target

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

[Install]
WantedBy=multi-user.target

systemctl daemon-reload
systemctl enable --now disable-thp.service

想保守一点就把 never 改成 madvise,再按需在应用侧用 madvise(MADV_HUGEPAGE)。

方式 B:GRUB 内核参数(彻底)

在 /etc/default/grub 的 GRUB_CMDLINE_LINUX 里追加:

transparent_hugepage=never

然后重建 GRUB:

# BIOS
grub2-mkconfig -o /boot/grub2/grub.cfg
# UEFI(若存在)
[ -d /boot/efi/EFI/centos ] && grub2-mkconfig -o /boot/efi/EFI/centos/grub.cfg

4)第二步:给数据库配静态大页(HugeTLB)

4.1 选 2 MB 还是 1 GB?

2 MB:最通用,运行时也能分配(vm.nr_hugepages),失败概率小。

1 GB:TLB 命中更稳,但必须内核启动时预留(hugepagesz=1G),且一旦给多了就真的“吃满”(别浪费)。

当晚我们先用 2 MB 快速落地,第二天凌晨维护窗口再切到 1 GB。

4.2 方案一:2 MB HugeTLB(无需重启)

计算页数(以 PostgreSQL shared_buffers=256GB 为例):

256 GB / 2 MB = 131072 页。再留 5–10% 冗余,取 145000。

# 临时分配
sysctl -w vm.nr_hugepages=145000
# 持久化
cat >/etc/sysctl.d/99-hugepages.conf <<'EOF'
vm.nr_hugepages=145000
EOF
sysctl --system

挂载 hugetlbfs(可选,但推荐给应用一个确定路径权限):

groupadd -r hugepages
usermod -aG hugepages postgres
mkdir -p /dev/hugepages
echo "nodev /dev/hugepages hugetlbfs mode=1770,gid=hugepages" >> /etc/fstab
mount -a

4.3 方案二:1 GB HugeTLB(需要重启)

编辑 /etc/default/grub,在 GRUB_CMDLINE_LINUX 追加:

default_hugepagesz=1G hugepagesz=1G hugepages=300 transparent_hugepage=never

300×1 GB=300 GB,只预留给共享缓冲;别把 OS cache、进程栈、WAL、后台任务都挤没了。一般给 OS 至少留 25–30%。

重建 GRUB 并重启(同 3.2 的命令)。重启后:

grep -i huge /proc/meminfo
# 关注:
# HugePages_Total: 300
# HugePages_Free:  xxx
# Hugepagesize: 1048576 kB  ← 1GB

挂载 1 GB 大页的 hugetlbfs:

mkdir -p /dev/hugepages-1G
echo "nodev /dev/hugepages-1G hugetlbfs pagesize=1GB mode=1770,gid=hugepages" >> /etc/fstab
mount -a

5)数据库侧的正确打开方式(以 PostgreSQL 为例)

5.1 基本配置

postgresql.conf:

shared_buffers = 256GB
huge_pages    = on      # 关键:让 PG 申请 HugeTLB
# 建议:
effective_cache_size = 320GB
work_mem = 64MB
maintenance_work_mem = 2GB

huge_pages = on 表示没有大页就拒绝启动;若想先试水,可用 try,内核分不到大页则回落到普通页。

5.2 systemd 权限(锁内存,避免被换出)

# /etc/systemd/system/postgresql.service.d/limits.conf
[Service]
LimitMEMLOCK=infinity

mkdir -p /etc/systemd/system/postgresql.service.d
systemctl daemon-reload
systemctl restart postgresql

验证 PG 是否吃到了大页:

grep Huge /proc/meminfo
# HugePages_Rsvd 会随着 PG 启动上升
cat /proc/$(pgrep -u postgres -n postgres)/smaps | grep -i huge -n | head

也可看 pg_log,启动时会打印 “huge pages enabled”。

6)如果你是 MySQL / Redis / RocksDB

MySQL 8.0+

在 mysqld 上启用大页(不同版本叫法略有差异,通用做法是加 --large-pages 或在 my.cnf 里加 large-pages),同时把 LimitMEMLOCK=infinity。分配不够时会失败启动,所以先按 2 MB 路径试跑再切 1 GB。
检查:SHOW GLOBAL STATUS LIKE 'Huge_pages%';、SHOW VARIABLES LIKE 'large_pages';

Redis

更敏感的是 THP:务必 never 或 madvise,否则延迟抖动肉眼可见。Redis 不直接使用 HugeTLB,但你可以通过 madvise 让一部分内存走 THP(慎用)。

RocksDB

典型是用户态 madvise(MADV_HUGEPAGE);核心仍是禁掉 THP defrag 的不确定抖动,再用 jemalloc+合理 arena 控制碎片。

7)验收 & 监控:不看图表就等于没做

7.1 核心观测点

类别 指标/命令 期望变化
THP /sys/kernel/mm/transparent_hugepage/enabled nevermadvise
HugeTLB /proc/meminfoHugePages_* Total≈规划;Rsvd≈DB 申请量
Compaction vmstat 1compact/pgscank(3.10 无直接列,但可看 kswapd 峰值下降
PG pg_stat_statements、检查点/脏页、背景写入 P95/P99 降波动
OS perf top/flamegraph 中 TLB miss 相关栈 占比下降

7.2 快速巡检脚本

# /usr/local/bin/hpage-health.sh
printf "THP: "; cat /sys/kernel/mm/transparent_hugepage/enabled
printf "THP defrag: "; cat /sys/kernel/mm/transparent_hugepage/defrag
egrep 'HugePages|AnonHugePages|Hugepagesize' /proc/meminfo
mount | grep -i huge
numactl --hardware | sed -n '1,12p'

8)可复现的基准(你可以直接照抄跑)

由于每家硬件/业务不一,我给出脚本与表头,你只需把你实测数据填进去即可。下面的数值是示例结构,不是你的金科玉律。

8.1 pgbench 快速脚本

# 初始化数据库
sudo -u postgres createdb bench
sudo -u postgres pgbench -i -s 50 bench

# 场景 A:THP=always(基线)
# 场景 B:THP=never + HugeTLB(2MB)
# 场景 C:THP=never + HugeTLB(1GB)

for scene in A B C; do
  # 每个场景下跑 10 分钟,客户端 64 并发
  sudo -u postgres pgbench -c 64 -T 600 -P 10 bench \
    | tee /var/log/pgbench_${scene}.log
done

8.2 记录模板(示例表头)

场景 TPS(平均) P95 延迟 P99 延迟 标准差 HugePages_Rsvd 备注
A:THP=always 92,300 21.8 ms 118 ms 7.1 ms 0 基线
B:THP=never + 2MB 95,700(+3.7%) 17.2 ms(-21%) 72 ms(-39%) 4.3 ms(-39%) 131,072×2MB ≈ 256 GB vm.nr_hugepages=145000(Total 约 283GB,PG 实际 Rsvd≈256GB)
C:THP=never + 1GB 97,900(+6.1%) 16.1 ms(-26%) 61 ms(-48%) 3.8 ms(-47%) 256×1GB = 256 GB GRUB 预留 hugepages=300(Total 300GB,Rsvd≈256GB)

一般我在 OLTP 上看到:B 相比 A 的 P99 降 20–40% 波动,C 再比 B 降 5–15% 波动;吞吐(TPS)可能持平或小幅上升,关键是稳定性。

9)那些年我踩过的坑(以及怎么填)

坑点 症状 定位 解决
THP 没关干净 延迟周期性尖刺,khugepaged 活跃 /sys/kernel/mm/transparent_hugepage/* never,并确认 defrag=never
HugeTLB 数量不够 DB 启不来或回落小页 HugePages_Free < 需求 提前计算+加 5–10% 冗余
1 GB 页过量导致 OS 吃紧 系统 cache 枯竭、IO 反而慢 free -mmeminfo 减少 hugepages=,留 25–30% 给 OS
NUMA 失衡 单节点内存爆、跨 NUMA 访问高 numactl -Hnumastat 绑核绑内存,或 interleave=all 试验
容器里用不到大页 容器内参数正常但没拿到 k8s/容器未声明 Docker 要 --mount type=hugetlb;K8s 需 resources: hugepages-2Mi/1Gi
升级内核/重建 GRUB 丢配置 重启后又是 THP=always 检查 /etc/default/grub 把参数写回去,并固化到 CM 平台
MySQL 名称差异 large-pages 配置不生效 版本差异 SHOW VARIABLES/STATUS 实证,必要时走启动参数

10)Ansible 一把梭(2 MB 版;生产前请先在灰度环境验证)

# roles/hugepages/tasks/main.yml
- name: Disable THP via systemd
  copy:
    dest: /etc/systemd/system/disable-thp.service
    content: |
      [Unit]
      Description=Disable Transparent Huge Pages
      After=sysinit.target
      [Service]
      Type=oneshot
      ExecStart=/bin/sh -c 'echo never > /sys/kernel/mm/transparent_hugepage/enabled; echo never > /sys/kernel/mm/transparent_hugepage/defrag'
      [Install]
      WantedBy=multi-user.target
  notify: reload systemd

- name: Enable service
  systemd: {name: disable-thp, enabled: yes, state: started}

- name: Set 2MB HugeTLB pages
  copy:
    dest: /etc/sysctl.d/99-hugepages.conf
    content: "vm.nr_hugepages=145000\n"

- name: Apply sysctl
  command: sysctl --system

- name: Mount hugetlbfs
  mount:
    path: /dev/hugepages
    src: nodev
    fstype: hugetlbfs
    opts: mode=1770,gid=hugepages
    state: mounted

11)变更回滚(实操口诀)

PG 先改 huge_pages=try → 重启 → 观察。

将 vm.nr_hugepages 调小或置 0;若是 1 GB 页,先改 GRUB,把 hugepages= 降低。

恢复 THP 到 madvise 或 always(按你原来的基线)。

观察 24 小时,确认没有负面连带(OS cache、备份作业、ETL)。

12)FAQ

我要不要直接上 1 GB?

取决于你的“维护窗口”与“可回滚性”。我通常先 2 MB 快速止血,再在窗口切 1 GB。

THP = madvise 有用吗?

对分析型/批处理类可能有用;对延迟敏感 OLTP 我倾向 never。

cgroup v1 能限制 HugeTLB 吗?

能,在 /sys/fs/cgroup/hugetlb 下有 hugetlb.2MB.limit_in_bytes 等,K8s/容器场景请单独设计。

凌晨 3:40,我们先把 THP 切到 never,vm.nr_hugepages 上到了 145000,PG 改 huge_pages=on,服务重启 40 秒完成。图上 P99 从 120 ms 立刻落到 27–35 ms;到 5:00 前再没出现尖刺。第二天的维护窗口,我们把 2 MB 切成了 1 GB,延迟波动进一步收紧。
回到宿舍时,便利店的灯已经灭了,但我心里那句“先把内存页搞干净,再谈别的”,从此刻进了我的标准流程。别一味加机器,先把 THP 与 HugeTLB 用对——它们能让你的大内存数据库,又稳又快。

目录结构
全文