操作系统内功 · 第 52 篇|Transparent Huge Pages:自动巨页的收益、抖动与数据库禁用惯例

动机:TLB 的容量焦虑与静态巨页之重

第 19 篇讲过 NUMA 与 HugeTLB:巨页把页的大小从 4KB 提到 2MB,同样的内存覆盖范围,页表项数量缩减五百多倍,TLB 的有效覆盖随之倍增。对几十 GB 堆的大内存工作集,这笔账非常可观:TLB 是一块极小的硬件缓存,4KB 页下它常常罩不住整个工作集,频繁的 TLB miss 触发多级页表遍历,一次 walk 要多轮内存访问,几十个时钟周期起步;miss 密集时,两位数百分比的 CPU 时间被耗在地址翻译上,这是性能剖析里最难被直觉发现的开销之一。

HugeTLB 是解决方案的第一代:显式预留巨页池,应用用专门接口申请,收益确定、行为可控。但它的代价也明确:池子要预先圈占物理内存,应用要改代码适配,闲置的池子不能挪作他用。这堵墙挡住了绝大多数普通进程——堆再大,也没人为它改代码建池子。THP(Transparent Huge Pages)就是为了推倒这堵墙而生:让内核在应用完全无感知的前提下,自动把常规的匿名内存用 2MB 巨页管理,TLB 红利人人有份。

要划清一条边界:THP 只管匿名内存——堆、匿名映射、写时复制页这些没有文件后备的内存;文件映射页不在它的管辖范围(tmpfs 这类内存文件有自己单独的巨页开关,属于进阶话题)。另外 THP 的巨页对应用仍是连续虚拟地址上的普通访问,没有新接口,也没有申请仪式——这正是「透明」二字的含义,也是它与 HugeTLB 的分水岭:HugeTLB 是你要改代码、它给你确定收益;THP 是你什么都不做、内核替你赌一把。

收益的量级给一组参考感受。数值计算与大数组遍历类负载,巨页开合之间的实测差异常见两位数百分比,高性能计算圈子对巨页的坚持由来已久;JVM 大堆服务在 TLB 压力大的场景也有可观收益,这正是 madvise 模式后来受大堆应用关注的原因。而在线事务类负载的收益通常温和得多——它们的访问模式分散、工作集局部性强,4KB 页的 TLB 覆盖本来就够用。收益因负载而异、代价却普遍存在,这个不对称正是 THP 争议的算术基础。

THP 的实现:fault 合并与 khugepaged

THP 的实现有两条路径。第一条是分配路径:进程缺页时,内核尝试一次性分配 2MB 连续物理内存,成功则直接以巨页粒度建立映射,一步到位。第二条是后台合并路径:内核线程 khugepaged 周期扫描各进程的常规页映射区,发现某个 2MB 范围内的 4KB 页占比足够高、值得合并时,分配新的巨页,把已有内容迁移合并进去,替换页表——这个操作叫 collapse(坍缩)。两条路径合起来,覆盖了「新内存直接用巨页」与「旧内存逐步升级为巨页」两种时机。

全局行为由 /sys/kernel/mm/transparent_hugepage/enabled 控制,三个档位。always:所有匿名内存都积极尝试巨页;never:全关;madvise:默认不启用,应用对特定区间调用 madvise 打上巨页意愿标记后才生效。defrag 参数控制巨页分配失败时的补救力度:物理内存碎片化后凑不出 2MB 连续块,内核可以选择同步做内存规整(把可迁移的页搬家腾出连续空间,代价是当前缺页请求原地等待)、异步规整(先失败返回,规整交给后台)、或者直接放弃。档位命名如 always、defer、defer+madvise、madvise、never,力度从大到小排列。khugepaged 的扫描节奏另有每轮页数与休眠毫秒两个旋钮,控制后台合并的激进程度。

这套机制的性能模型是理解一切争议的基础。巨页的收益集中在大工作集的顺序访问与随机访问均匀分布的场景——TLB 命中率上升、缺页次数下降(一次缺页拿到 2MB)、页表本身占用的内存也缩减五百倍。代价集中在分配时机:2MB 连续块在运行久了的机器上并不好凑,同步规整会让一次普通的缺页停顿变成毫秒甚至几十毫秒级——应用视角就是一次莫名其妙的延迟尖刺。收益是持续性的,代价是脉冲式的,而脉冲式代价恰恰是延迟敏感系统最害怕的形态。

