磁盘 await < 2ms,没有问题

搬运并适配自国外社区真实案例。原文出处见文末。

TL;DR

症状 :生产数据库突然出现周期性延迟尖刺(P99 从 2ms 跳到 2000ms),CPU 无异常,磁盘 IO 正常,内存余量充足。根因 :内核 Transparent Huge Pages(THP)默认 defrag=always 模式,在分配大页时触发同步内存压缩(compact_memory),STW(Stop-The-World)长达数百毫秒。解决echo never > /sys/kernel/mm/transparent_hugepage/defrag,改异步压缩(khugepaged)或直接关闭。权威背书:Red Hat、MongoDB、PostgreSQL 官方文档均建议数据库场景关闭 THP。

现象

复制代码
$ top
  PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND
 9876 mysql     20   0   16.2g   8.1g   8232 S   5.0   5.1  24:30.12 mysqld

$ free -h
              total        used        free      shared  buff/cache   available
Mem:          157Gi        96Gi        8.0Gi       450Mi        53Gi        58Gi
Swap:         2.0Gi        0.0Gi       2.0Gi

$ vmstat 1
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
 3  0      0 8264832 678912 56053760  0    0  1234  5678  28901 4503  8  2 88  2  0
 8  1      0 8251392 678912 56054784  0    0   512   0  → 突然大量线程阻塞在 D 状态

$ cat /proc/vmstat | grep -E "compact|thp|kswapd"
compact_migrate_scanned 283471923
compact_free_scanned 891234567
compact_isolated 45678234
thp_fault_alloc 123456
thp_fault_fallback 9876543  ← 分配大页失败次数极高
thp_collapse_alloc 234567

环境:CentOS 7 / Linux 4.18.0 | MySQL 8.0 InnoDB | 128GB 内存 | 运行 6 个月正常,近期延迟尖刺从每月 1 次增加到每天 3 次。

排查过程

第一回合:常规诊断 → 无功而返

复制代码
$ iostat -x 1
# 磁盘 await < 2ms,没有问题

$ perf top -p $(pgrep mysqld)
# 热点函数正常:ha_innobase::index_read、btr_search_guess_on_hash
# 没有异常锁竞争

为什么常规指标失灵 :THP 压缩在 khugepaged 内核线程中执行,不消耗进程 CPU 时间、不产生磁盘 IO、不改变 free -h 数值。它只做一件事:把分散的 4KB 物理页搬到一个连续区域。这个过程不改变进程地址空间,但对被移动页面的任何访问都会触发硬件 page fault ------ 等待时间远超正常内存访问。

第二回合:抓到关键信号

复制代码
$ cat /proc/vmstat | grep compact_
compact_migrate_scanned 283471923  # 已扫描的迁移页面
compact_free_scanned    891234567  # 已扫描的空闲页面
compact_isolated        45678234   # 已隔离的页面
compact_stall           1234       ← 关键!触发同步压缩停滞
compact_fail            567        # 压缩失败次数

$ cat /proc/vmstat | grep thp
thp_fault_alloc         123456     # fault 时成功分配大页
thp_fault_fallback      9876543    # fallback 到 4KB ← 极高
thp_collapse_alloc      234567     # khugepaged 合并成功
thp_split_page          45678      # 大页被拆分

compact_stall=1234 意味着应用线程在 alloc_pages()被强制等待内存压缩完成------这是同步阻塞,时长取决于物理内存碎片化程度。

第三回合:定位根因参数

复制代码
$ cat /sys/kernel/mm/transparent_hugepage/enabled
[always] madvise never

$ cat /sys/kernel/mm/transparent_hugepage/defrag
[always] defer defer+madvise madvise never

两个参数都设成了 always : - enabled=always → 所有进程默认尝试用大页 - defrag=always → 如果大页分配失败,同步等待内存压缩------应用线程卡住,等内核把碎片整理好

数据库运行 6 个月后,物理内存碎片化严重,大页分配失败概率飙升 → compact_stall 从 0 增长到 1234 → 每次 STW 100-2000ms。

第四回合:确认效果

复制代码
# 方案 1:defrag 改异步(推荐)
$ echo never > /sys/kernel/mm/transparent_hugepage/defrag
$ echo never > /sys/kernel/mm/transparent_hugepage/enabled

# 验证
$ cat /proc/vmstat | grep compact_stall
compact_stall 1234  # 不再增长

# 应用延迟恢复
$ # P99 恢复至 < 10ms

根因:内核代码级解析

THP 的核心在两个文件中:

  • mm/huge_memory.c --- fault-time 分配:进程缺页中断时,内核尝试分配 2MB 大页
  • mm/khugepaged.c --- 后台合并:khugepaged 内核线程扫描,把连续 4KB 页合并成大页

defrag=always 时,fault-time 分配失败后的逻辑(简化):

复制代码
// mm/page_alloc.c --- __alloc_pages_slowpath()
static struct page *
__alloc_pages_slowpath(gfp_t gfp_mask, ...)
{
    // ... reclaim 尝试后仍失败

    // defrag=always → 同步压缩
    if (gfp_mask & __GFP_DIRECT_RECLAIM) {
        // try_to_compact_pages() 会迁移页面,耗时取决于碎片程度
        page = __alloc_pages_direct_compact(gfp_mask, order, ...);
        // 这个过程可能耗时数百毫秒!
    }
    // 失败 → 返回 4KB 页面(fallback)
    return fallback_to_4k_pages();
}

