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

午夜 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 |
never 或 madvise |
| HugeTLB | /proc/meminfo 的 HugePages_* |
Total≈规划;Rsvd≈DB 申请量 |
| Compaction | vmstat 1 的 compact/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 -m、meminfo |
减少 hugepages=,留 25–30% 给 OS |
| NUMA 失衡 | 单节点内存爆、跨 NUMA 访问高 | numactl -H、numastat |
绑核绑内存,或 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 用对——它们能让你的大内存数据库,又稳又快。