搬运并适配自国外社区真实案例、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.c 的 get_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,而是使用 numactl 或 mbind():
# 将 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
启示
free -h是全局视图,NUMA 系统上它可能严重误导你。Node 0 内存耗尽 + Node 1 内存充裕 =free -h显示 "很好",但实际在疯狂做 direct reclaim。- 系统级的全局开关几乎总是坏的。
zone_reclaim_mode是一个典型案例:它假定你知道所有进程的内存访问模式,但现实是混合 workload 环境。永远优先使用 per-process 的 NUMA 策略(membind/preferred/interleave)。 pgscan_direct >> pgscan_kswapd是一个危险信号。正常情况下,kswapd 应该在后台完成大部分回收工作,direct reclaim 只作为最后的兜底。当 direct scan 远超 kswapd scan 时,意味着进程在每次分配时都被迫自己去做回收。- 内核参数不是"设了就完了"。
zone_reclaim_mode可能在特定的 BIOS/固件/内核版本组合下被自动设为 1(从 ACPI SLIT 表计算而来)。部署新硬件后务必检查这个参数。 - 关注 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