分类: 系统运维与底层架构

  • 深入 NUMA 内存失衡排查:zone_reclaim_mode 引发的 THP 压缩阻塞与局域 OOM 击穿实战

    结论先行。针对 Elasticsearch/Kafka 等重度依赖 mmap 和 Page Cache 的应用,彻底关闭 THP(never)、设置 vm.zone_reclaim_mode=0 并强制 numactl --interleave=all 是规避 NUMA 局域 OOM 的铁律。跨 NUMA 访问的纳秒级延迟惩罚,远低于本地 Node 深度回收(Direct Reclaim)与大页压缩(Compaction)带来的秒级 I/O 夯死。

    现场还原:Load 飙升与诡异的毛刺

    某次排查中,业务反馈一个基于 Elasticsearch 7.17(底层系统为 Ubuntu 20.04,Kernel 5.4.0)的日志集群 P99 写入延迟出现极规律的剧烈抖动。平时延迟在 10ms 左右,但每隔几小时就会突发飙升至 2000ms+,伴随 Load Average 瞬间冲高到 80 以上。

    登录机器初步勘查,物理内存 256GB,JVM Heap 配置为 31GB(为了利用指针压缩),理论上剩余的 200GB+ 都会被 OS 用于 Page Cache 加速 mmap 读写。通过 free -g 查看,系统整体还有近 80GB 的 available 内存。

    然而,在查阅 /var/log/syslog 时,却发现了明确的 OOM Killer 介入日志:

    [51234.567890] java invoked oom-killer: gfp_mask=0x100cca(GFP_HIGHUSER_MOVABLE), order=0, oom_score_adj=0
    [51234.567895] CPU: 12 PID: 14532 Comm: java Tainted: G        W         5.4.0-122-generic #138-Ubuntu
    [51234.567901] Node 0 Normal free:45056kB min:45056kB low:56320kB high:67584kB
    [51234.567902] Node 0 Normal: 452*4kB (UME) 310*8kB (UME) ...
    [51234.567905] Node 1 Normal free: 83886080kB min:45056kB low:56320kB high:67584kB
    

    注意看日志里的致命细节:Node 0 的 free 内存已经触底(约 45MB,达到了 min watermark),而 Node 1 竟然还有 80GB 的空闲内存!

    性能观测:找出幕后黑手

    为了弄清为什么系统宁愿 OOM 也不用 Node 1 的内存,我拉起了常规的观测工具链。

    通过 numastat -m 查看 NUMA 节点的内存分布:

    $ numastat -m
                                 Node 0          Node 1           Total
                     --------------  --------------  --------------
    MemTotal                 128000          128000          256000
    MemFree                      43           81920           81963
    MemUsed                  127957           46080          174037
    Active                   110540           20480          131020
    Inactive                  12400           22500           34900
    

    Node 0 已经被彻底榨干。在延迟飙升期间,使用 perf top -p 抓取内核态调用栈,发现 CPU 极度密集地消耗在以下几个函数上:

    1. compaction_alloc

    2. isolate_freepages

    3. shrink_page_list

    同时,通过 /proc/vmstat 观察系统计数器,发现 compact_stallthp_fault_fallback 两个指标在毛刺期间呈现出几乎垂直的增长。

    为什么整体内存充足,却依然触发了局域 OOM 与 mmap 阻塞?

    这是一个典型的由 NUMA 架构默认分配策略、zone_reclaim_mode 回收机制以及 THP(透明大页)碎片整理共同酿成的惨剧。我们层层剖析。

    1. NUMA 的 Local Allocation 陷阱

    现代多路服务器默认开启 NUMA(Non-Uniform Memory Access)。Linux 内核默认的内存分配策略是 default,即优先在当前进程运行所在的 NUMA 节点上分配内存。 Elasticsearch 的主进程启动后,如果被调度器主要分配在 Node 0 的 CPU 上执行,它产生的大量 mmap 缺页中断(Page Faults)会疯狂吃掉 Node 0 的内存构建 Page Cache。最终,Node 0 被填满,而 Node 1 在旁边“看戏”。

    2. zone_reclaim_mode 引发的 Direct Reclaim 阻塞

    当 Node 0 的内存达到 low 水位线时,内核有两种选择:

    • A: 去 Node 1 借用空闲内存。

    • B: 强行在 Node 0 本地进行内存回收(驱逐 Page Cache 或 Swap)。

    内核如何决策?取决于 vm.zone_reclaim_mode 的值(以及节点间的距离 node_distance)。 在部分发行版或 BIOS 设置下,当 NUMA 节点距离较远时,系统倾向于在本地强行回收。此时如果业务正在高并发地写入,后台的 kswapd0 回收速度跟不上分配速度,内核就会挂起当前申请内存的用户态线程,进入Direct Reclaim(直接回收)路径。 shrink_page_list 就是在疯狂扫描和驱逐 Node 0 上的 Page Cache。这对于极度依赖 mmap 的 ES 和 Kafka 来说,相当于把热数据从内存里生生挖掉,下一次访问直接产生严重的磁盘 I/O 停顿。

    3. THP(Transparent Huge Pages)的致命一击

    如果只是缺内存,驱逐 Page Cache 最多带来 I/O 延迟。但 perf top 中的 compaction_alloc 揭示了更严重的问题:透明大页(THP)正在进行内存碎片压缩。 内核默认开启了 THP(madvisealways),试图为进程分配 2MB 的连续物理大页以减少 TLB Miss。当 Node 0 内存碎片化严重,没有连续的 2MB 空间时,内核的 khugepaged 或者触发 Direct Compaction 的线程会强行移动内存页面,试图“拼凑”出 2MB 的连续空间。 这个过程需要获取 Zone 级别的锁,会完全阻塞该 NUMA 节点上的其他内存分配请求。此时,业务看到的现象就是:机器负载瞬间飙到 80+,所有的写请求全部卡死(Hang),直到压缩超时或失败回退(thp_fault_fallback),随后由于 Node 0 实在挤不出哪怕 4KB 的内存,触发 OOM Killer 杀掉进程。

    核心调优实战与防御性配置

    不要迷信 OS 的默认配置,对于高吞吐的 DB/存储类应用,以下三步是必须落地的防御性基线:

    1. 强制 NUMA 内存交错分配(Interleave)

    通过 numactl 覆盖默认的本地分配策略,让应用在所有 NUMA 节点上均匀分配内存,彻底打散 Page Cache。 修改 ES 或 Kafka 的 systemd service 文件:

    [Service]
    # 将原来的 ExecStart 替换为带 numactl 的版本
    ExecStart=/usr/bin/numactl --interleave=all /usr/share/elasticsearch/bin/elasticsearch
    

    注:很多老鸟会担心 Interleave 带来的跨节点访问延迟(约增加 10~20 纳秒)。但在存储类系统中,因为局域内存耗尽引发的磁盘 I/O 阻塞(毫秒级甚至秒级),其代价是纳秒级跨节点延迟的 100,000 倍以上。

    2. 关闭 THP 与调整 zone_reclaim_mode

    透明大页对于 Redis/ES/Kafka 这类内存访问极度随机、频繁分配释放的应用,百害而无一利。必须在内核层彻底关闭,同时禁止本地激进回收。

    写入 /etc/sysctl.d/99-sysctl.conf

    # 优先去其他 Node 借用内存,绝不强行在本地发起深度回收
    vm.zone_reclaim_mode = 0
    # 降低 Swap 倾向,保护 Page Cache
    vm.swappiness = 1
    # 预留总内存的 1%-2% 给内核态,防止网络突发包导致网卡/内核分配内存失败触发直接回收
    # 256G 内存建议设置为 2G (2097152) 到 4G
    vm.min_free_kbytes = 2097152
    

    关闭 THP(不要只改 sysfs,建议写到 grub 引导参数里彻底干掉): 编辑 /etc/default/grub,在 GRUB_CMDLINE_LINUX 中追加: transparent_hugepage=never 执行 update-grub 并重启系统。若不重启,可通过以下命令即时生效:

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

    3. OOM Score 防御性保护

    对于关键存储进程,适当调低其 OOM Score,防止在极端情况下被内核误杀。可以在启动脚本中注入:

    echo -500 > /proc/$$/oom_score_adj
    

    常见问题 (FAQ)

    Q1:如何判断我现在的系统有没有受到 THP 的性能毒害? 查看 /proc/vmstat 中的关键计数器增量。执行 watch -n 1 "grep -e compact_stall -e thp_fault_fallback -e pgmigrate_success /proc/vmstat"。如果在你的业务高峰期,这几个指标在疯狂跳动,说明系统正在花费大量 CPU 周期进行内存整理,你的 P99 延迟绝对已经出问题了。

    Q2:vm.min_free_kbytes 设置得越大越好吗? 绝对不是。如果设置得太大(例如超过总内存的 5%),会导致系统提前触碰 low 甚至 high 水位线,触发后台 kswapd 极其频繁地唤醒,一直在做无用的 Page Cache 回收,反而降低了内存利用率并推高 CPU sys 使用率。一般 256G 内存给 2G~4G 足矣。

    Q3:除了 numactl --interleave=all,修改 BIOS 里的 Node Interleaving 有什么区别? BIOS 级别的 Node Interleaving 是从硬件层把 NUMA 给屏蔽掉(UMA 模式),OS 看到的只有一个大的 NUMA 节点。这种方式虽然简单粗暴,但所有进程都被迫交错访问。而使用 numactl 可以在 OS 保留 NUMA 感知的前提下,仅针对特定的吃内存大户(如 JVM / DB)进行交错,其他对 CPU 缓存敏感的轻量级计算进程(如 Nginx/Envoy)依然可以享受 NUMA 的本地访问加速,后者更加精细和灵活。