上个月帮一家客户做Oracle数据库性能体检,遇到一个很典型的例子:服务器CPU使用率只有三成,但业务就是压不上去,数据库AWR里一堆latch free和CPU wait,看起来像是集群问题,又像是SQL问题。排查到最后,问题不出在SQL,也不出在存储,而是出在Linux内核的内存页面管理上——准确说,是大页没配上,小页模式下TLB开销把性能吃掉了。客户当时问了我一个问题:Linux里的大页和小页到底差在哪?为什么Oracle、DPDK都建议开大页,但换了MySQL、MongoDB又建议我们把透明大页THP关掉?这一篇就把这个问题彻底说清楚。
我自己这几年处理过不少类似案例,从数据库实例调优到DPDK数据面部署,几乎都绕不开页大小这个话题。很多人对大页的理解停留在“能用就行、抄个参数就完”,一旦遇到HugePages_Total起不来、内存预留失败、THP又和数据库命中锁冲突,就抓瞎了。所以这篇文章不只是讲区别,更会把原理、配置、坑、排查方法一次讲透,希望对正在调Linux服务器性能、做内核优化或者看运维故障的同学有帮助。
1. 从一次数据库性能排查说起:页、页表和TLB
1.1 为什么Linux内存管理默认选4KB小页
先聊个基础问题:Linux内核为什么默认把物理内存切成4KB大小的小页?在x86体系下,标准页面大小就是4KB,也就是4096字节。这个设计初衷很朴素——内存分配粒度越细,按需分配时浪费越少。进程只需要1KB数据,那就分配1个4KB页,浪费3KB;如果页面是2MB,那一次就浪费超过2MB。小页在内存利用率上天然有优势,这也是它几十年没被替换掉的根本原因。
但“按需分配”和“地址映射”是两回事。操作系统给每个进程提供了一个虚拟地址空间,进程以为自己在独占内存,实际上这些虚拟地址要通过页表(Page Table)映射到物理页。CPU访问任何一个地址,都要先把虚拟地址转换成物理地址,而页表就是这张换算表。在64位系统里,页表是四级结构:PML4、PUD、PMD、PTE,层层递进,最后一级PTE记录4KB小页的物理位置。
问题来了:如果全是4KB小页,一个大型进程需要多少页表条目?假设一个数据库进程映射了100GB内存,每个条目对应4KB,那就是2600万个条目。每条页表项按8字节算,光最后一级页表就要200MB左右内核内存。页表越多,CPU去查表的代价也越大。这里可以做一个简单的换算:4KB页表项每项覆盖4KB,2MB大页的每条PMD项覆盖2MB,同样100GB映射,大页只需要5万多个条目,页表内存从200MB级直接降到400KB级,差了整整两个数量级。
1.2 页表膨胀与TLB命中率:大页存在的底层逻辑
页表大还不是最致命的,真正要命的是TLB。TLB(Translation Lookaside Buffer)是CPU内部的高速缓存,专门保存最近用过的虚拟地址到物理地址的映射,你可以把它想成页表的“快捷速查本”。CPU访问内存时先翻这本速查本,查到了就直接过去,查不到才走完整的页表四级查找流程,这个流程叫Page Walk,代价非常高,相当于每次都要从内存里四级目录翻下去。
但TLB的容量是固定的,x86 CPU上每层TLB通常只有几十到几百个条目。这个数量在4KB小页下意味着什么?假设一个核心的L1 TLB有64项,用4KB页时它能覆盖的内存只有256KB。现在的数据库、JVM堆、DPDK数据面,工作集动不动就是几个GB甚至几十GB,TLB根本装不下,命中率可能不到90%,大量CPU周期都在重复做Page Walk,这直接反映在latch free和莫名的CPU等待上。
换成大页就不一样了。2MB大页让单个TLB条目覆盖2MB内存,同样64项条目能覆盖128MB;1GB大页则能覆盖64GB。TLB覆盖范围指数级扩大,命中率通常能拉到99%以上。这次帮客户排查的服务器就是一个典型:数据库SGA配置了30GB,却全部跑在4KB页上,TLB压力极大,光地址转换开销就让CPU白白消耗了二三十个百分点。把大页配好之后,最直观的变化就是AWR里的CPU wait明显下降,业务高峰期不那么吃力了。
2. Linux内核里的大页:两类实现和各自的脾气
2.1 静态HugeTLB:手动预留的HugePages
Linux内核里最早出现、也最稳定的大页方案是HugeTLB,就是你在/proc/meminfo里看到的HugePages_Total那套机制,通常被称为静态大页。它的思路很直接:管理员按需预留一定数量的2MB或1GB大页,这些页面提前从伙伴系统里锁定,之后只能被特定类型的应用通过shmget或mmap等方式申请使用,普通的小页分配完全不会碰到它们。
在x86-64架构上,HugeTLB有两种页面尺寸可选:默认的2MB,以及部分平台支持的1GB。1GB大页适合极端大内存场景,但预留成本也高,通常只在数据库或DPDK场景下按需使用。2MB则是主流。预留大页的前提是当时系统里有足够连续的物理内存块,2MB页需要物理地址2MB对齐的连续2MB区间,1GB页更是需要连续1GB的物理区域,这是预留失败最常见的原因之一。
静态大页最大的优点是确定性高。它不受内存碎片化影响,预留好就是自己的,不参与内存回收,也不会被swap出去。缺点是灵活性差——预留的页面即使没人用也不能还给普通内存,调整数量经常需要重启或重算内存布局。这就是为什么在生产环境里设置vm.nr_hugepages需要非常谨慎的原因:它不是普通调优参数,而是相当于给内存做了一次分区规划。
2.2 透明大页THP:内核动态帮你合并
与静态大页对应的,是Linux内核从2.6.38版本开始引入的Transparent Huge Pages,简称THP,中文翻译为透明大页。THP的思路是反过来的:应用不需要感知大页,内核在后台自动把相邻的小页合并成2MB大页,由khugepaged内核线程负责扫描和合并,同时也在内存紧张时把这些大页重新分裂回小页。
THP听起来很美好,但它“透明”的背后是两大代价。第一,合并和分裂都是后台动作,会在运行时抢占内存管理锁,对延迟敏感的应用造成意想不到的抖动。第二,THP分配大页时,如果系统里没有合适的连续物理内存,会触发直接内存回收甚至内存整理,这个动作在高峰期可能阻塞分配路径几十到上百毫秒。对于数据库这类对延迟有严格要求的应用,这种抖动无法接受。
所以行业内会出现一个看似矛盾的现象:数据库厂商都建议“开启HugePages”,但同时“关闭THP”。Oracle、MySQL、MongoDB官方文档无一例外都建议在生产环境把THP设成never。因为THP本身是不合时宜的“好心办坏事”,而静态HugeTLB才是数据库真正需要的。在后续章节里我会反复强调这一点,这个区分是避坑的核心。
3. 大页的落地步骤:从预留配置到让进程真正用上
3.1 预留与确认HugePages:别急着改,先看当前状态
先看当前系统上的大页状态,执行:
cat /proc/meminfo | grep -i huge输出里最关键的一组字段是:
| 字段 | 含义 |
|---|---|
| HugePages_Total | 系统总共预留的大页数 |
| HugePages_Free | 仍空闲的大页数 |
| HugePages_Rsvd | 已被程序申请、但尚未实际写入的页数 |
| HugePages_Surp | 超出/sys/vm/nr_hugepages设定的临时预留数量 |
| Hugepagesize | 当前大页单位大小,x86-64下通常是2048kB |
如果Hugepagesize显示2048kB,说明用的是2MB大页;如果显示1048576kB,那就是1GB大页。我之前遇到过一台机器,明明已经配了nr_hugepages=1024,但HugePages_Total一直是0,很典型的原因是内存碎片化导致预留失败。为什么要特别强调状态确认?因为很多人配置完不看状态,直接在应用上开大页参数,结果应用根本没申请到大页,性能没提升还以为是方案无效。
临时预留大页的方式:
echo 1024 > /proc/sys/vm/nr_hugepages永久生效则写入/etc/sysctl.conf:
vm.nr_hugepages = 1024然后执行sysctl -p使它生效。预留1GB大页时,需要先确认平台支持pdpe1gb标志,而且要预留足够大的连续物理内存,这个后面在问题排查里单独讲。
多路服务器上,我还建议按NUMA节点单独预留,避免所有大页都堆在某个节点上。以node0为例:
echo 512 > /sys/devices/system/node/node0/hugepages/hugepages-2048kB/nr_hugepages这样在NumaBind应用时,进程能优先使用本节点的大页,减少跨节点访问延迟。我遇到不少生产事故其实不是大页不够,而是NUMA节点分配不均造成的,这个细节容易被人忽略。
3.2 三种让用户态程序使用大页的方式
预留好大页之后,应用侧有三种常见的使用路径。
第一种是通过SysV共享内存,这是Oracle SGA最常用的方式。调用shmget时加上SHM_HUGETLB标志:
int shmid = shmget(IPC_PRIVATE, size, IPC_CREAT | 0666 | SHM_HUGETLB); void *addr = shmat(shmid, NULL, 0);Oracle从11.2开始可以直接通过初始化参数use_large_pages=only来强制使用大页,但前提是/proc/sys/kernel/shmmax要大于SGA大小,同时大页数量必须足够覆盖SGA。这里有个隐患:如果你设置的SGA是32GB,但只预留了30GB的大页,Oracle启动时会直接拒绝启动或者退化成普通小页模式,具体要看use_large_pages的设置。
第二种是通过mmap加MAP_HUGETLB标志,典型场景是DPDK。DPDK要求数据包的内存池(mempool)使用大页,因为需要给网卡DMA使用,并要求物理地址连续的大块内存。使用方式:
void *addr = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_HUGETLB, -1, 0);注意MAP_ANONYMOUS与MAP_HUGETLB组合时,文件描述符传-1即可。代码侧的大页存取与普通内存没有区别,但底层物理页是从预留的大页池里拿的。
第三种方式是挂载hugetlbfs文件系统,然后通过文件接口映射。这个方式适合想用更精细控制、又不想碰SysV共享内存的项目:
mkdir -p /mnt/huge mount -t hugetlbfs nodev /mnt/huge然后应用在/mnt/huge下打开一个文件,再mmap映射。相比前两种,hugetlbfs的好处是可以按文件管理,权限控制和生命周期都更清晰,很多中间件和大数据组件也支持这种方式。
JVM场景也有对应入口,在启动参数里加:
java -XX:+UseLargePages -XX:LargePageSizeInBytes=2m ...但要先确认JVM所在用户有没有锁定内存的权限,否则启动时会报Unable to lock JVM memory。这个权限指的是/etc/security/limits.conf里的memlock限制,数据库进程同样需要检查这个项:
oracle soft memlock unlimited oracle hard memlock unlimitedmemlock接线的坑我在生产环境见过太多次了,配好大页却发现程序与内存相关的启动直接报权限错误,浪费大量时间。
3.3 大页与共享内存参数的联动关系
落地方案时,还需要把几个系统参数联动起来看。除了上面提到的shmmax,还要看/proc/sys/kernel/shmall,它代表系统总共允许使用的共享内存页数(以4KB为单位的数量)。如果shmall太小,即使大页足够,shmat也会失败。
以一个常见规划为例:物理内存64GB,计划给Oracle SGA 32GB,使用2MB大页,需要预留多少页?
32GB ÷ 2MB = 16384 页那么设置:
vm.nr_hugepages = 16384 kernel.shmmax = 34359738368 # 32GB kernel.shmall = 8388608 # 32GB ÷ 4KB = 8388608 页这是一个值得细说的计算过程。shmall的单位是4KB,所以32GB对应8388608页。很多人只改shmmax不改shmall,共享内存分配照样受限。把这三组参数放一起一次性改好,能省掉后续不少排查时间。同样的逻辑也适用于DPDK,虽然DPDK不依赖SysV共享内存,但memlock限制必须放开,否则大页内存映射时会失败。
4. 大页的实际影响和选型边界
4.1 收益最大的几类应用场景
并不是所有应用都适合切大页。我根据自己的实践,把常见场景分成了下面几类。
数据库类应用是收益最明显的。Oracle SGA、MySQL的InnoDB Buffer Pool、PostgreSQL共享缓冲区、MongoDB的WiredTiger Cache,动辄几十GB的常驻内存,用4KB页时TLB开销非常夸张。数据库类应用通常还会配合预读、批量扫描等特性,内存访问模式集中且线性,大页能带来立竿见影的效果。
Java类应用同样受益明显。JVM的堆内存通常被划分为连续的大块区域,GC算法本身就是基于连续堆扫描设计,大页可以显著降低GC期间的对象寻址开销。不过要注意,JVM还需要关闭THP,否则后台khugepaged的合并操作和GC线程的锁竞争会导致莫名的停顿。
网络和虚拟化场景也离不开大页。DPDK这类用户态数据面需要将网卡队列直接映射到用户态,物理内存必须连续或至少是2MB对齐的块,大页是刚需而非优化项。KVM虚拟机的Guest物理内存如果映射到大页上,宿主机的TLB压力会大幅下降,虚拟机内存延迟也更稳定,这也是很多超融合平台的常规配置。
嵌入式环境里,大页更多是为了确定性和减少页表开销。实时内核或工控程序对中断响应时间有硬要求,大页能把Page Walk的不确定性降到最低。有人会说嵌入式内存小,用大页浪费,但2MB大页在嵌入式里完全可以按需预留几十个,换来的是响应延迟的可预测。
4.2 大页不是免费的午餐:代价与坑
占用的资源代价首先是内存浪费。一个程序只用了几KB内存,但如果基于2MB大页分配,那整个2MB区间就被占掉了,这部分内存无法被普通分配使用。所以对小内存进程,大页不仅无益,还会加快内存耗尽。实际选型时,我一般建议只对常驻内存大于512MB的进程启用大页。
第二个代价是不可换出。HugeTLB大页不会参与swap,页面一旦分配就无法淘汰,即使进程长期不访问也一直占着物理内存。有swap依赖的业务,或者内存水位本来就偏紧的系统,使用大页更要谨慎,否则很容易造成物理内存耗尽。内核的OOM Killer在这种情况下不一定能帮忙,因为大页本身与普通内存池相互隔离,OOM评分往往参考的是普通内存占用。
第三个代价是动态调整困难。静态大页的预留是开机决策,运行时修改nr_hugepages不一定成功,尤其是预留1GB页或者系统运行很久之后内存已经碎片化。很多运维同学希望“晚上偷偷调一下大页数量”,但往往发现改不回去,被迫重启服务器。我处理过的几个案例里,为了预留更多1GB大页,最后都只能做计划内维护窗口重启系统。
4.3 透明大页THP为什么经常被建议关闭
THP是个很有意思的机制,它想“既要又要”:既有大页的性能收益,又能动态适应负载。但它实现在内核的内存管理路径里,对运行中的进程来说完全是黑盒。数据库场景下,一个事务正在写入热点页,khugepaged线程突然把页面合并成2MB大页,这个过程要持有页表锁,可能直接引发毫秒级抖动。更严重的是内存碎片时,THP分配大页会触发compact内存整理,这在生产环境里时间不可控,我亲眼见过一个MySQL实例因为THP内存整理导致RT从几毫秒飙到几百毫秒。
MySQL官方从很早开始就建议关闭THP,Oracle也有同样的建议。关闭的方式也很简单:
echo never > /sys/kernel/mm/transparent_hugepage/enabled如果希望重启后仍生效,把它写入rc.local或者通过systemd service执行。OpenStack里也有类似的风险,nova-compute如果遇上THP,虚拟机迁移时的内存脏页率会异常高,这种坑藏得很深。
要注意的是,THP和HugeTLB并不是二选一。很多人以为关了THP就等于不用大页了,其实THP只是内核自动合并机制;关掉它之后,如果你通过HugeTLB预留页面,应用依然能显式使用2MB大页。合理的生产配置通常就是:HugeTLB按需预留,THP设为never,两条线互不干扰。
5. 常见问题与排查技巧实录
下面这张表是我最近几年在性能调优和故障排查中总结的“大页问题速查表”,先看表,后面逐条展开讲细节。
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
HugePages_Total始终为0 | 预留时内存碎片化、预留失败 | `dmesg -T |
程序申请大页成功,但HugePages_Free不减 | 应用实际没使用大页,可能走了普通内存 | 查看/proc/pid/smaps的HugePages字段 |
| 数据库启动报内存不足 | shmmax/shmall过小,或者memlock受限 | 检查共享内存参数和limits.conf |
| Oracle显示Hugepages=0 | use_large_pages未设置或大页数量不足 | show parameter use_large_pages,核对SGA与大页数量 |
| 进程RT莫名波动 | THP后台合并/分裂,或内存整理 | 查看系统日志,确认THP状态并关闭 |
| 预留1GB大页失败 | 内存碎片、未开启pdpe1gb | 检查CPU标志位,必要时重启预留 |
5.1 HugePages_Total为0:预留失败怎么定位
我遇到HugePages_Total=0的第一反应是看内核日志:
dmesg -T | grep -i huge常见输出是HugeTLB: cannot allocate memory之类。这种情况通常是两个原因:系统已经运行很久,内存碎片化严重,找不到2MB对齐的连续块;或者cgroup内存限制挡住了一部分内存。临时缓解可以先把预留页数设小一点,比如从512页开始,确认成功了再逐步扩大。生产环境如果一定要预留大量大页,最好在刚启动时或者维护窗口内做,这时候内存整体是连续的,成功率远高于跑了一段时间后的系统。
5.2 应用程序没真正用上大页怎么查
有些时候HugePages_Free一直不少,但应用侧性能没有改善。这个问题的核心在于确认进程到底有没有用大页。最直接的方法是查看进程的smaps:
grep -E "Huge|AnonHugePages" /proc/<pid>/smaps如果看到HugePages_Total: 512这种字段,说明进程确实映射了大页;如果只有AnonHugePages相关,说明走的是THP而非HugeTLB。我看到很多部署文档写“开了Huge Pages”,实际一查进程全是小页,问题自然没解决。这里要注意,smaps需要root权限才能看,普通用户看不到详细信息,但可以从/proc/<pid>/status里看HugetlbPages字段。
5.3 数据库大页分配却报内存不足的处理
Oracle报内存不足是个经典问题。即使你已经预留了足够的大页,shmat还是可能失败。第一查shmmax是否大于SGA,第二查shmall是否充足,第三查memlock是否放开。这三项的顺序我建议从shmmax开始,因为它是数据库最外层的一道门槛。之前处理过一个案例,客户把shmmax改成了40GB,但shmall还是默认值2GB,Oracle启动报ORA-27102,改成8388608页后一次通过。这套排查思路放到MySQL和MongoDB上也适用,它们都有对应的共享内存限制参数,只是名称不同而已。
5.4 THP导致的抖动怎么确认和规避
THP引起的性能抖动比较隐蔽,原因是它不直接报错,而是让系统“偶尔变慢”。我一般这样定位:业务出现周期性RT抖动时,同时看系统日志里有没有mm compaction相关事件,或者khugepaged线程的CPU占用是否有规律地波动。如果确认是THP的问题,先执行:
echo never > /sys/kernel/mm/transparent_hugepage/enabled再观察几分钟。我自己有不少生产案例,把这个开关改成never之后,抖动立即消失,说明问题定位准确。值得注意的是,THP的never模式在部分内核版本里还会保留对shmem的影响,需要顺带检查/sys/kernel/mm/transparent_hugepage/defrag,如果defrag是always,也要一并改成never。
5.5 从监控指标预警大页耗尽
最后分享一个监控层面的经验。大页的监控不建议只看HugePages_Free,更要关注HugePages_Rsvd和HugePages_Surp这两个字段。Rsvd表示已经被应用声明、但尚未真正使用的页面,它是数据库等预分配应用的“已承诺占用”,一旦涨上去就很难降回来。Surp则是超过nr_hugepages设置之外临时多用的页数,出现它通常意味着系统在紧急情况下动用了超预留机制,这是资源失控的前兆。我惯用的监控阈值是:HugePages_Free长期低于总页数的20%,并且Rsvd在持续增长,基本可以触发预警,优先排查应用是否内存泄漏或者SGA配置得过大。
这套排查方法我跑过很多环境,不管是几百GB的Oracle RAC服务器,还是小规模的嵌入式设备,都能对齐到一个基本问题:大页的预留、使用和回收状态是否跟预期一致。大页并不是越用越大或者越开越好的路子,它更像内存池规划,提前想清楚谁会吃大页、吃多少、什么时候还,部署起来就不慌。
我个人的体会是,真正把大页调明白的人,对Linux内存管理的理解都会上一个大台阶。因为你会被迫搞清楚页表层级、TLB、伙伴系统、NUMA分配这些底层机制,这些知识反过来又会帮你解决很多看似无关的问题,比如Go程序的内存忽高忽低、Redis fork时的延迟尖刺。这套技能在Linux服务器运维里非常实用,值得花时间系统过一遍。