数据库跑在Linux上,内存越大,越容易碰到一个隐蔽的性能瓶颈:TLB(Translation Lookaside Buffer)未命中。默认4KB的小页面,让一个几百GB的Buffer Pool被拆成天文数字的内存映射条目,CPU的TLB缓存根本装不下,每次地址转换都可能穿透到内存里的页表,延迟陡增。大页(Huge Pages)就是直接把这个映射粒度从4KB提升到2MB甚至1GB,用更少的页表条目覆盖更大的连续内存,TLB命中率瞬间拉满,数据库的吞吐能提升10%以上,延迟抖动明显收敛。

但大页不是开了就完事。类型选错了、分配时机不对、没跟数据库的内存模型对齐,反而会让内存碎片化更严重,甚至OOM。这篇文章从原理到操作,把内存大页的配置逻辑讲透,让数据库性能真正吃到这波红利。

TLB与内存映射的硬伤

先理解问题本质。CPU访问内存时,必须把虚拟地址转换成物理地址,这个映射关系存在页表里,页表本身也在内存。为了加速,CPU内置了TLB,相当于页表的专用高速缓存。一台服务器跑MySQL或PostgreSQL,Buffer Pool动辄上百GB,按4KB分页,会产生几千万个页表条目。而一颗CPU核心的TLB条目通常只有几十到几百个,根本覆盖不了,导致每次内存访问有极大概率TLB miss,需要去内存里多查一次甚至多次页表,额外增加几十到上百个CPU周期。OLTP场景下随机读写密集,这个开销会被急剧放大。

大页的解决思路简单粗暴:用2MB页面替代4KB,同样512GB内存,页表条目从1.34亿降到26万,TLB覆盖率高几个数量级。1GB大页更极致,但通常只适合超大内存、完全独占的负载。对于数据库,2MB大页是通用性和性能的最佳平衡点。

透明大页的坑:为什么数据库必须关掉它

Linux默认开启透明大页(Transparent Huge Pages, THP),内核在后台自动尝试合并4KB页面成2MB大页,听起来很美好,但对数据库是灾难。THP的合并过程会引发内存紧凑(compaction),导致进程被短暂挂起,延迟毛刺不可控;更致命的是,THP的分配是机会主义的,内存碎片化时分配失败会fallback到4KB,性能不一致。数据库自己管理一大块共享内存,完全知道哪里需要连续大页,却被THP的异步行为打乱节奏,经常出现“明明开了大页,性能反而抖动”的现象。

所以,生产环境数据库服务器的铁律:关闭THP,改用显式大页。关闭方式:

echo never > /sys/kernel/mm/transparent_hugepage/enabled
echo never > /sys/kernel/mm/transparent_hugepage/defrag

并写入/etc/rc.local或systemd服务保证重启生效。检查状态:

cat /sys/kernel/mm/transparent_hugepage/enabled
# 输出应为 always madvise [never]
显式大页的两种模式:hugetlbfs与共享内存

Linux显式大页依赖hugetlbfs文件系统,挂载后应用程序通过mmap映射使用。数据库使用大页通常两条路径:一是通过hugetlbfs挂载点直接分配,二是利用共享内存段结合SHM_HUGETLB标志。MySQL的InnoDB和PostgreSQL都支持直接配置大页,本质是后者:数据库启动时用shmget申请共享内存,如果系统预留了大页池,就会自动使用大页。

这里有一个极易踩的坑:大页必须提前预留。数据库启动时一次性申请一大块共享内存,如果大页池不够,就会退化到4KB小页,而且没有明显报错,只在日志里留一条不起眼的提示。所以配置大页的顺序是先预留,再启动数据库。

精确计算大页数量:宁可多不可少

预留大页的数量取决于数据库实际需要锁定的内存大小。以MySQL InnoDB为例,大页覆盖的范围是innodb_buffer_pool_size加上一些额外内存(如AHI、Change Buffer),通常按Buffer Pool的1.05倍估算。假设innodb_buffer_pool_size=128G,那么需要的大页内存约为128×1.05≈134.4GB。每个2MB大页的实际大小是2048KB,所以需要的大页数:

134.4 × 1024 × 1024 / 2048 ≈ 68813

向上取整并留一些余量,比如设置70000个。查看当前大页配置:

cat /proc/meminfo | grep Huge

重点关注HugePages_Total、HugePages_Free、Hugepagesize。预留大页:

echo 70000 > /proc/sys/vm/nr_hugepages

如果预留失败(通常因为内存碎片),需要在系统启动早期分配。修改/etc/sysctl.conf:

vm.nr_hugepages = 70000

并执行sysctl -p。更稳妥的做法是在内核启动参数里指定:

hugepages=70000

修改/etc/default/grub,在GRUB_CMDLINE_LINUX里加上这个参数,然后update-grub重启。启动阶段内存碎片最少,大页分配成功率最高。

数据库层的配置对接

预留好大页后,需要让数据库真正用上。MySQL从5.6开始支持大页,但需要显式开启。在my.cnf里:

