简介:本资源为一份PDF格式的Linux HugePages配置指南,面向需要优化大内存应用性能的系统管理员、DBA及运维工程师,尤其适合为Oracle数据库等场景快速启用大页内存。内容从memlock限制、vm.nr_hugepages参数设置到hugepages_settings.sh脚本的使用均有清晰阐述,并给出sysctl与重启生效的完整流程,还特别整理了ASM/AMM兼容性、SGA与pga_aggregate_target计算等关键注意事项,帮助读者规避常见误区。资源包共1个PDF文件,大小约53KB,便携易用,可在本地或服务器上随时查阅。目前已有1702人学习浏览,适合具备一定Linux基础、希望快速掌握HugePages配置实操的读者作为简明参考手册。
1. HugePages让数据库和中间件性能翻倍:大多数Linux服务器其实没吃透这个参数
我给一台跑着Oracle的服务器做性能排查时,top 里 CPU 的 sy 列长期占着 30% 以上,数据库业务量却不大。查了一圈,最后定位到问题不在 SQL,而是内核的页表开销把 CPU 烧掉了。这种情况在 Linux 服务器上太常见了:内存越配越大,内核默认的 4KB 页帧让页表项膨胀到千万级,TLB 缓存根本装不下,访问内存时 CPU 只能不断去查页表。HugePages(大页)就是把这个粒度从 4KB 放大到 2MB 甚至 1GB,让页表项数量直接砍掉两个数量级,TLB 命中率上去了,CPU 自然就闲下来。
这篇文章要讲的是怎么在 Linux 系统下快速把 HugePages 配置起来。我会从原理讲到落地命令,再到参数调整和验证方法,最后把我在生产环境里踩过的坑一条条列给你。适合正在维护数据库、中间件、JVM 大堆服务,或者做嵌入式 Linux 调优的运维和开发。你照着步骤走,半小时内能把一个节点的大页池建起来并验证到位。
2. 为什么要快速配置HugePages:页表开销与TLB命中率背后的账
2.1 页表膨胀与TLB miss:一个4KB页帧的真实开销
Linux 内核默认把物理内存切成 4KB 的页帧来管理。一个进程想访问虚拟内存,CPU 得通过多级页表把虚拟地址翻译成物理地址,这中间每一级查表都在消耗时间。最要命的是,内存一多,页表本身就会长得非常夸张:一台 128GB 内存的机器,如果全用 4KB 页,光页表项就要 3200 万个,每张页表项就算只占 64 字节,也要吃掉将近 2GB 的内存来放页表。
更直接的代价在 TLB(Translation Lookaside Buffer)上。TLB 是 CPU 内部缓存最近用过的虚拟地址到物理地址映射的硬件,大小通常只有几百到几千条。进程每访问一个没有命中 TLB 的页,都要走一遍完整的多级页表查表流程,这种 miss 在 CPU 的 perf 统计里会表现为很高的 cycle 开销,在 top 里就是 sy 列居高不下。数据库这类大内存进程,工作集会高频访问分布在大范围地址上的数据,4KB 页会让 TLB 频繁失效,CPU 大量时间花在内核态的地址转换上,业务线程反而抢不到执行资源。
HugePages 把页帧放大到 2MB 或 1GB,页表项数量直接缩减为原来的 1/512 或 1/262144。对 Oracle SGA、PostgreSQL shared buffers、JVM 堆这类动辄几 GB 到几百 GB 的常驻内存,使用大页后 TLB miss 率能成数量级下降。我接触的项目里,配置完 HugePages 的数据库服务器,CPU sy 占比普遍能降下来好几个百分点,这在高并发场景下直接转化为吞吐量提升。
2.2 什么场景值得配HugePages:数据库、JVM与Redis的取舍
不是所有 Linux 系统都需要配 HugePages。我一般用一条经验判断:单进程常驻内存超过 1GB,并且对延迟敏感,就值得配。这里说的“常驻”不是任务管理器里显示的 RSS,而是这个进程主动向系统申请并且会持续占用的那部分内存。
最典型的受益者是共享内存型数据库。Oracle 把 SGA 放在共享内存段里,PostgreSQL 的 shared_buffers 和 WAL buffer 也是大块常驻内存,这些进程配 HugePages 后收益最明显。其次是 JVM 系中间件,WebLogic、Tomcat 配了几十 GB 堆内存的话,大页能减少 GC 之外的额外 CPU 消耗。再就是自己做 mmap 映射大文件的搜索或缓存服务,页表项巨大时同样值得调整。
反过来,有些场景我反而不建议开。容器环境里每个 Pod 被 CGroup 限制到几百 MB 内存的时候,大页池的 2MB 粒度会导致无法均匀分配,内存碎片反而更严重。单纯跑 Nginx 这类进程本身内存占用很小的服务,配大页收益微乎其微,还额外占用了不可换出的内存池。所以配置之前先想清楚:你这个进程到底是不是大内存、常驻型、延迟敏感的。这三个条件缺两个,就没必要折腾。
值得注意的是,HugePages 和 Linux 内核的透明大页(THP)是两套机制。前者靠vm.nr_hugepages显式分配池子,进程必须主动用mmap或共享内存的方式申请大页;后者是内核自动把 4KB 页合并成 2MB 页,对应用透明。对企业数据库来说,THP 的在线合并行为在内存碎片多时反而可能引发性能抖动,这点我会在避坑章节专门展开。
2.3 为什么不建议直接改nr_hugepages就完事:快速不等于随便
只要一条sysctl -w vm.nr_hugepages=1024就能让内核去分配大页池,听上去很简单,但生产环境里我从来不会这么干。原因有三层:
第一,nr_hugepages写进去只是把“期望值”告诉内核。系统当前内存碎片较多时,内核可能只分出一部分页,剩下的处于deferred状态挂在一边等内存整理。你看到HugePages_Total小于期望值,以为没生效,实际上内核在后台慢慢挤;更麻烦的是你没法随时知道它到底分到哪一步了。
第二,大页内存的页帧是从连续物理内存里切出来的,而且这部分内存一旦分配给进程就不可换出(swap)。也就是说,你分出去的页,要等进程主动释放才会回到池里。如果一开始没算好,把池子建大了,想缩回来却发现进程还没退出,内存就卡在那儿白白占着。
第三,只调nr_hugepages不处理权限和挂载点,等于把池子建好了却不给进程开门的钥匙。Oracle 这类数据库通常是专用系统用户跑的,不是 root,不配vm.hugetlb_shm_group或ulimit -l权限,shmget 调用会直接被拒绝。
所以我给团队定的配置顺序永远是:先确认目标进程的真实内存需求,按这个需求算池子大小,然后写入sysctl.d的持久化配置,再处理权限与挂载,最后用/proc/meminfo和进程的smaps做双重验证。每一步都有对应的检查命令,缺一步,后面就得多花几倍时间去排障。这个顺序也是本文第 3 章的完整落地路径。
3. 快速配置HugePages的完整步骤:从检查现状到生效验证的六条命令
3.1 第一步:用grep确认当前大页状态与物理内存布局
配置之前先看这台机器现在处于什么状态。有些系统镜像或云厂商 AMI 已经在/etc/sysctl.conf里改过内核参数,不查清楚就继续写,可能出现两处配置互相覆盖的情况。
# 查看当前系统的大页分配总量、空闲量与池大小 grep -i huge /proc/meminfo # 直接读取内核当前的池参数 cat /proc/sys/vm/nr_hugepages cat /proc/sys/vm/nr_overcommit_hugepages # 确认透明大页的开启状态,后面必须处理 cat /sys/kernel/mm/transparent_hugepage/enabled第一条命令里grep -i huge /proc/meminfo会输出HugePages_Total、HugePages_Free、HugePages_Rsvd、Hugepagesize几个关键字段。Hugepagesize是当前架构下的大页粒度,x86_64 上默认是 2048 kB。如果HugePages_Total一直是 0,说明这台机器从来没显式分配过大页池。
读取/proc/sys/vm/nr_hugepages得到的是当前大页池的目标数量,注意这是“期望值”,不等于实际已分配的页数。nr_overcommit_hugepages是超额分配的上限,意思是在期望池之外,内核还允许临时多给一些页出去,这个参数在业务峰值时能救急,但不建议设太大,否则大页内存可能挤占普通内存。
第三条关于透明大页的命令,输出通常是always [madvise] never这种方括号形式,方括号里就是当前生效的模式。数据库场景下我后面会要求你把 THP 关掉,如果这里显示[always],现在就要记下来,那是最容易被忽略的前置条件。
3.2 第二步:按业务内存算推荐值并写入sysctl.conf
计算推荐值没有通用公式能一刀切,我用的方法很简单:先看目标进程当前的最大常驻内存,然后向上取整到 2MB 的整数倍,再加 2% 的余量。比如一台 Oracle 服务器,SGA_TARGET配的 16GB,进程加 PGA 后 RSS 峰值约 17GB,那大页池就按 18GB 建,换算成 2MB 页就是 9216 个页。
# 把大页池设为 9216 个 2MB 页(约 18GB),并允许临时超额 512 个页 echo 'vm.nr_hugepages = 9216' >> /etc/sysctl.d/99-hugepages.conf echo 'vm.nr_overcommit_hugepages = 512' >> /etc/sysctl.d/99-hugepages.conf echo 'vm.hugetlb_shm_group = 54321' >> /etc/sysctl.d/99-hugepages.conf # 重载 sysctl 配置并确认生效 sysctl -p /etc/sysctl.d/99-hugepages.conf为什么用/etc/sysctl.d/99-hugepages.conf而不是直接改/etc/sysctl.conf?因为sysctl.d目录是 kernel 推荐的覆盖式配置入口,文件名以 99 开头能确保在通用配置之后被读取,后续系统升级也不会被覆盖。vm.nr_hugepages是池子大小的基准页数,vm.nr_overcommit_hugepages是允许超额分配的页数,vm.hugetlb_shm_group是允许使用大页共享内存的组 ID,我这里假设数据库用户所属的组 ID 是 54321,你需要用id oracle这类命令查到真实值再替换。
执行sysctl -p后,内核会尽力去分配指定数量的大页。这里有个常见等待:因为内存碎片,你运行完命令后立刻去看/proc/meminfo,HugePages_Total很可能小于 9216。这是正常现象,内核后续会在内存整理完成后补上,但你不应该等它慢慢补,应该主动做一次内存规整,我放在第 5 章坑 2 里讲。
3.3 第三步:挂载hugetlbfs,并验证实例进程成功映射
大页池建好之后,还要给应用提供使用它的入口。最常见的做法是挂载hugetlbfs文件系统到/dev/hugepages,数据库或 JVM 会在这个路径上创建文件来做内存映射。另外要给目标进程的属主配置好权限,否则非 root 用户拿不到大页。
# 创建挂载点并挂载 hugetlbfs,权限 1770 表示组内共享、其他用户不可访问 mkdir -p /dev/hugepages mount -t hugetlbfs hugetlbfs /dev/hugepages # 写入 fstab,保证系统重启后挂载不丢 echo 'hugetlbfs /dev/hugepages hugetlbfs mode=1770,gid=54321 0 0' >> /etc/fstab # 重载让 hugetlb_shm_group 生效,然后检查一次 sysctl -p /etc/sysctl.d/99-hugepages.conf grep -E 'HugePages_Total|HugePages_Free|HugePages_Rsvd' /proc/meminfomount命令中的mode=1770和gid=54321两个参数是生产环境必须写的。1770中的 1 是 sticky bit,确保目录里每个进程只能删除自己创建的文件;770表示只有组内用户能读写执行,防止其他系统用户往大页文件系统里塞东西;gid必须和上面vm.hugetlb_shm_group保持一致。如果漏掉gid,默认属主是 root 组,数据库专用用户就碰不到这个大页池了。
验证映射是否成功,最好的办法是重启目标实例然后看进程实际的页映射。比如 Oracle,启动后检查单个后台进程的smaps文件,里面应该出现大量 2048 kB 的映射段。如果进程确实起来了但映射的是普通 4KB 页,说明权限或参数没生效,这时候再回头看vm.hugetlb_shm_group是否和进程的属组一致。这也是为什么我强烈建议你在配置前先把id查清楚,组件一旦跑起来,权限错位很难一眼看出来。
4. 配置HugePages后的验证与参数调优:别只看free,要看实际映射
4.1 用/proc/meminfo确认大页分配成功
free -h只能看到常规内存的分配概况,看 HugePages 必须回到/proc/meminfo。配置完成后,我每次上线前都会固定跑这一组命令,确认池子状态稳定,而不是刚配置完那一瞬间的数字。
grep -E 'HugePages_Total|HugePages_Free|HugePages_Rsvd|HugePages_Surp|Hugepagesize' /proc/meminfo当一个 18GB 的大页池建好后,你应该看到HugePages_Total等于你配置的页数(比如 9216),HugePages_Free接近同样的数值——这些页还没被进程领取。HugePages_Rsvd是被共享内存段预留给具体进程的页数,当 Oracle 启动后这个值会往上涨,而HugePages_Free会相应往下掉。如果HugePages_Total长时间小于你写的目标值,就要怀疑内存碎片和nr_hugepages期望值之间的差距,这属于没分到位,不是配置格式错误。
HugePages_Surp这个字段很多人忽略,它记录的是超过nr_hugepages限制额外分配出去的页数。它涨起来说明nr_overcommit_hugepages在起作用,如果它长期大于 0,说明你的基准池子设小了,业务在靠超额分配撑着,这种情况下需要把基准值往上调,而不是依赖超额池一直兜底。
Hugepagesize必须确认是 2048 kB,除非你在 x86 分页上通过内核启动参数改成 1GB 大页。如果你的数据库进程是按照 2MB 页来计算的,而这里显示的是 1GB,那你建的池子页数对应的内存总量会完全错位,业务直接分配失败。我在一台上了新补丁的服务器上遇到过 GRUB 默认参数被改的情况,所以这个值每次上线前我都会亲自看一眼。
4.2 用smaps检查进程实际映射
池子建好只能说明内核把这批页准备出来了,还不能证明业务进程真的用上了。判断大页是否真正生效,要看进程的页表映射。每个进程的映射明细都放在/proc/<pid>/smaps里,找到目标服务的主进程后,看里面有没有 2048 kB 的映射段。
# 找到 oracle 的主进程 PID pgrep -f 'ora_pmon|oracle' | head -1 # 查看该进程 smaps 中带 huge 标记的行 grep -B5 -A5 -i huge /proc/<pid>/smaps | head -60smaps里与 HugePages 相关的映射行有几种典型表现。对 Oracle 来说,SGA 映射到的文件路径通常包含/SYSV或直接是大页文件系统/dev/hugepages/...,对应的映射段大小是 2048 kB 的整数倍;对 Java 应用来说,如果你通过-XX:+UseLargePages开启了 JVM 大页支持,smaps中会出现大量 2048 kB 的AnonHugePages之外的显式大页映射。
一个关键判断点:AnonHugePages不等于HugePages。前者可能是透明大页(THP)自动合并出来的 2MB 匿名页,后者才是走hugetlbfs或共享内存显式分配的 HugePages。如果你的数据库确实开了大页,却在smaps中只看到AnonHugePages而看不到显式大页映射段,那就要按第 5 章坑 1 的方向去排查了。
4.3 参数微调:overcommit、shm_group与sysctl持久化的细节
配置完成后,线上运行一段时间,你还需要根据实际观测微调参数。最常见的两个调整目标:vm.nr_overcommit_hugepages和vm.hugetlb_shm_group。前者在业务峰值期如果看到HugePages_Surp频繁大于 0,可以适当增加额度;后者在你新增了多个业务用户时,要把所有需要共享大页的组都加进去,或者统一权限模型。
# 临时调高超额分配额度,先观察再决定是否写持久化 sysctl -w vm.nr_overcommit_hugepages=1024 # 确认修改已生效 cat /proc/sys/vm/nr_overcommit_hugepages # 如果需要持久化,追加到已有配置文件并重载 echo 'vm.nr_overcommit_hugepages = 1024' >> /etc/sysctl.d/99-hugepages.conf sysctl --system这里要特别说明sysctl -w和写配置文件再sysctl --system的区别。-w只改运行时值,重启后丢失;写配置文件再执行sysctl --system才是完整的持久化流程。不少运维图省事只跑-w,结果节点重启后大页池缩回默认值,数据库起来直接失败,这就是典型的“配置失效”事故。
还有一个容易被忽略的细节:sysctl --system会按目录遍历顺序应用所有配置,文件名前缀决定了加载顺序,99 这个编号在大多数发行版里都是最后加载的,保证你的值能覆盖可能存在的默认配置。如果你在别的路径看到同名参数被改了却不知道为什么,多半是加载顺序问题,用sysctl -a | grep huge可以查实际生效值。
5. 配置HugePages的避坑记录:透明大页、内存碎片和数据库参数三处翻车点
5.1 坑1:THP没关,业务进程照样翻车
现象:按照标准流程配置完vm.nr_hugepages,挂载也做了,数据库实例成功启动,但top里 CPU 的 sy 占比没有明显变化,查看进程smaps时发现映射的不是显式大页而是AnonHugePages。
原因:系统开启了透明大页(THP),内核的khugepaged后台线程会把进程的匿名页自动合并成 2MB 的大页。这个动作看起来让进程“用上了大页”,但合并过程需要扫描进程内存,并在内存碎片时不停做迁移,CPU 开销反而更高。更关键的是,THP 的页是自动申请的,跟显式 HugePages 池完全是两条路径,你建的池子根本没被进程摸到,业务没有得到预期收益。
解决:在 GRUB 内核启动参数里加transparent_hugepage=never,同时关闭khugepaged服务,然后重启节点。需要说明的是,部分发行版(比如某些版本的 CentOS/RHEL)在启动参数设了之后还需要检查/sys/kernel/mm/transparent_hugepage/enabled确实显示never才算成功,光改 grub 不执行grub2-mkconfig等于没改。具体命令我在第 6 章给出。
5.2 坑2:内存碎片导致HugePages分配失败
现象:修改sysctl -w vm.nr_hugepages=9216后,/proc/meminfo显示HugePages_Total只有 6000 多,而且dmesg日志里能看到Huge Pages allocation failed这类信息,数据库启动时因共享内存不足报out of memory。
原因:大页池需要从物理内存的连续区域中切出 2MB 的页帧,系统运行时间长了之后,内存被拆得七零八落,没有足够多的连续 2MB 块满足大页分配。内核此时会把这部分请求放在deferred状态,等内存整理后再补,但数据库启动等不了那么久。
解决:主动触发一次内存规整,把内存碎片压缩掉,给大页分配创造条件。这在生产环境是需要谨慎的操作,我一般选在业务低峰期执行。
# 先触发内存压缩(需要 root),执行后立即检查大页分配 echo 1 > /proc/sys/vm/compact_memory sysctl -w vm.nr_hugepages=9216 # 如果 compact 后仍差很多,顺手释放 page cache 再试一次 echo 3 > /proc/sys/vm/drop_caches sysctl -w vm.nr_hugepages=9216 # 确认是否分配到位 grep -E 'HugePages_Total|HugePages_Free' /proc/meminfocompact_memory是让内核去做内存规整的命令,把分散的空闲页往一起挪;drop_caches值为 3 表示释放页缓存和目录项缓存,腾出更多可连续分配的内存。这两条命令的风险点在于:drop_caches会让文件缓存失效,执行后磁盘 IO 可能短暂升高;compact_memory在某些内核版本上短时间内会占用 CPU 做页迁移。所以我加了一句“业务低峰期执行”,不是套话。
更稳妥的办法是把vm.nr_hugepages写进第 6 章的 GRUB 启动参数里,让内核在开机时、内存碎片几乎不存在的最早阶段就把大页池建好。这是彻底解决碎片问题的姿势。
5.3 坑3:数据库SGA参数与大页池不匹配
现象:Oracle 实例启动失败,告警日志报ORA-27102: out of memory,或者实例起来了但V$SGA显示部分内存仍在使用普通页。
原因:SGA_TARGET设得比大页池总量还大,或者 SGA 里包含了一些无法用大页映射的部分(比如某些内部的reduction内存)。Oracle 对共享内存段映射大页时要求整个段都落在大页里,只要段尾多出哪怕一页落在了普通内存,就会报错或回退。
解决:先把数据库启动到nomount状态,查看真实的共享内存段大小,再让大页池总量覆盖它并留出一定余量。我的习惯是池子大小比 SGA 预期值大 2% 到 5%,因为共享内存段实际映射时会有页对齐的额外开销。
# 在 SGA 从相关隐藏参数或文档确定前,先按实际共享内存挂载段核查 ipcs -m | grep oracleipcs -m能看到当前由 oracle 用户创建的共享内存段的字节数,把这个值除以 2MB 得到需要的页数,再对照HugePages_Total。如果差了 1 到 2 个页,就把nr_hugepages补上去。数据库这类场景的兜底原则:大页池宁多勿少,但不要多到吃光普通内存导致系统整体无内存可用——我一般控制在总物理内存的 50% 以内。
5.4 坑4:共享内存权限与内存锁定不足
现象:数据库进程在非 root 用户下启动,shmget报Cannot allocate memory或Operation not permitted,进程起不来;即使起来了,做过大页映射的线程在vmstat里看到 si/so 持续不为 0,说明内存还是落到了 swap。
原因:大页映射的共享内存默认只有 root 有权限操作,或ulimit -l的锁定内存上限不够。大页内存本来是不可换出的,如果进程因为权限不够没用上大页,而是带着普通内存运转,被换出的风险就回来了。
解决:在前面的配置里给vm.hugetlb_shm_group指定业务组,并确认ulimit -l上限不低于大页池大小。用 systemd 管理服务时,在 service 文件里显式加LimitMEMLOCK=infinity,普通 shell 启动则编辑/etc/security/limits.conf里的memlock行。这几个位置漏一处,进程表面能起来但实际没拿到大页,排查起来比启动失败更隐蔽。
6. 让HugePages开机自动生效的进阶技巧:把配置写进grub与systemd的两种可靠姿势
sysctl.d的配置在系统启动阶段也能起作用,因为systemd-sysctl服务会在开机时加载这些文件。但这里有个时序问题:加载 sysctl 配置时内核已经运行起来了,内存碎片已经存在,nr_hugepages很可能又被丢进deferred状态慢慢补。要想从根上消除碎片干扰,就得把大页池的创建提前到内核初始化的早期阶段,也就是通过 GRUB 内核启动参数来指定。
# 编辑 GRUB 配置,在大页相关参数中追加 hugepages=N # N 为期望的页数,比如 9216 sudo vi /etc/default/grub # 找到 GRUB_CMDLINE_LINUX 一行,在引号内追加: # transparent_hugepage=never hugepages=9216 GRUB_CMDLINE_LINUX="... transparent_hugepage=never hugepages=9216" # 重新生成引导配置(UEFI 与 BIOS 发行版命令有差异) sudo grub2-mkconfig -o /boot/grub2/grub.cfg sudo reboot内核启动参数里的transparent_hugepage=never直接把 THP 关在门外面,确保后续不会出现 THP 与显式 HugePages 抢内存的问题;hugepages=9216让内核在系统初始化阶段、物理内存几乎全部空闲时去切大页池,这时候没有碎片问题,分配成功率远高于系统跑起来之后再写 sysctl。重启之后再用/proc/meminfo确认HugePages_Total,如果显示的页数等于你启动参数写的值,这条路就走通了。
如果你的业务进程不是数据库这类用共享内存的系统,而是自己用hugetlbfs做内存映射的 Java 或自研 C++ 服务,还有一个更灵活的做法:用 systemd 服务在业务进程启动前预留大页并挂载。这个方案的好处是不需要重启节点,适合不能随便重启的存量机器。
# /etc/systemd/system/hugepages-setup.service [Unit] Description=Reserve HugePages before application starts Before=myapp.service [Service] Type=oneshot ExecStart=/usr/sbin/sysctl -w vm.nr_hugepages=2048 ExecStart=/usr/bin/mount -t hugetlbfs hugetlbfs /dev/hugepages -o mode=1770,gid=54321 RemainAfterExit=yes [Install] WantedBy=multi-user.target这个 service 文件我一般会在新装业务节点时配合业务进程的 systemd unit 一起写,用Before=myapp.service保证业务起之前大页已经就绪。需要注意的是,同一台机器如果同时有数据库和 Java 服务要分大页池,nr_hugepages只能写一个总量,具体哪个进程拿到多少页要靠进程的启动顺序和申请量来竞争,这时最好把池子建大一点,并通过各自服务配置里的-XX:LargePageSizeInBytes=2m这类参数去限制申请粒度。
到现在为止,我每新装一台数据库或大内存中间件服务器,第一件事永远是先确认 HugePages 再装业务,顺序错了我宁可推倒重来,也不想在业务跑起来之后再去碰内核参数。这套流程我走了很多年,踩过的坑都在前面这些章节里了,希望帮到你。
本文还有配套的精品资源,点击获取