Linux运维11 min read次阅读

透明大页不是免费午餐:THP 怎么把数据库延迟搞出毛刺,以及几种靠谱的关法

有段时间我们的 Redis 实例每隔几秒就冒一次延迟尖刺,redis-cli --latency 的曲线上能清楚看到规律的锯齿:大部分请求在 0.1 毫秒内返回,但每隔三五秒就有一批请求卡在 50 到 200 毫秒。业务方开始投诉接口偶发超时,我第一反应是查慢查询、查大 key、查网络,结果慢日志干干净净,bigkeys 扫描也没异常,主机 CPU 还闲得很。

那次排查花了我小半天,最后真正 culprit 不是应用,也不是网络,是内核在后台帮我们「好心」做的一件事——透明大页(Transparent Huge Pages,THP)。这件事之后我觉得值得把 THP 单独写一遍,因为它太容易被忽略,又太容易伪装成别的毛病。

THP 到底在干什么

常规 Linux 内存管理用的是 4KB 一页。进程地址空间越大,页表项就越多,CPU 的 TLB(页表缓存)被频繁撑满,每次缺页都要走多级页表查一遍,开销不小。THP 的想法很直接:把连续的 4KB 页自动合并成 2MB 的大页,页表项变少,TLB 命中率上去,理论上内存访问更快。

听起来全是好处,对不对?问题是「合并」和「维持合并」不是免费的。内核有个叫 khugepaged 的后台线程,不停地扫描内存、把能凑成 2MB 的零散页往一起搬;当应用申请内存、而系统暂时没有现成的大页时,如果 defrag 策略开得激进,分配线程会被直接阻塞住,等内核把一块 2MB 大页整理出来才放行。这个「等」就是延迟尖刺的来源——它可能只卡几毫秒,也可能卡上百毫秒,而且完全不体现在你的应用日志里。

还有一种更隐蔽的情况:像 Redis 做 RDB 快照或 AOF 重写时会 fork 出子进程,靠写时复制(Copy-On-Write)共享内存。THP 下,哪怕只改一个 4KB 里的几个字节,内核也得复制整个 2MB 大页,内存复制量和碎片一下子放大几百倍,fork 耗时会从毫秒级跳到秒级。很多「Redis 备份时卡顿」的锅,其实也是 THP 背的。

先确认是不是它在捣乱

别上来就关。先看看机器上 THP 现在是什么状态:

# 这三个值能告诉你当前开关与碎片整理策略
cat /sys/kernel/mm/transparent_hugepage/enabled
cat /sys/kernel/mm/transparent_hugepage/defrag
grep AnonHugePages /proc/meminfo

enabled 常见输出是 [always] madvise never 或者 always [madvise] never,方括号标的是当前生效项。defrag 同理。AnonHugePages 不为 0 说明系统里确实有匿名大页在被使用——数据库这类吃内存的服务最容易贡献这一项。

对 Redis 这种服务,最直接的对证方式是开它的延迟监控,看尖刺是不是和 khugepaged 的扫描节奏对得上:

redis-cli CONFIG SET latency-monitor-threshold 100
# 让它跑几分钟,再来看历史
redis-cli LATENCY LATEST
redis-cli LATENCY HISTORY fork
redis-cli LATENCY DOCTOR

LATENCY HISTORY fork 如果显示 fork 耗时偶尔飙到几百毫秒,而那段时间和你观察到的请求毛刺重合,基本就能锁定。Oracle 这边更省事,MOS 文档(1557478.1)明确建议关掉 THP,alert 日志里有时还能看到它关于大页的提示;更实在的办法是直接看 /proc/meminfoAnonHugePages 趋势,配合 perf stat -e kmem:* 或者用 cat /proc/vmstat | grep -i thpthp_fault_allocthp_collapse_alloc 的计数是否在涨——计数猛涨说明内核正在频繁地现场凑大页,这正是延迟抖动的温床。

khugepaged 本身也能调,先看清它现在多勤快:

cat /sys/kernel/mm/transparent_hugepage/khugepaged/scan_sleep_millisecs
cat /sys/kernel/mm/transparent_hugepage/khugepaged/alloc_sleep_millisecs
# 默认 scan_sleep 是 10000ms,alloc_sleep 是 60000ms,但扫描力度仍足以制造尖刺

哪些服务最该关,哪些可以留

不是所有场景都要关。这东西的本质是「用可预测的延迟换吞吐」的取舍。

  • Redis、Oracle、PostgreSQL 这类延迟敏感、且内存长期驻留的状态服务:基本都建议关,或者至少把 defrag 设成 never。Oracle 官方态度最硬,直接让关;Redis 官方文档也明确不推荐 THP;PostgreSQL 社区一般建议 madvise 而非 always。
  • 跑 JVM 的服务:JVM 堆如果用 THP always,GC 和对象分配都可能被大页整理卡住。Oracle/OpenJDK 一般建议关掉 THP,转而用 -XX:+UseLargePages 显式申请标准大页(HugePages),那是另一套更可控的机制。
  • 批量计算、内存分析、吞吐优先的离线任务:THP 反而可能帮你省 TLB 开销,可以保留 madvise,让应用自己用 madvise(MADV_HUGEPRTF) 决定哪段内存要大页。

一句话:面向用户的、讲究尾延迟的服务,宁可关了求稳;离线吞吐型,可以留着看收益。别听谁说一句「THP 有害」就全网所有机器一刀切,那样反而会坑了那些真能从大页里得益的负载。

关掉它:几种姿势和各自的坑

1. 临时关,立刻验证

最轻量的办法,改 sysfs:

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

