kswapd0 吃满 100% CPU,free -h 却显示 available 还有 100GB+——Linux NUMA zone_reclaim_mode 引发的 direct reclaim 风暴:内核代码级根因与解决方案

人工智能Agent 2026-07-05 84
预计阅读时间:15 分钟

搬运并适配自国外社区真实案例、LWN.net 内核开发讨论及 Red Hat 知识库。原文出处见文末。

TL;DR

在双路/多路 NUMA 服务器上,如果 vm.zone_reclaim_mode 被设置为非零值(某些 BIOS/内核版本可能默认开启),Linux 会在单个 NUMA 节点内存紧张时拒绝使用远程节点的空闲内存,转而触发 direct reclaim(直接回收)——即使 free -h 显示全局还有 100GB+ 的 available 内存。这会导致 kswapd 吃满 CPU、应用程序 P99 延迟飙升数十倍、系统吞吐量雪崩式下降。2025 年 12 月 Linux 内核社区已正式提交 RFC 补丁废弃该参数(删除 466 行代码,只保留 9 行)。本文从内核页分配器代码路径出发,解释这个机制为什么"actively harmful"。

现象

生产环境一台双路 Xeon Gold 服务器,256GB 内存(每路 128GB),跑 PostgreSQL + Redis: - free -h 显示 available 还有 140GB,负载很低 - 但 top 显示 kswapd0 吃满一个 CPU 核心的 100% sys 时间 - 应用 P99 延迟从 3ms 飙升到 800ms+ - 系统整体 CPU sys 使用率飙升到 80%+ - echo 3 > /proc/sys/vm/drop_caches 可以暂时缓解,但过一段时间问题重现 - 持续时间从几分钟到半小时不等,然后自动恢复 最诡异的是:没有任何进程实际使用那么多内存,free 内存充裕,系统却在疯狂做内存回收。

排查过程

第一回合:常规检查——颠覆认知

$ free -h
              total        used        free      shared  buff/cache   available
Mem:          251Gi        98Gi        16Gi       1.2Gi       137Gi       140Gi
Swap:            0B          0B          0B

available 有 140GB,swap 没开。没有 OOM killer 事件。但 kswapd0 在 top 里稳定占据 100% CPU。

第二回合:看 /proc/vmstat——发现问题

$ grep -E 'pgscan|pgsteal|allocstall|zone_reclaim' /proc/vmstat
pgscan_kswapd 12843017
pgscan_direct 89421156        # ⚠️ direct reclaim 扫描了 8900 万页!
pgsteal_kswapd 9842031
pgsteal_direct 67120456       # ⚠️ direct reclaim 回收了 6700 万页!
allocstall 34321              # ⚠️ 分配停滞 34000+ 次
zone_reclaim_success 0
zone_reclaim_failed 28904     # ⚠️ zone reclaim 失败 28000+ 次

关键信号: - pgscan_direct 远远大于 pgscan_kswapd——说明系统在走 direct reclaim 路径,而不是正常的 kswapd 后台回收 - allocstall 很高——每次 allocstall 意味着有一个进程在分配内存时被阻塞,等待内核回收页面 - zone_reclaim_failed 很大——zone reclaim 被尝试了大量次数但都失败了

第三回合:检查 NUMA 拓扑

$ numactl --hardware
available: 2 nodes (0-1)
node 0 cpus: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
node 0 size: 128917 MB
node 0 free: 1423 MB              # ⚠️ Node 0 只剩 1.4GB!
node 1 cpus: 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
node 1 size: 129020 MB
node 1 free: 89562 MB             # Node 1 空闲 89GB

真相大白:Node 0 几乎耗尽了内存(只剩 1.4GB free,低于 watermark),而 Node 1 有 89GB 空闲。全局的 free -h 看不出这个问题,它把两个节点的空闲内存加在一起了。 再看每个 NUMA 节点的内存分配统计:

$ numastat
                           node0           node1
numa_hit              1247839201      891204356
numa_miss                 45023     124567890    # ⚠️ 1.2 亿次远程访问!
numa_foreign         124567890           45023
interleave_hit             2345            1892
local_node           1247790100      891183450
other_node               50124     124588796

Node 0 上的进程频繁跨节点访问 Node 1 的内存(1.2 亿次),而本应直接使用 Node 1 空闲内存的分配请求,却被 zone_reclaim_mode 拦截下来做本地回收。

第四回合:确认根因——zone_reclaim_mode

$ sysctl vm.zone_reclaim_mode
vm.zone_reclaim_mode = 1

就是这个参数! 它在某些内核版本/BIOS 组合下可能被自动设为 1(非零),导致 "优先本地回收,拒绝远程分配" 的激进策略。

根因:zone_reclaim_mode 的内核代码级解析

这个参数是干什么的

vm.zone_reclaim_mode 于 2005 年引入 Linux 内核,目的是在 NUMA 系统上避免跨节点内存访问的高延迟。 它的核心逻辑在 mm/page_alloc.cget_page_from_freelist() 函数中——这是内核页分配器的核心函数,每次 malloc() / mmap() 最终都会走到这里。 简化后的代码路径如下:

/* mm/page_alloc.c — get_page_from_freelist() 简化逻辑 */
static struct page *
get_page_from_freelist(gfp_t gfp_mask, unsigned int order,
                       const struct alloc_context *ac)
{
    /* 遍历 zonelist:先本地 zone,后远程 zone */
    for_next_zone_zonelist_nodemask(zone, z, ac->zonelist, ...) {
        /* 检查当前 zone 是否满足 watermark */
        if (!zone_watermark_fast(zone, order, mark, ...))
            continue;  /* 不满足 → 看下一个 zone */
        /* ⚠️ 关键分支:如果 zone_reclaim_mode 开启 */
        if (node_reclaim_enabled() && 
            zone_allows_reclaim(ac->preferred_zoneref, zone)) {
            /* 尝试在本地节点做 page reclaim,而不是跳去远程节点 */
            ret = node_reclaim(zone->zone_pgdat, gfp_mask, order);
            switch (ret) {
            case NODE_RECLAIM_NOSCAN:
                continue;  /* 回收失败 → 继续下一个 zone */
            case NODE_RECLAIM_FULL:
                /* 回收完成 → 重新扫描本地 zone */
                goto try_this_zone;
            }
        }
        /* 从当前 zone 分配页面 */
        page = rmqueue(ac->preferred_zoneref->zone, order, gfp_mask);
        if (page)
            return page;
    }
    return NULL;  /* 所有 zone 都无法满足 → OOM */
}

node_reclaim() 做了什么?

node_reclaim() 定义在 mm/vmscan.c 中。当它被调用时,它会同步地触发 direct reclaim(直接回收)——不是唤醒后台的 kswapd 慢慢干活,而是阻塞当前进程,直到回收了足够的内存页面。

/* mm/vmscan.c — node_reclaim() 核心逻辑 */
int node_reclaim(struct pglist_data *pgdat, gfp_t gfp_mask, unsigned int order)
{
    struct scan_control sc = {
        .nr_to_reclaim = max(SWAP_CLUSTER_MAX, 
                             high_wmark_pages(zone) - min_wmark_pages(zone)),
        .gfp_mask = gfp_mask,
        .order = order,
        .may_writepage = !!(node_reclaim_mode & RECLAIM_WRITE),
        .may_unmap = !!(node_reclaim_mode & RECLAIM_UNMAP),
        .reclaim_idx = gfp_zone(gfp_mask),
    };
    /* 同步回收:调用者被阻塞 */
    return do_try_to_free_pages(pgdat, &sc);
}

这就是 kswapd 吃满 CPU 的原因:kswapd 虽然是后台线程,但当 zone_reclaim_mode 开启时,每个内存分配请求(来自应用程序、内核、任何地方)都可能触发同步的 direct reclaim。direct reclaim 可能回收不成功,然后重试,再失败,从而进入一个紧循环——kswapd 和应用程序轮流尝试回收,但谁都回收不出来,因为: 1. Node 0 的页面大多是热的(应用程序正在使用) 2. Node 1 有大量空闲内存,但 zone_reclaim_mode 不允许使用 3. 回收热页面 → 页面被换出 → 应用程序立刻又 fault 回来 → 再次回收 → 恶性循环

为什么这是 "actively harmful"

2025 年 12 月,Meta(Facebook)的内核工程师 Joshua Hahn 向 LKML 提交了 RFC 补丁废弃 zone_reclaim_mode,补丁删除了 466 行代码,只新增 9 行。他在补丁的 cover letter 中分析了四种场景: | 场景 | zone_reclaim_mode 的行为 | 结论 | |------|------------------------|------| | 1. 工作集完整放入一个 NUMA 节点 | 无事发生(无压力) | 无用 | | 2. 多个 workload 竞争一个节点 | 一个 workload 的分配触发 direct reclaim,没有公平性机制,导致另一个 workload 被饿死 | 有害 | | 3. 工作集跨多个 NUMA 节点 | 持续触发 direct reclaim,内存颠簸(thrashing) | 有害 | | 4. 只有新分配内存是热的才受益 | 但内核不是预言机(oracle),不知道新分配的热不热 | 不可靠 | 核心问题zone_reclaim_mode 是一个系统级的、全局的赌注——"我赌回收本地页面的代价小于远程访问的代价"。但在混合工作负载环境中,这个赌注几乎永远是错的。 LinkedIn 工程团队的实际数据:关闭 zone_reclaim_mode 后,P99 延迟降低 400%(从 ~200ms 降到 ~2ms)。

解决方案

方案一:关闭 zone_reclaim_mode(首选,即刻生效)

# 临时关闭
sudo sysctl -w vm.zone_reclaim_mode=0
# 永久关闭
echo 'vm.zone_reclaim_mode = 0' | sudo tee /etc/sysctl.d/99-zone-reclaim.conf
sudo sysctl -p /etc/sysctl.d/99-zone-reclaim.conf

无需重启,立即生效。这是绝大多数生产环境应该采取的设置。

方案二:使用 NUMA 内存策略替代(精细控制)