khugepaged 的工作节奏也值得感知。它按毫秒级休眠周期醒来,每轮扫描固定数量的页区,把「值得坍缩」的候选收集起来逐个合并,整体强度受参数节流。它移动页用的是内存迁移机制——把源页内容复制到新位置、改映射、释放旧页——这套设施与内存规整共用基础,所以后台合并的活动高峰常与规整压力高峰同步出现。观测上若看到页迁移计数持续活跃而业务无明显变化,多半是它在勤奋工作;调小扫描参数、或切换 madvise 模式,都是给它降温的手段。

收益与代价:TLB、停顿与膨胀

第一项代价:分配停顿。always 模式下,任何匿名缺页都可能触发同步内存规整——内核为了凑一个 2MB 连续块,搬移大量在用页,期间持有锁、占用 CPU,等待的进程完全无感地冻结了几十毫秒。这类停顿没有应用层痕迹:没有异常、没有慢查询标记、栈上一切正常,只有从内核视角(内存规整阻塞的计数指标)才能看见。这是「周期性莫名其妙卡一下」类故障的头号嫌疑犯,无数 P99 排查最终折在这里。

第二项代价:内存膨胀。巨页是分配与驻留的原子单位:一个 2MB 巨页哪怕只用了一个 4KB 尾巴,物理占用也是完整的 2MB,且不能部分回收或换出——回收巨页要先拆解成常规页。堆增长稀疏的应用(预留大、使用散),物理内存占用被 THP 显著放大,OOM 阈值提前到来。Redis 这类内存数据库对此格外敏感,原因还有一层:它的持久化快照依赖 fork 加写时复制,4KB 页时代子进程只复制被写的零星页,2MB 巨页时代任何一页被写就要复制整 2MB——fork 之后的写放大以五百倍粒度发生,快照期间内存峰值翻倍的风险从理论变成现实。

第三项代价:后台抖动。khugepaged 的扫描与迁移、规整的页搬家,都在和业务负载争抢 CPU、锁与内存带宽。always 模式下这些后台活动持续存在,负载毛刺随之而来。三项代价的共同点是:它们全部由「内核自动做主」引发,应用既不知情也无法干预——这正是社区最终走向「默认关或折中」的根本原因。收益真实存在(HPC、数值计算、大数组扫描场景收益显著),但代价的分布方式让在线服务行业集体投了反对票。

THP 与 NUMA 的互动还有一段恩怨。早期实现里巨页分配偏好特定节点,跨节点访问放大了远端内存惩罚,多路服务器上的 THP 曾是性能事故高发区,后来内核补了节点感知的分配策略才逐渐平息。这段历史的教训有普适性:任何「自动优化」在多节点拓扑上的行为都要单独验证,单机结论不能平移到多路服务器。今天的内核在这类互动上成熟了许多,但「自动机制加复杂拓扑等于新变量」这条公式依然成立,验证义务不因版本更新而豁免。

数据库社区的禁用惯例

数据库行业对 THP 的态度高度一致。MySQL 的官方与 Percona 文档明确建议禁用,理由就是分配停顿直接打击查询延迟;Oracle 数据库的安装文档同样写明要关掉 THP;MongoDB、Redis 也都在文档或启动检查里给出禁用建议,Redis 更是在启动时主动探测 THP 状态并打印醒目警告。这份名单的共同点是:全部属于延迟敏感、内存密集、且依赖 fork 类操作做快照的服务——THP 的三项代价恰好每一项都打在它们的要害上。

由此形成行业惯例:数据库主机的系统级 THP 设为 never,或至少 madvise。两条路径实现永久化:内核启动参数里带上 transparent_hugepage=never,或者由 systemd 服务、tuned 这类配置管理工具在开机后写 sysfs。检查方法也简单:cat 出 enabled 与 defrag 两个文件的当前值,方括号括住的就是生效档位;部署流水线里应把这两个检查做成自动断言,防止换机器时惯例丢失。