[mysqld]
large-pages = ON

同时,必须将MySQL运行用户加入hugetlb的锁定内存限制。修改/etc/security/limits.conf:

mysql soft memlock unlimited
mysql hard memlock unlimited

这个配置决定了用户能锁定多少物理内存,大页本质上是锁定在RAM中不换出,所以memlock限制必须大于大页池大小。重启MySQL后,检查大页使用情况:

cat /proc/meminfo | grep Huge

如果HugePages_Free明显小于Total,说明大页已被使用。还可以查看MySQL错误日志,有“Using HugeTLB”字样即成功。如果仍然是小页,多半是memlock限制不够或大页预留不足。

PostgreSQL的配置更简单,设置shared_buffers后,将huge_pages参数设为on:

huge_pages = on

重启后同样检查/proc/meminfo。PostgreSQL会在启动时尝试从大页池分配,失败则报错退出,不会静默降级,这点比MySQL更安全。

NUMA架构下的大页分配策略

多路服务器上,NUMA节点间的内存访问延迟差异显著。如果数据库进程跑在Node 0,但大页全部分配在Node 1,跨节点访问会让大页的性能优势大打折扣,甚至比本地小页还慢。Linux默认的页面分配策略是“就近分配”,但大页预留是全局的,可能不均匀分布。

优化策略分两层。第一层,使用numactl绑定数据库进程到指定NUMA节点,同时确保该节点有足够的大页。查看每个节点的大页分布:

numastat -m | grep Huge

或者直接看/sys/devices/system/node/node*/hugepages/。如果发现不均衡,可以按节点预留:

echo 35000 > /sys/devices/system/node/node0/hugepages/hugepages-2048kB/nr_hugepages
echo 35000 > /sys/devices/system/node/node1/hugepages/hugepages-2048kB/nr_hugepages

第二层,数据库内部开启NUMA感知。MySQL的innodb_numa_interleave=1会让Buffer Pool交错分配在所有节点上,配合所有节点都有大页,可以平衡访问延迟。但如果数据库实例只绑定在单个节点,就应该把大页集中在该节点,避免交错。

1GB大页的适用场景与代价

2MB大页已经能满足绝大多数场景,但极端情况下,比如1TB以上的Buffer Pool,2MB大页的TLB条目仍然有50万个,而CPU的二级TLB通常只能缓存1536个条目,覆盖率依然不够。这时可以考虑1GB大页。配置方式类似,在启动参数里加上:

default_hugepagesz=1G hugepagesz=1G hugepages=512

表示预留512个1GB大页,共512GB。但1GB大页的代价也很明显:内存必须物理连续,碎片化稍重就分配失败;一旦分配,即使数据库没用满,这些内存也无法被其他进程使用,浪费巨大。而且,1GB大页不能swap,内存压力大时OOM风险更高。所以,1GB大页只适合专有数据库服务器、内存巨大且负载非常稳定的场景。

大页的监控与长期维护

大页配置不是一劳永逸。内存碎片会随时间累积,如果数据库重启,大页池可能因为碎片无法完整回收再分配,导致下次启动失败。监控要点:

1. /proc/meminfo的HugePages_Free变化趋势,如果持续减少但数据库内存没变,说明有泄漏或碎片化。

2. /proc/buddyinfo反映内存碎片程度,高阶连续内存(order 9对应2MB)的可用块数如果持续为0,大页分配将不可持续。

3. 数据库日志中关于大页的告警,MySQL的“Failed to allocate”类信息不能忽视。

长期维护策略:定期在业务低峰重启数据库,释放大页池,重新预留;或者配置内核参数vm.min_free_kbytes和vm.extra_free_kbytes,保持足够连续内存余量,减缓碎片化。还可以设置vm.compaction_proactiveness主动内存紧凑,但需谨慎调整,避免CPU开销。

性能验证:大页到底带来了什么

量化大页收益,不能只看TPS。用perf stat观察TLB miss:

perf stat -e dTLB-load-misses,dTLB-loads -p  sleep 30

计算miss率,开启大页后通常从10%以上降到1%以下。再结合vmstat的sy列(系统CPU占比),大页减少页表遍历,内核态开销下降,sy占比降低,us占比提升,CPU更多花在真正执行数据库逻辑上。延迟层面,用数据库自带的慢查询统计或p99延迟,大页带来的毛刺消除往往比平均延迟下降更明显,这对OLTP业务的价值最大。

另外,大页锁定物理内存,避免了数据库Buffer Pool被swap出去的风险。即使没有性能提升,仅凭这一点,对生产稳定性就是质的保障。很多数据库“突然变慢”的故障,根因就是内存压力大时部分Buffer Pool被换出,大页从根本上杜绝了这个问题。

大页是数据库性能调优中成本最低、收益最确定的手段之一,但前提是理解透明大页的陷阱、精确计算预留量、处理好NUMA亲和性,并建立持续的监控机制。这些细节做到位,数据库的响应速度和稳定性会有一个肉眼可见的跃升。