如果你的 workload 确实需要 NUMA 本地性(比如 HPC 场景),不要用全局的 zone_reclaim_mode,而是使用 numactlmbind()

# 将 PostgreSQL 绑定到 Node 0,只使用本地内存
numactl --cpunodebind=0 --membind=0 postgres -D /pgdata
# 或者使用 preferred 策略:优先本地,但不禁止远程
numactl --cpunodebind=0 --preferred=0 postgres -D /pgdata

membind:进程只能从指定节点分配内存。当指定节点的内存用完时,分配会失败(返回 NULL),而不是触发 zone reclaim。这给了应用程序一个明确的失败信号,而不是静默的性能退化。 preferred:优先从指定节点分配,但允许 fallback 到其他节点。这是大多数场景下更好的选择。

方案三:升级到未来可能移除该参数的内核

Joshua Hahn 的 RFC 补丁如果在 Linux 6.15+ 合入,zone_reclaim_mode 将被彻底移除。届时升级内核即可。

方案四:调整 NUMA 节点间距离(高级)

如果你确实需要 zone_reclaim_mode(某些特殊 HPC 场景),且你的平台远程访问延迟极高,可以调整 zone_reclaim_distance 阈值。但这个参数不是 sysctl,需要编译内核时修改 include/linux/topology.h

#define RECLAIM_DISTANCE 30  /* 默认值:NUMA distance < 30 时不触发 zone reclaim */

增大这个值意味着只有 NUMA distance 更大的节点才会触发 zone reclaim,减小则相反。

如何自检你是否中招

一行命令快速检查

sysctl vm.zone_reclaim_mode

如果不是 0,你需要往下看。

检查是否已经产生性能影响

# 查看 vmstat 中的 direct reclaim 和 alloc stall 计数
awk '/^pgscan_direct|^allocstall|^zone_reclaim_failed/ {printf "%s = %d\n", $1, $2}' /proc/vmstat

如果 pgscan_direct 等同于或超过 pgscan_kswapd,且 allocstall > 1000,你的系统已经受到影响了。

检查 NUMA 节点内存不均

# 一行看所有 NUMA 节点的空闲内存
numactl --hardware | awk '/node [0-9]+ free:/ {print "Node " $2, $4 "MB free"}'

如果某个 node 的 free 远小于其他 node(比如差了一个数量级以上),而全局 free 还很充裕,就是典型的 NUMA 节点间不平衡。

持续监控脚本

#!/bin/bash
# 每 10 秒采集一次 NUMA 内存分布和 direct reclaim 指标
while true; do
    echo "=== $(date) ==="
    numactl --hardware | grep "free:"
    awk '/pgscan_direct|pgscan_kswapd|allocstall/ {printf "%s=%d ", $1, $2}' /proc/vmstat
    echo
    sleep 10
done

启示

  1. free -h 是全局视图,NUMA 系统上它可能严重误导你。Node 0 内存耗尽 + Node 1 内存充裕 = free -h 显示 "很好",但实际在疯狂做 direct reclaim。
  2. 系统级的全局开关几乎总是坏的zone_reclaim_mode 是一个典型案例:它假定你知道所有进程的内存访问模式,但现实是混合 workload 环境。永远优先使用 per-process 的 NUMA 策略(membind/preferred/interleave)。
  3. pgscan_direct >> pgscan_kswapd 是一个危险信号。正常情况下,kswapd 应该在后台完成大部分回收工作,direct reclaim 只作为最后的兜底。当 direct scan 远超 kswapd scan 时,意味着进程在每次分配时都被迫自己去做回收。
  4. 内核参数不是"设了就完了"zone_reclaim_mode 可能在特定的 BIOS/固件/内核版本组合下被自动设为 1(从 ACPI SLIT 表计算而来)。部署新硬件后务必检查这个参数。
  5. 关注 LKML。2025 年 12 月的 RFC 补丁表明内核社区已经认识到 zone_reclaim_mode 的设计缺陷。466 行的删除说明:有时候最好的优化是删除错误的优化

原始出处: - ServerFault: kswap using 100% of CPU even with 100GB+ of RAM available(answered by J. Grizzard) - LWN.net: Deprecate zone_reclaim_mode — RFC patch by Joshua Hahn (Meta)(Dec 2025) - kernel-internals.org: NUMA & Reclaim — per-node kswapd, zone_reclaim_mode, NUMA balancing interaction - Red Hat KB: The pitfalls of using zone_reclaim_mode in a multi-node server (RHEL8) - DataStax Apache Cassandra Docs: Recommended production settings — Disable zone_reclaim_mode on NUMA systems - LinkedIn Engineering: Optimizing Linux Memory Management for Low-latency / High-throughput Databases - Linux Kernel Mailing List: Default zone_reclaim_mode = 1 on NUMA kernel is bad for file/email/web servers


本文首发于 CSDN 专栏《运维漏洞指南》,专注于搬运和适配国外社区已验证的生产级运维排障方案。欢迎关注。


本文由 admin 原创,转载请注明出处。

相关推荐

评论

0
暂无评论,来发表第一条评论吧

发表评论

登录 后发表评论

发现更多