PostgreSQL 必须使用 HugePages 的场景及原因
这是一个关于 CPU TLB(Translation Lookaside Buffer)命中率 和 页表内存开销 的问题。HugePages 的价值在「连接多 + 连接持久 + 对象多」的场景里被放大,本质原因是工作集超出了 TLB 的覆盖能力。
一、先理解两个底层机制
1. TLB 是虚拟地址翻译的"高速缓存"
x86_64 CPU 的 L1 dTLB 通常只有 64~128 条目 L2 TLB 一般 1~2K 条目 每次 TLB miss 都要做 页表遍历(4 级页表 = 4~5 次内存访问),单次代价 100~500 个 CPU 周期
2. TLB reach(TLB 能覆盖的内存大小)
| 256 KB ~ 512 KB | ||
| 64 MB ~ 128 MB | ||
结论:HugePage 把 TLB 覆盖范围放大 256~512 倍。
二、PostgreSQL 在"多连接 + 多对象 + 持久连接"时的内存特征
每个 backend 进程都会在本地堆里维护几类热缓存(libc malloc 出来的虚拟内存):
| relcache | ||
| catcache | ||
| typcache | ||
| plancache | ||
| Portal/QueryDesc 等 |
叠加效应(这才是关键) :
后端进程数 N × 每后端缓存大小 M = 全局热工作集
↑ ↑ ↑
多连接 多对象 总可达数 GB 甚至数十 GB
N 大(连接池保持几百~几千长连接) M 大(应用访问成千上万张表/索引) 每个后端的热数据(relcache、catcache、plancache)又分散在大量 4 KB 页面里
→ 每个 backend 的"热页"都远超 L1 dTLB 覆盖范围。每次 cache 查找几乎都 TLB miss。
三、没有 HugePages 时会发生什么
1. 持续的 TLB miss 风暴
relcache 查找:执行一条简单 SELECT * FROM t WHERE id=$1涉及 多次 catcache + relcache 查找每个查找都是几次指针解引用 → 触发若干次 TLB miss 一千个长连接 × 每秒数千次查找 → TLB miss 变成 CPU 的主要开销之一 实测在某些 PG 14/16 大连接数场景下,perf stat 看到的 TLB miss / iTLB load miss 会成为 top 3 热点
2. 页表本身占用大量内存
假设每个 backend 占用 1 GB 缓存虚拟内存,4 KB 页 → 页表条目 ≈ 25.6 万项 每项 8 字节 ≈ 2 MB 页表 一千个 backend = 2 GB 内核页表内存 而且每个 backend 的页表是各自独立的(不像共享内存可以共享页表)
3. 缺页 / swap 敏感
后端缓存增长时会有 soft page fault 申请新页 物理内存吃紧时,部分热页被换出 → 再访问时 hard fault,延迟尖刺 长连接下这种现象更难被发现,因为"看起来"是运行中的进程
4. mmap/munmap 锁竞争
PostgreSQL 用 mmap()管理本地内存上下文连接数高时,频繁的 mmap 操作会在 mmap_lock 上排队 这会直接表现为 connection storm 时的新建连接延迟尖刺
四、HugePages 在这些场景的具体收益
1. TLB miss 率断崖式下降
把后端进程的热内存区域(PGDATA 本地缓存、shared_buffers)映射到 2 MB 大页 同样 1 GB 工作集:从 25.6 万个 4 KB 页表项 → 降到 512 条 L1 dTLB 只要 8 条就能覆盖整个 1 GB 热数据 单次 cache 查找从"几次 TLB miss"变成"零 TLB miss"
2. 页表内存减少 ~250 倍
1 GB 工作集的页表:从 2 MB → 8 KB 一千个 backend 节省 ≈ 2 GB 内核内存 这部分内存可以还给 page cache / shared_buffers → 提升缓存命中率
3. 减少 mmap 锁竞争 / fork 开销
大页是预先在系统启动时 reserve 出来的连续区域 PostgreSQL 的 shared_buffers 用 huge_pages=on 时直接落到 HugeTLB fs 本地内存虽然仍用 4 KB 页(glibc 限制),但共享区热点收益已经很大
4. 物理内存锁定,绝无 swap
HugePage 是 预留+锁定 的,不进 swap 长连接累积的本地缓存即便触碰不到物理内存,至少 shared_buffers 这块绝对不会被换出 性能曲线变"平",没有抖动
5. NUMA 友好
在 NUMA 系统上,可以精确地把 HugePage 分配到指定 NUMA 节点 numactl --membind+vm.nr_hugepages按节点分配PostgreSQL 大连接场景下,避免跨节点访问内存的代价
五、量化感受(参考量级)
在 64 核 / 256 GB / PG 14 / shared_buffers=64GB / 1000 长连接 / 应用访问 ~5000 张表的场景里,开启 huge_pages=on 后典型的:
当然,前提是 shared_buffers 配置得足够大(通常 ≥ 8 GB 才值得专门配 HugePage),否则收益会被 hugepage 预留带来的内存浪费抵消。
六、什么时候 HugePage 帮助不大 / 有反效果
七、配置建议
# /etc/sysctl.d/postgres.conf
vm.nr_hugepages = <shared_buffers_MB> / 2 # 预留 2MB 页
vm.nr_overcommit_hugepages = 100 # 允许过度预留
# postgresql.conf
huge_pages = try # 或 on(on 时若 HugePage 不够会启动失败)
启动前用 cat /proc/meminfo | grep Huge 确认 HugePages_Free 足够。
一句话总结
连接多 + 持久 + 对象多 → 单个后端的"热数据"超过 L1 TLB 覆盖范围 → 每次 PG 内部的 cache 查找都付出 TLB miss 代价 → 用 HugePage 把 TLB 覆盖范围扩大几百倍,等于"白拿"一笔 CPU 周期。
HugePage 不是 PostgreSQL 专属的银弹,但是它是 少数几个能在不改一行 SQL、只动 OS+PG 几个参数就让大连接数库"避免被OOM"的手段之一。