为什么数据库场景特别严重 : - 数据库的内存访问模式偏向随机读写,页面分散,THP 的"连续虚拟地址 → 连续物理地址"优势不明显 - InnoDB 的 innodb_flush_method=O_DIRECT 绕过 page cache,THP 对数据库页面的加速效果几乎为零 - 频繁 malloc/free 导致物理内存碎片化,大页分配失败率随时间线性增长

为什么 Redis 也受影响: - Redis fork 子进程做 RDB/AOF rewrite 时,Copy-on-Write(COW)会因 THP 放大:一个 2MB 大页哪怕只改 1 byte,也要完整复制 2MB

解决方案(按推荐度)

方案 1:彻底关闭 THP(数据库场景首选)

复制代码
# 立即生效
echo never > /sys/kernel/mm/transparent_hugepage/enabled
echo never > /sys/kernel/mm/transparent_hugepage/defrag

# 持久化
cat >> /etc/rc.local << 'EOF'
echo never > /sys/kernel/mm/transparent_hugepage/enabled
echo never > /sys/kernel/mm/transparent_hugepage/defrag
EOF

# 或 grub 内核参数
# /etc/default/grub:
# GRUB_CMDLINE_LINUX="transparent_hugepage=never"
# $ grub2-mkconfig -o /boot/grub2/grub.cfg

方案 2:用 defer 模式(折中)

复制代码
echo defer > /sys/kernel/mm/transparent_hugepage/defrag

defer 模式:fault 时不触发同步压缩,交给 khugepaged 后台异步处理。应用线程不卡,但大页利用率可能略低。

方案 3:per-process 精细化控制(JH/Java 场景)

复制代码
# 只给 Java heap 开 THP(madvise 模式)
echo madvise > /sys/kernel/mm/transparent_hugepage/enabled
# Java 启动参数:
# -XX:+UseTransparentHugePages 或 -XX:+UseLargePages

如何自检你是否中招

复制代码
# 一行命令:检查是否有同步压缩停滞
awk '/^compact_stall/ { stalls=$2 }
     /^thp_fault_alloc/ { alloc=$2 }
     /^thp_fault_fallback/ { fallback=$2 }
     END {
         printf "compact_stall=%d, alloc=%d, fallback=%d\n", stalls, alloc, fallback;
         if (stalls > 0) print "⚠️  检测到同步压缩停滞!建议关闭 THP 或改 defrag=defer";
         if (fallback > alloc * 10) print "⚠️  大页分配成功率 < 10%,物理内存碎片化严重"
     }' /proc/vmstat

持续监控脚本/etc/cron.d/thp-monitor):

复制代码
*/5 * * * * root awk '/compact_stall/{s=$2}/compact_fail/{f=$2}END{if(s>100||f>50)system("logger -t thp-monitor WARNING compact_stall="s" compact_fail="f)}' /proc/vmstat

启示

  1. 默认配置不是为你优化的 ------Linux 内核 THP 默认 always 是为通用负载(Web 服务、文件服务器)设计的,不是为数据库。数据库场景 90% 的情况该关掉。
  2. 内存余量充足 ≠ 没有内存问题 ------THP 的延迟尖刺在 free -hvmstatiostat 上都不可见。唯一能抓到的信号是 compact_stall 字段。
  3. 不要只用 enabled,一定要管 defrag ------很多运维只关了 enableddefrag 还是 always,以为问题解决了。两个参数独立生效
  4. 碎片化随时间加重 ------今天没延迟不代表明天没有。物理内存碎片化是不可逆的累积过程,compact_stall 只增不减(除非 reboot)。
  5. "透明"不透明------THP 的名字承诺"对应用透明",但延迟尖刺就是透明度破产的证明。真正透明的性能提升不该有代价。

原始出处 : - kernel.org: Transparent Hugepage Support --- 官方文档,defrag 五种模式详解 - kernel-internals.org: THP --- 内核数据结构、khugepaged 扫描机制、/proc/vmstat 字段解释 - Red Hat Performance Tuning Guide --- RHEL 官方建议:数据库场景禁用 THP - YugabyteDB TA-26440 --- THP 导致内存使用异常增长和 RSS 膨胀


本文首发于 CSDN 专栏《运维漏洞指南》。每篇都是排查回合制实录,从现象到内核代码,给可执行命令而非堆概念。

相关推荐
阿拉斯攀登9 小时前
嵌入式 Linux 驱动概述:分类、内核模块、开发环境
linux·嵌入式硬件·linux内核·嵌入式·linux驱动
Qt程序员5 天前
内核定时底层核心:时间轮算法
linux·linux内核·定时器·时间轮
坤昱1 个月前
cfs调度类深入解刨——pelt细节篇
linux·linux内核·cfs调度·eevdf·cfs调度类深入解刨·pelt·pelt细节篇
Qt程序员1 个月前
Linux RCU 原理与应用
linux·c++·内核·linux内核·rcu
程序员老舅1 个月前
从内核视角,看Linux文件读写过程
linux·服务器·c++·内核·linux内核·vfs·linux内存
行者-全栈开发1 个月前
【运维安全】CVE-2026-46333:Linux内核ptrace权限提升漏洞深度解析与修复指南
运维·linux内核·权限提升·ptrace·cve-2026-46333·ssh-keysign-pwn·安全修复
bsauce1 个月前
【kernel exploit】CVE-2026-23271 perf_event竞态UAF漏洞-ROP提权
linux·linux内核·内核漏洞·内核漏洞利用
坤昱1 个月前
cfs调度类深入解刨——最新内核细节分析3
linux·linux内核·deadline·cfs调度·eevdf·kernel 7.1·最新内核调度
Qt程序员2 个月前
突破I/O天花板:Linux零拷贝技术
网络·linux内核·io·linq·零拷贝·mmap