透明大页:一个"看起来是优化"的内核默认值
如果你在 Linux 上用 top 或者 /proc/meminfo 观察过,会注意到一个叫 AnonHugePages 的指标。它背后是内核的透明大页(Transparent Huge Pages,THP)机制:把默认 4KB 的内存页自动合并成 2MB 的大页,减少 TLB(地址转换缓存)miss,从而提升内存密集型程序的性能。
听起来很好,但对跑数据库和长期运行的 Java/PHP 服务来说,THP 往往是延迟抖动的元凶。Redis、MongoDB、MySQL、Elasticsearch 的官方文档都明确要求关闭 THP。这篇文章讲清楚:THP 为什么会产生问题、怎么正确关闭、以及 NUMA 节点与内存碎片在低配服务器上该不该管。
THP 为什么会造成延迟抖动
关键在于 THP 的分配时机。当内核尝试为一段内存分配 2MB 连续物理页时,如果内存已经碎片化、找不到连续区域,就会触发内存整理(compaction)——这个过程会暂停相关进程,甚至全局停顿。表现就是"服务平时很快,偶尔卡住几百毫秒到几秒",而且这种卡顿很难在 CPU、磁盘指标上找到对应。
更麻烦的是 khugepaged 内核线程会在后台持续扫描内存、尝试合并页。这个后台行为本身就是 CPU 开销,且在内存紧张时可能引发连锁的回收与整理。对已经精心调过 innodb_buffer_pool_size 的 MySQL 来说,THP 的介入会让缓存行为变得不可预测。
一句话总结:THP 优化的是吞吐,牺牲的是延迟稳定性。批处理任务喜欢它,在线服务讨厌它——而网站恰恰是延迟敏感的在线服务。
先确认 THP 当前状态
# 检查 THP 的三个开关
cat /sys/kernel/mm/transparent_hugepage/enabled
# 输出 [always] madvise never —— 方括号内是当前值
cat /sys/kernel/mm/transparent_hugepage/defrag
# [always] defer madvise never
cat /sys/kernel/mm/transparent_hugepage/khugepaged/defrag
# 1 表示启用
# 看实际用了多少大页
grep -E 'AnonHugePages|ShmemPmdMapped' /proc/meminfo
# 如果这个值很大且你不希望它大,说明 THP 在工作判断方式很直接:enabled 显示 [always] 就是最激进的模式;defrag 显示 [always] 意味着每次分配大页都允许同步整理内存——这是延迟抖动最严重的配置,必须改。
正确关闭 THP:三种方式与各自边界
方式一:临时关闭(验证用,重启失效)
echo never > /sys/kernel/mm/transparent_hugepage/enabled
echo never > /sys/kernel/mm/transparent_hugepage/defrag这是最快的验证手段:先临时关掉,观察半天,如果服务的延迟尖峰消失了,再考虑永久化。这一步几乎是零成本的——永远先验证,再持久化。
方式二:systemd 单元(Debian/Ubuntu 推荐)
用 rc.local 在 systemd 系统上不可靠,因为启动顺序不保证。正确做法是写一个 oneshot 单元:
cat > /etc/systemd/system/disable-thp.service <<'EOF'
[Unit]
Description=Disable Transparent Huge Pages
DefaultDependencies=no
After=sysinit.target local-fs.target
Before=mysql.service redis.service nginx.service
[Service]
Type=oneshot
ExecStart=/bin/sh -c 'echo never > /sys/kernel/mm/transparent_hugepage/enabled'
ExecStart=/bin/sh -c 'echo never > /sys/kernel/mm/transparent_hugepage/defrag'
RemainAfterExit=yes
[Install]
WantedBy=basic.target
EOF
systemctl daemon-reload
systemctl enable --now disable-thp
systemctl status disable-thp注意 Before= 那一行:必须让关闭 THP 排在数据库服务启动之前,否则 MySQL 启动时已经按 THP 策略分配好了内存,之后再关也没用。这是很多人"关了 THP 但没效果"的真实原因。用 systemctl list-dependencies 或者直接 systemctl show mysql.service | grep Before 核对顺序。
方式三:内核启动参数(最可靠,需重启)
# 编辑 GRUB 配置
sed -i 's/^GRUB_CMDLINE_LINUX="/GRUB_CMDLINE_LINUX="transparent_hugepage=never /' /etc/default/grub
# 重新生成引导配置(Debian/Ubuntu)
update-grub
# 重启后验证
cat /proc/cmdline
cat /sys/kernel/mm/transparent_hugepage/enabled这是最彻底的方式,因为它在内核初始化阶段就生效,绕过了所有启动顺序问题。代价是需要重启——对生产站来说要挑低峰期。另外部分云厂商的内核可能忽略该参数,重启后要实测确认。
一次"莫名卡顿"的定位过程
有位站长反馈:网站 TTFB 平时 200 毫秒左右,但每隔十几分钟会突然涨到两秒以上,持续两三秒后恢复。他排查了慢查询日志(干净)、PHP 执行时间(正常)、Nginx 日志(没有 5xx),甚至换了台机器问题依旧。
转折点出现在一次对照实验上:他在服务器上跑了一个循环脚本,每秒记录一次 /proc/vmstat 里的 compact_stall 和 thp_fault_alloc 计数。结果发现每次卡顿的时间点和 compact_stall 的跳变完全吻合。compact_stall 表示进程因为等待内存整理而停顿的次数——而触发整理的,正是 THP 在尝试分配 2MB 连续页。
根因清楚了:这台机器已经运行了一个多月,物理内存碎片化严重,THP 每次申请大页都找不到连续区域,于是触发同步整理,整理期间相关进程被暂停。因为整理发生在内核态,用户态的慢查询日志和 PHP 执行记录里都看不到任何痕迹,所以前面所有排查都扑了空。
修复动作只有两步:把 enabled 和 defrag 都设为 never,重启一次服务让已有大页释放。改造后 AnonHugePages 归零,compact_stall 不再增长,TTFB 的尖峰彻底消失,稳定在 180-220 毫秒。整个过程没有升级任何硬件。
这个案例的价值在于它展示了排查思路:当所有用户态指标都"正常"而问题依旧存在时,要把观察窗口下沉到内核态。/proc/vmstat、/proc/buddyinfo 这些文件平时没人看,但在解释"无法归因的抖动"时往往是唯一线索。
NUMA:单机小服务器基本不用管
NUMA(非统一内存访问)是双路 CPU 才需要认真对待的问题:每个 CPU 有自己的本地内存,跨节点访问内存延迟更高。但绝大多数个人站长的服务器是单路 CPU,此时只有一个 NUMA 节点,所有 NUMA 调优都是无意义的。
# 先确认有几个 NUMA 节点
numactl --hardware 2>/dev/null | head -5 || lscpu | grep NUMA
# NUMA node(s): 1 → 单节点,可以直接跳过 NUMA 调优
# 如果是多节点,查看内存分布是否均衡
numastat只有当 NUMA node(s) 大于 1 且内存分布明显不均时,才需要考虑用 numactl --interleave=all 或者给数据库绑定节点。不要盲目照搬"高性能调优清单"里的 NUMA 参数——在单节点机器上执行 numactl 绑定反而可能因为工具未安装或策略冲突带来新问题。
内存碎片:什么时候才需要关心
THP 的问题本质是内存碎片。用 /proc/buddyinfo 能看到各阶空闲页块的分布:
cat /proc/buddyinfo
# Node 0, zone Normal 512 256 128 64 32 16 8 4 2 1 0
# 最右边的列代表大块连续内存(高阶),数字很小说明碎片严重
# 触发一次手动整理(会消耗 CPU,仅诊断用)
echo 1 > /proc/sys/vm/compact_memory
# 查看整理统计
grep compact /proc/vmstat对个人站点来说,只要关掉 THP,内存碎片基本不会成为可感知的问题。真正会因为碎片受苦的是长期运行的 JVM(Java 应用),它需要大块连续内存做堆分配。如果你确实在跑 Java 服务且无法避免高负载,可以考虑 vm.min_free_kbytes 适当调高来保留更多空闲页,为内核留出整理空间。
常见问题
Q:关掉 THP 会不会让性能变差?
对延迟敏感的在线服务,关掉几乎总是净收益,代价是 TLB miss 增多带来的一点吞吐损失。对纯计算的大内存应用(比如大数据分析),THP 的吞吐优势才明显。网站属于前者,放心关。
Q:改成 madvise 是不是比 never 更好?
madvise 表示只有程序显式调用 madvise(MADV_HUGEPAGE) 才用大页。对不知道自己在干什么的程序,效果等同 never;对做了优化的程序(如部分版本的 JVM),它能保留大页收益。如果你不确定,选 never 更安全,因为可控性最高。
Q:关掉之后 AnonHugePages 还有值?
两种情况:一是关闭前已经分配的大页需要等进程释放;二是 ShmemPmdMapped(tmpfs 共享内存大页)走的是另一套逻辑,enabled=never 不影响它。重启相关服务后应该归零,若仍不降,检查是不是有进程在关闭前就启动了。
总结
THP 是一个典型的"内核默认值不适合你的场景"的例子。它优化平均吞吐,代价是尾延迟抖动,而在线服务的用户体验恰恰由尾延迟决定。处理路径清晰:先 echo never 临时验证,确认抖动消失后,用 systemd oneshot 单元持久化,并确保它在数据库服务之前执行;追求彻底就用 GRUB 参数。NUMA 调优只对多路 CPU 有意义,单路小机器跳过即可;内存碎片在关掉 THP 后基本无需单独处理。
这类调优的共同原则是:先量化症状,再改参数,然后验证。不要因为"某篇优化清单说该关"就照做——先看看你的 /proc/meminfo 里 AnonHugePages 到底占了多大比重,看看关与不关时 p99 延迟有没有差别。有数据支撑的改动才是优化,没有数据的改动只是碰运气。