madvise 模式是折中方案:系统默认不动任何进程的内存,应用对确实受益的区间显式表达意愿。这个模式的理论上限很高——让懂自己访问模式的进程自己决定,而不是内核一刀切。现实是大多数应用从不打标,madvise 模式实际退化为「开着但没人用」。真正用好它的姿势是组合拳:系统级 madvise,数据库类进程保持沉默(等效关闭),大堆数值计算类进程显式启用,各取所需。HugeTLB 的分工则保持不变:需要确定收益与预留保障的场景(大型共享内存数据库、DPDK 数据面),仍然走静态巨页池的路线,两者不互相替代。

给架构评审加一条检查项:任何新数据库或内存密集中间件入场前,核对它的官方文档对 THP 的表态,并把结论固化进部署清单。这条流程的价值不在技术本身——查一次文档五分钟——而在组织层面:THP 配置从此有出处、有记录、有断言,不再依赖某位老工程师的记忆。文档没表态的项目,按延迟敏感处理默认禁用,宁可少赚一份 TLB 收益,不赌一次莫名其妙的延迟毛刺。

观测与实操

观测口径集中在两处。/proc/meminfo 里的匿名巨页占用字段,回答「现在有多少内存以巨页形态存在」;/proc/vmstat 里的一组 THP 计数——缺页时成功分配巨页的次数、后台坍缩成功的次数、分配失败的次数——回答「THP 的活动有多频繁、成功率几何」。内存规整的阻塞计数是延迟排查的专用指标:数值持续增长,配上应用侧无规律的长尾延迟,基本可以锁定 THP 或碎片化的嫌疑。诊断顺序建议:先看规整阻塞与分配失败计数,再看巨页占比与膨胀幅度,最后决策是关是留。

实操清单。数据库与延迟敏感服务:THP 与 defrag 都设 never 或至少 madvise,部署断言固化检查;通用后台计算与大数组负载:可以试 always,但必须配 P99 观测对比,收益与毛刺都量化后再定;内存利用率敏感的场景(大内存预算的容器宿主):警惕膨胀,优先 madvise;任何调整都要在压测环境复现负载 profile,只看吞吐不看延迟尾部的结论不作数。永久化配置写进系统镜像或初始化脚本,别靠手工 echo——重启即忘是这类调优最常见的翻车方式。

常见误区顺带列三条。误区一:THP 与 HugeTLB 是一回事——前者自动赌、后者手动买,机制与适用面完全不同。误区二:关了 THP 就没有巨页收益—— HugeTLB 与 madvise 定点启用是替代路径。误区三:把 THP 停顿当应用 bug 排查三天三夜——先查内核侧规整阻塞计数,能省下大半排查时间。

最后回答一个高频问题:为什么有时候「明明关了 THP,内存账本里还是有巨页」。常见原因有三:配置只在运行态生效而重启回滚(老话题);某个服务单元或容器运行时又把它打开了(继承链故事的重演);以及共享内存类巨页来自 tmpfs 或静态巨页池,与 THP 无关,属于另一套机制的正常存在。三线排查,通常两分钟内破案。这也再次呼应本系列的老规矩:内核功能要看生效链,不看出厂默认。

小结

THP 是把巨页收益自动化的尝试:缺页时直接分配 2MB 巨页,khugepaged 后台把零散常规页坍缩合并,TLB 命中率与缺页成本双双改善。但收益是持续的,代价是脉冲的:同步规整带来毫秒级停顿,巨页粒度带来内存膨胀与 fork 写放大,后台合并带来抖动——三项代价全是「内核替你做主」的直接后果。

数据库行业集体禁用不是保守,而是负载画像的精准匹配:延迟敏感、内存密集、快照依赖 fork,THP 的每一项代价都正中要害。madvise 模式提供了「懂行者自选」的折中,HugeTLB 则守住确定收益的阵地,三者各安其位。

这更像一堂机制设计的课:自动化不等于无代价,把选择权留给懂负载的人,往往比全局最优的幻觉更可靠。THP 的故事留给运维的肌肉记忆是两行检查、一组计数器,和「延迟尖刺先查内核」的诊断直觉。

← 返回资讯列表

读者留言

COMMENTS 暂无
仅本站原创文章开放留言 · 请勿留下手机号、邮箱等个人信息

还没有留言,来说第一句?