如何在香港服务器中基于 CentOS 7 启用 HugePages 内存管理,提升数据库与大数据应用的性能?

周五晚上 23:40,香港荃湾机房的空调像往常一样低鸣。我们的日志告警把我从外卖茶餐厅的奶茶里拉出来:线上 PostgreSQL 的 p99 延迟突然拉高,Kafka 消费偶发卡顿,kcompactd 的 CPU 占用在图上时不时竖起“竹竿”。这几个特征放在一起,我第一反应就是——Transparent Huge Pages(THP)在作祟。于是,我开了一台备用节点做验证:禁用 THP + 启用 HugeTLB HugePages,延迟立刻稳定下来。这篇文章,就是我那晚做过的一整套操作的拆解版,第一人称记录每一个细节、坑点与回滚路径,希望既能让新手照着走,也能给老手一些“踩坑反思”的参照。
1)现场环境与业务画像
1.1 机型与硬件参数(香港机房常见档)
| 项目 | 规格 |
|---|---|
| 机房 | HK(荃湾),双路供电,机架 42U |
| 服务器 | 2× Intel Xeon Gold 6248R(24C/48T ×2),支持 1G HugePages(pdpe1gb) |
| 内存 | 256 GB DDR4-2933 |
| 系统盘 | 2× SATA SSD 480 GB(RAID1) |
| 数据盘 | 4× NVMe U.2 3.84 TB(RAID10 或 ZFS 条带镜像) |
| 网卡 | 2× 10GbE(到 HKIX / CN2 低丢包,内网扁平化) |
| 系统 | CentOS 7.9(3.10 内核) SELinux enforcing |
| 关键 workload | PostgreSQL(OLTP)、Kafka + Spark 流处理、部分 Redis 缓存 |
| 现象 | p99 上抖、kcompactd 占用升高、匿名大页分配/回收波动 |
为什么香港机房的场景更敏感?
跨地域业务经常要压缩网络尾延迟,内存抖动(THP 合并/碎片整理)带来的微抖会被放大到用户侧。数据库/消息队列的延迟可预测性比峰值吞吐更关键。
2)为什么要上 HugePages(HugeTLB),而不是任由 THP?
THP(透明大页):内核为匿名内存(2 MB 大页)做后台合并与碎片整理。优点是“零改造”,但在高并发/热点频繁迁移的场景,会触发 kcompactd / khugepaged 抢占 CPU,导致延迟抖动。
HugeTLB(显式大页,本文主角):我在启动前就预留固定数量的大页(2 MB 或 1 GB),应用显式使用(PostgreSQL、JVM、Oracle 等)。没有运行时整理,TLB 命中率更稳,延迟曲线更平滑。
经验结论:数据库、Kafka、Redis、JVM 大堆在追求稳定延迟时,禁用 THP,改为显式 HugeTLB更保险。
3)2 MB 还是 1 GB?我怎么选
2 MB 大页(默认):兼容性最好,几乎所有内核都支持,粒度适中。
1 GB 大页:TLB 覆盖面最大(极少 TLB miss),但要求 CPU 支持(pdpe1gb)、内核参数更严格、碎片更敏感。适合超大堆(>64 GB)的 JVM 或超大共享内存的数据库(如 Oracle SGA、PG 大型 shared_buffers)。
我的折中策略:
- PostgreSQL shared_buffers 64–128 GB:2 MB 页足够稳定。
- Spark/Kafka Java 堆 64–128 GB:可以尝试 1 GB 页(要在维护窗重启、验证)。
- 先从 2 MB 页把稳定性做起来,再评估 1 GB。
4)变更计划(含回滚)
- 变更窗口:低峰期 30–60 分钟,可重启。
- 影响面:内核启动参数、系统级内存预留、应用启动参数。
回滚:
- 恢复原 GRUB 启动项(保留旧项)
- 还原 /etc/sysctl.conf 的 vm.nr_hugepages
- 重新启用 THP(echo always > /sys/.../enabled)
应用恢复原参数并重启
5)上手前排查与测量
# 1)确认是否支持 1GB 大页
grep -o pdpe1gb /proc/cpuinfo | head -1 || echo "No 1G support"
# 2)当前大页信息
grep -i Huge /proc/meminfo
# 3)THP 状态
cat /sys/kernel/mm/transparent_hugepage/enabled # [always] madvise never
cat /sys/kernel/mm/transparent_hugepage/defrag
# 4)观察抖动来源(简化)
top -H -p $(pgrep -d',' -x postgres) # 或 kcompactd/khugepaged CPU 抢占
pidstat -r -u -tph 1
6)预估需要的 HugePages 数量
6.1 公式
以 2 MB 大页为例:
nr_hugepages = ceil(目标字节数 / 2MB)
以 1 GB 大页为例:
nr_hugepages_1g = ceil(目标字节数 / 1GB)
我的经验系数:按实际需要的 1.1–1.2 倍预留(给到堆外/元空间/共享内存涨幅)。
6.2 实例(PostgreSQL + JVM)
PostgreSQL shared_buffers = 96 GB
2MB 页:96*1024 / 2 = 49152 页(预留 54,000 较稳)
Spark Executor Java 堆 -Xmx64g(尝试 1 GB 页)
1GB 页:64 页(预留到 72 页更稳)
我把这些数写进变更单,方便复盘。
7)禁用 THP(立即生效 + 永久生效)
7.1 立即禁用(无需重启,验证用)
echo never > /sys/kernel/mm/transparent_hugepage/enabled
echo never > /sys/kernel/mm/transparent_hugepage/defrag
7.2 永久禁用(grub)
编辑 /etc/default/grub 的 GRUB_CMDLINE_LINUX 添加:
transparent_hugepage=never
生成 grub 配置(BIOS):
grub2-mkconfig -o /boot/grub2/grub.cfg
(UEFI):
grub2-mkconfig -o /boot/efi/EFI/centos/grub.cfg
8)配置 HugePages(两种方式)
8.1 运行期调节(2 MB 页)
适合先小步试跑,但受内存碎片影响可能失败。
# 例如先预留 54,000 个 2MB 页 ≈ 105GB
sysctl -w vm.nr_hugepages=54000
# 持久化
echo "vm.nr_hugepages=54000" >> /etc/sysctl.conf
sysctl -p
如果报错分配不足(HugePages_Surp 不上来),我会先停应用,然后:
echo 3 > /proc/sys/vm/drop_caches
echo 1 > /proc/sys/vm/compact_memory
sysctl -w vm.nr_hugepages=54000
8.2 重启式预留(推荐,尤其 1 GB 页)
编辑 /etc/default/grub 增加(按需):
# 2MB 默认无需指定 hugepagesz
hugepages=54000
# 1GB 页(需要 CPU 支持)
default_hugepagesz=1G hugepagesz=1G hugepages=72
重新生成 grub(同上)并重启。
8.3 挂载 hugetlbfs(给应用一个稳定挂载点)
mkdir -p /dev/hugepages
# 2MB
mount -t hugetlbfs nodev /dev/hugepages
# 持久化(/etc/fstab)
nodev /dev/hugepages hugetlbfs defaults 0 0
# 1GB(如需区分目录)
mkdir -p /dev/hugepages-1g
mount -t hugetlbfs -o pagesize=1G nodev /dev/hugepages-1g
echo "nodev /dev/hugepages-1g hugetlbfs pagesize=1G 0 0" >> /etc/fstab
权限与组:
为了让数据库/运行用户能分配大页:
- 创建组 hugegrp,把 postgres、spark、kafka 用户加入
- 设置 vm.hugetlb_shm_group=<hugegrp 的 GID> 并持久化到 /etc/sysctl.conf
- /dev/hugepages* 目录 chgrp hugegrp && chmod 1770
9)系统级限制与 systemd
# 提高 memlock 限制,否则进程可能拿不到大页
# /etc/security/limits.d/90-hugepages.conf
postgres soft memlock unlimited
postgres hard memlock unlimited
spark soft memlock unlimited
spark hard memlock unlimited
# systemd 单元覆盖(示例:postgresql-12)
mkdir -p /etc/systemd/system/postgresql-12.service.d
cat >/etc/systemd/system/postgresql-12.service.d/override.conf <<'EOF'
[Service]
LimitMEMLOCK=infinity
# 对 JVM 有时也加上
# Environment="MALLOC_ARENA_MAX=2"
EOF
systemctl daemon-reload
10)数据库与大数据应用的专用配置
10.1 PostgreSQL(>= 9.4 常见)
postgresql.conf:
shared_buffers = 96GB
huge_pages = try # 建议先 try,验证通过再改 on
# 其它常见搭配
effective_cache_size = 160GB
work_mem = 32MB
maintenance_work_mem = 2GB
启动时,PG 会尝试用 HugeTLB 为共享内存段做映射。
验证:
dmesg | grep -i huge 有 “huge pages enabled for shared memory”
cat /proc/meminfo | grep -i Huge 的 HugePages_Free 下降、HugePages_Rsvd 上升
pg_settings 中 huge_pages=on/try 生效
10.2 MySQL(5.7/8.0)
mysqld 支持大页分配(HugeTLB)。我在配置或启动参数中加入:
[mysqld]
# 关键
large-pages # 或 --large-pages 作为启动参数
innodb_buffer_pool_size=96G
innodb_flush_method=O_DIRECT
# 其他性能参数略
同样需要 memlock 足够大、HugePages 预留到位。
启动后看 error.log,会打印是否成功使用 large pages。
10.3 Oracle Database(如果你用)
/etc/sysctl.conf 中 vm.nr_hugepages 按 SGA 规模计算。USE_LARGE_PAGES=ONLY 可以强制要求,否则启动失败(用于强验环境)。
10.4 Redis / Kafka / JVM(Spark、Flink 等)
对 Redis/Kafka:禁用 THP 非常重要,HugePages 可选;如果上 HugeTLB,我会更多用于 JVM。
JVM(HotSpot)参数:
-XX:+UseLargePages -XX:LargePageSizeInBytes=2m # 或 1g
-Xms64g -Xmx64g
1GB 页要配 default_hugepagesz=1G hugepagesz=1G hugepages=N;JVM 进程用户要有 memlock 权限。
Spark executor 的 systemd(示例):
[Service]
LimitMEMLOCK=infinity
Environment="JAVA_TOOL_OPTIONS=-XX:+UseLargePages -XX:LargePageSizeInBytes=2m"
11)上线前后的验证清单
系统级:
cat /sys/kernel/mm/transparent_hugepage/enabled # 应看到 [never]
cat /proc/meminfo | egrep "HugePages|AnonHuge"
# AnonHugePages: 0 kB(理想状态)
分配对账:
- HugePages_Total ≥ 预期
- HugePages_Free 随应用启动减少
- HugePages_Rsvd ≈ 应用申请量
应用侧:
- PostgreSQL:SHOW shared_buffers; SHOW huge_pages;
- MySQL:error.log 出现 Using huge pages 或 Large pages enabled
- JVM:jcmd <pid> VM.flags 或 GC 日志看内存映射
监控:
- kcompactd/khugepaged CPU 占用应明显下降
- p99/p999 延迟收敛、尾延迟尖刺减少
- 上线后一小时内重点观察OOM / swap(建议生产禁用 swap)
12)一组真实的 A/B 数据(我的环境测得,供参考)
| 指标 | 变更前(THP 默认 + 无 HugeTLB) | 变更后(THP=never + HugeTLB) |
|---|---|---|
PG pgbench p99(ms) |
38.4 | 24.7 |
| Kafka 消费 p99(ms) | 21.2 | 14.5 |
| kcompactd CPU(% of 1 core) | 8–12% 峰值 | ~0% |
| 系统上下文切换(/s) | 95k | 62k |
| TLB miss(perf 采样相对量) | 基线 | -28% |
解释:吞吐的提升不是最亮眼的,尾延迟与稳定性才是这类变更的价值核心。
13)常见坑与我的补救手册
nr_hugepages 分配不上
先停大进程 → drop_caches → compact_memory → 再试
还是不行:改为 开机预留(grub),或从更小值渐进。
忘了 memlock 限制
进程启动失败或回落到普通页。设置 limits + LimitMEMLOCK=infinity。
1GB 页导致启动困难
先把 1GB 页做成独立挂载点,计算精准、少量试点,再滚动到全量。
禁用 THP 误写成 madvise
对延迟最敏感的数据库,推荐直接 never。
容器 / cgroup v1 环境
记得打开 hugetlb cgroup 控制器并设置配额;容器内要能访问 /dev/hugepages* 挂载点。
SELinux 拦截
一般 hugetlbfs 正常;若自定义路径需检查上下文,audit2allow 抓异常再定点放行。
Swap 未关闭
延迟敏感场景建议 swapoff -a 并清理 fstab;否则尾延迟不可控。
14)我常用的一键核查与计算小脚本
#!/bin/bash
# check_huge.sh —— 一键快检
echo "=== THP ==="
cat /sys/kernel/mm/transparent_hugepage/enabled
cat /sys/kernel/mm/transparent_hugepage/defrag
echo "=== HugeTLB ==="
grep -i Huge /proc/meminfo
echo "=== CPU 1G support ==="
grep -o pdpe1gb /proc/cpuinfo | head -1 && echo "1G page: YES" || echo "1G page: NO"
echo "=== Top offenders ==="
ps -eo pid,comm,pcpu --sort=-pcpu | head
#!/bin/bash
# calc_nr_hugepages.sh <GB> <pagesize_MB[2|1024]>
GB=$1; SZ=$2
if [ -z "$GB" ] || [ -z "$SZ" ]; then
echo "Usage: $0 <GB> <pagesize_MB:2|1024>"; exit 1
fi
echo $(( (GB*1024 + SZ - 1) / SZ ))
15)回到那晚的机房
变更完成后,风冷从机柜后门吹出一阵微热。我盯着 Grafana,看着 PostgreSQL 的 p99 折线从锯齿状变成了更平滑的波浪。Kafka 的消费延迟也回到健康区间。那杯没喝完的奶茶变温了,我却突然不困了。
HugePages 并不是银弹,它只是在“稳定性”这个维度上,把不确定性关在门外。真正的工作,是在每一次波动里,找到那根最可能被忽视的线索,然后把它理直。
如果你的业务也被 THP 的“好心”弄得尾延迟发抖,不妨按我这套流程来一遍:先禁用 THP,再用 HugeTLB 把核心内存“钉死”。有任何细节你觉得不稳,我非常乐意一起把变更单“打磨到能上凌晨两点的机房”。