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

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

发布人:Minchunlin 发布时间:2025-08-18 10:48 阅读量:818


周五晚上 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 把核心内存“钉死”。有任何细节你觉得不稳,我非常乐意一起把变更单“打磨到能上凌晨两点的机房”。

目录结构
全文