enabled=never 是关分配,defrag=never 是关同步整理。两个都写 never 才彻底。重启即失效,适合先验证「关了之后毛刺是不是真的没了」,确认有效再上持久化方案。

2. 用 systemd 在开机时写回

比改 grub 简单,写个 oneshot 服务即可,重启后依然生效:

cat > /etc/systemd/system/disable-thp.service <<'EOF'
[Unit]
Description=Disable Transparent Huge Pages
After=local-fs.target

[Service]
Type=oneshot
ExecStart=/bin/sh -c 'echo never > /sys/kernel/mm/transparent_hugepage/enabled; echo never > /sys/kernel/mm/transparent_hugepage/defrag'
RemainAfterExit=yes

[Install]
WantedBy=multi-user.target
EOF

systemctl daemon-reload
systemctl enable --now disable-thp.service

这套的好处是只动 systemd,不碰引导配置,回滚也快。坑在于:如果某些服务(比如 Oracle 用 HugePages 显式大页)需要在更早的启动阶段就看到正确的 THP 状态,oneshot 服务执行时机可能偏晚,这时候就不如 grub 方案彻底。

3. grub 内核参数,最持久也最影响早期启动

RHEL/CentOS 7 系改 /etc/default/grub

# 在 GRUB_CMDLINE_LINUX 那一行末尾追加
transparent_hugepage=never
# 然后重新生成配置
grub2-mkconfig -o /boot/grub2/grub.cfg        # BIOS 引导
# grub2-mkconfig -o /boot/efi/EFI/redhat/grub.cfg  # UEFI 引导

CentOS 8 / Alibaba Cloud Linux 4 用的是 BLS(Boot Loader Specification),光改 /etc/default/grub 不一定生效,得用 grubby 直接改内核项:

grubby --args="transparent_hugepage=never" --update-kernel=ALL
grubby --info=ALL | grep -i hugepage

这步我踩过:在一台云上机器只改了 default/grub 没用 grubby,重启后 THP 还是 always,白排查一轮。cloud 镜像和物理机的引导方式不一样,动手前先 ls /sys/firmware/efi 存不存在、确认是 BIOS 还是 UEFI,再选对的命令。

4. 容器和 Kubernetes 环境

THP 是节点级的内核特性,容器共享宿主内核,所以在 Pod 里 echo never 往往改不动 sysfs(很多发行版把 /sys 挂成只读或各自挂载)。正确做法是在节点上关,要么用上面的 systemd 服务,要么 DaemonSet 跑一个 privileged 的 init 容器去写宿主的 sysfs——但更稳妥的还是直接改节点。Kubernetes 生态里常见做法是把 transparent_hugepage=never 写进节点的内核启动参数,配 kubelet 的 node-allocatable 无关,但和拓扑、NUMA 有关的特性要一起评估,别改完发现别的调优被带偏。

关完怎么验收

别以为写了 never 就完事。改完之后,隔几分钟再 cat 一次 sysfs 确认两个值都是 never;重启服务(尤其是数据库,要让它重新分配内存);然后盯一段时间延迟,Redis 看 LATENCY HISTORY,Oracle 看 AWR 里的 DB CPU 和等待事件有没有变化,PostgreSQL 看 pg_stat_database 的锁等待。

我那次是临时关掉后观察了二十分钟,锯齿尖刺消失,redis-cli --latency 曲线变平,才正式上了 systemd 方案。AnonHugePages 那一栏也跟着慢慢回落到 0,算是闭环。

还有个细节:如果你为了 Oracle 显式用了 HugePages(vm.nr_hugepages),那是标准大页,和 THP 是两码事,关 THP 不影响它,反而能避免两者打架。很多文档把「大页」混着讲,实际运维时要把 transparent huge pages 和 explicit hugepages 分清楚。

别把它当成银弹

写了这么多,核心是:THP 这东西值得每个跑数据库的 Linux 运维心里有数,但它解决不了你代码里的慢查询、解决不了网络丢包、也解决不了磁盘 IO 打满。它只是「延迟毛刺」这个大家庭里一个经常被误判的成员。遇到规律的、周期性的、应用层查不出原因的延迟尖刺,把它加进排查清单;确认它在涨、关了就好,就关;不确定,就先留 madvise 观察。看 AnonHugePagesthp_* 的计数趋势,比背一句结论靠谱得多。

分享:

相关文章

单库跑着不算保险:用 Oracle 19c Data Guard 物理备库扛住主机房塌了
数据库运维12 min read

单库跑着不算保险:用 Oracle 19c Data Guard 物理备库扛住主机房塌了

RMAN 备份恢复写过,日常运维也写过,但这篇讲另一件真出事才显价值的事——给 Oracle 19c 建一套物理备库。主机板子烧了、机房网络断了,有 DG 你一条命令切过去继续跑;没有,就只能从备份恢复, downtime 和数据丢失都够喝一壶。从前置条件、RMAN 复制建库、broker 配置到 switchover/failover 和那些必踩的坑,这篇都给你敲出来。

Oracle 19c 装完能连只是开始:表空间、内存、AWR 和那些半夜弹出来的 ORA 报错
数据库运维13 min read

Oracle 19c 装完能连只是开始:表空间、内存、AWR 和那些半夜弹出来的 ORA 报错

RMAN 备份那套另写过,这篇只讲上线之后每天真会撞上的事:表空间悄悄涨满、业务喊慢你却不知道慢在哪、ORA-01555 和 ORA-00257 半夜报警、还有一句 ALTER SYSTEM KILL SESSION 救活被锁死的接口。不堆概念,给能直接敲的命令、能看懂的判断,以及我踩过的几个弯。

评论区