Linux 内存 Watermark 工作机制详解:从 3.x 到 6.x
原文地址:https://github.com/ForceInjection/linux-from-beginner-to-master/blob/main/kernel/memory_watermark.md
1. 引言
在 Linux 内核的物理内存管理中,伙伴系统(Buddy Allocator)通过一系列水位线(Watermark)来判断系统的内存健康度,并驱动 kswapd、direct reclaim 乃至 OOM Killer 的触发时机。
这三条水位线(WMARK_HIGH、WMARK_LOW、WMARK_MIN)直接决定:
• 页面分配是否会触发回收 • kswapd 是否启动或退出 • 系统是否进入内存紧张或崩溃态 • NUMA node 间如何维持内存平衡
理解水位线机制是分析 内存压力、性能抖动、OOM、kswapd 频繁唤醒 等问题的基础。本文将统合 Linux 3.x 到 6.x 的演进过程,帮助读者全面掌握这一核心机制。
2. Watermark 的作用与整体工作机制
2.1 三条水位线简述
无论内核版本如何演进,三条核心水位线的语义保持不变:
| 水位线 | 定义 (Relationship) | 语义 | 触发行为 |
|---|---|---|---|
| WMARK_HIGH | MIN + 2 * distance | ||
| WMARK_LOW | MIN + distance | ||
| WMARK_MIN | min_free_kbytes 转换) |
核心公式:
• WMARK_MIN由系统参数min_free_kbytes决定。• distance是水位线之间的间距,其计算逻辑是版本演进的核心差异点。
2.2 内存分配时的决策流程
+-------------------------------+
| Free pages > WMARK_LOW |
| → 内存健康,正常分配 |
+-------------------------------+
|
v
+-------------------------------+
| Free pages <= WMARK_LOW |
| → 唤醒 kswapd(异步) |
| → kswapd 回收到 HIGH 后休眠 |
+-------------------------------+
|
v
+-------------------------------+
| Free pages <= WMARK_MIN |
| → 内核进入 direct reclaim |
| → 无法恢复 → 可能触发 OOM |
+-------------------------------+3. 初始化与基准值计算
Watermark 的初始化始于系统启动阶段,核心是确定系统级的保留内存总量 min_free_kbytes。
3.1 计算 min_free_kbytes
min_free_kbytes 用于防止高并发场景下的内存分配抖动。其计算公式在各版本中保持一致,采用平方根增长策略:
// mm/page_alloc.c
lowmem_kbytes = nr_free_buffer_pages() * (PAGE_SIZE >> 10);
min_free_kbytes = int_sqrt(lowmem_kbytes * 16);设计考量:
• 内存越大,预留量适当增加。 • sqrt函数确保平滑增长,避免在大内存机器上浪费过多内存。
3.2 数值上限的演变
虽然计算公式一致,但内核对自动计算值的上限限制发生了变化:
• Linux 3.x / 早期 4.x:上限通常为 64 MB ( 65536 KB)。• Linux 5.x / 6.x:上限提升至 256 MB ( 262144 KB)。
注意:管理员可以通过
sysctl手动设置/proc/sys/vm/min_free_kbytes来突破这些软限制。
3.3 WMARK_MIN 的分配
系统总的 min_free_kbytes 确定后,内核会将其按比例分配给每个 Zone:
// 按 Zone 大小比例分配
zone_min = pages_min * zone_managed_pages(zone) / total_lowmem_pages;
WMARK_MIN = zone_min;4. 水位线间距 (Distance) 的演进
这是新老内核最大的区别。distance 决定了 kswapd 开始回收(LOW)到停止回收(HIGH)之间的缓冲空间大小。
4.1 传统模式:固定比例 (Linux 3.x - 4.5)
在早期内核中,distance 与 WMARK_MIN 绑定,采用固定比例:
distance = WMARK_MIN / 4
• WMARK_LOW = MIN * 1.25• WMARK_HIGH = MIN * 1.5
缺陷:在大内存系统中,由于 WMARK_MIN 有上限(如 64MB),导致 distance 也被限制在很小的值(如 16MB)。对于吞吐量巨大的现代系统,16MB 的缓冲瞬间就会被耗尽,导致 kswapd 频繁唤醒甚至直接进入 direct reclaim,引发严重的性能抖动。
4.2 现代模式:比例扩展 (Linux 4.6+)
为了解决上述问题,Linux 4.6 引入了 watermark_scale_factor 参数(默认 10,即 0.1%)。
计算逻辑:取以下两者的较大值
1. 基准间距: WMARK_MIN / 4(兼容旧逻辑)2. 比例间距: 总内存 * watermark_scale_factor / 10000
// mm/page_alloc.c (4.6+ 版本)
distance = max(WMARK_MIN / 4,
mult_frac(zone_managed_pages(zone),
watermark_scale_factor, 10000));优势:水位线间距随内存大小线性增长。在 256GB 内存的机器上,默认 0.1% 的间距可达 256MB,提供了充足的缓冲空间,大幅减少了抖动。
5. 实战案例验证 (基于真实生产环境)
为了验证上述理论,我们采集了一台生产环境服务器的数据进行比对。
服务器配置:
• 内存总量:251 GB • NUMA 拓扑:2 个 Node • 内核版本: 3.10.0-1160.119.1.el7.x86_64(CentOS 7,符合传统模式)• 关键参数:管理员已将 /proc/sys/vm/min_free_kbytes手动调整为 1 GB (1,048,576 KB)。
5.1 实际采样数据
通过 cat /proc/zoneinfo 采集到的实际水位线与 Managed 页面数:
| Node/Zone | Managed Pages | MIN (实际) | LOW (实际) | HIGH (实际) |
|---|---|---|---|---|
| N0 DMA | ||||
| N0 DMA32 | ||||
| N0 Normal | ||||
| N1 Normal |
(注:N0 Normal 的 Managed Pages 约为 124 GB,N1 Normal 约为 126 GB)。
5.2 验证一:WMARK_MIN 的分配
系统总 min_free_kbytes 为 1GB,对应 262,144 页。
总 Managed Pages 为 65,914,666。
我们逐个验证各 Zone 的分配比例:
1. Node 0, DMA Zone (极小区域): 262,144 * (3,973 / 65,914,666) = 15.8-> 15
(实际值: 15 —— 完全一致)2. Node 0, DMA32 Zone (较小区域): 262,144 * (388,293 / 65,914,666) = 1,544.1-> 1,544
(实际值: 1,544 —— 完全一致)3. Node 0, Normal Zone (大区域): 262,144 * (32,501,579 / 65,914,666) = 129,259.4-> 129,259
(实际值: 129,259 —— 完全一致)4. Node 1, Normal Zone (大区域): 262,144 * (33,020,821 / 65,914,666) = 131,324.6-> 131,324
(实际值: 131,324 —— 完全一致)
结论:内核极其精确地按照内存占比将保留内存分配给了每一个 Zone。
5.3 验证二:水位线间距 (Distance)
该内核为 3.10,应遵循 distance = MIN / 4。我们选取不同规模的 Zone 进行验证。
1. Node 0, DMA32 Zone (中等规模):
• 实际 MIN: 1,544 • 理论 Distance: 1,544 / 4 = 386• 理论 LOW: 1,544 + 386 = 1,930—— 与实际值 (1,930) 完全一致• 理论 HIGH: 1,544 + 386 * 2 = 2,316—— 与实际值 (2,316) 完全一致
• 实际 MIN: 129,259 • 理论 Distance: 129,259 / 4 = 32,314.75-> 取整32,314• 理论 LOW: 129,259 + 32,314 = 161,573—— 与实际值 (161,573) 完全一致• 理论 HIGH: 129,259 + 32,314 * 2 = 193,887—— 与实际值 (193,888) 基本一致 (仅差 1 页,源于浮点取整误差)
• 实际 MIN: 131,324 • 理论 Distance: 131,324 / 4 = 32,831• 理论 LOW: 131,324 + 32,831 = 164,155—— 与实际值 (164,155) 完全一致• 理论 HIGH: 131,324 + 32,831 * 2 = 196,986—— 与实际值 (196,986) 完全一致
5.4 对比:如果是 5.x 内核会怎样?
假设我们将该服务器升级到 5.x 内核,且保持 watermark_scale_factor = 10 (0.1%)。
此时 distance 计算逻辑变为:
Scale Distance = 33,020,821 (Zone Pages) * 10 / 10000 = 33,020 Pages
Base Distance = 131,324 / 4 = 32,831 Pages
Distance = max(32,831, 33,020) = 33,020 Pages分析:在这个特定案例中,管理员手动将系统级 min_free_kbytes 设为 1GB。由于该 Zone (Node 1 Normal) 约占总内存的 50%,其分得的 WMARK_MIN 约为 512MB。因此,旧算法计算出的间距 distance = 512MB / 4 = 128MB,恰好与新算法 (0.1%) 计算出的间距 (~128MB) 相当。
这反过来说明了一个重要的优化经验:在旧内核中,手动大幅调大 min_free_kbytes 是缓解内存抖动最有效的手段,其本质就是人工模拟了新内核 "增大水位线间距" 的效果。
6. 关键差异总结
| 水位线间距算法 | MIN / 4 | max(MIN/4, 内存总量 * scale_factor) |
| watermark_scale_factor | 不存在 | 存在 |
| min_free_kbytes 上限 | ||
| 抗抖动能力 | ||
| 内存分层支持 |
7. 常用命令速查
7.1 查看当前水位线
cat /proc/zoneinfo7.2 查看/修改 min_free_kbytes
cat /proc/sys/vm/min_free_kbytes
# 修改为 1GB
sysctl -w vm.min_free_kbytes=10485767.3 查看/修改 scale_factor (仅 4.6+)
cat /proc/sys/vm/watermark_scale_factor
# 修改为 0.2%
sysctl -w vm.watermark_scale_factor=208. 最佳实践建议
基于上述机制的分析与实战验证,我们整理了针对不同内核版本的调优最佳实践。
8.1 针对 CentOS 7 / 3.10 内核 (及其他旧版本)
在这些旧内核中,默认的 min_free_kbytes 计算公式过于保守(上限仅 64MB),对于 64GB 以上的大内存服务器完全不够用。
• 典型症状:系统看似还有大量 Buffer/Cache,但业务进程偶尔出现卡顿,监控显示 pgsteal_direct(直接内存回收) 计数增长。• 核心问题: distance(缓冲带) 太小 (64MB / 4 = 16MB)。在高速 I/O 场景下,这 16MB 的缓冲瞬间就会被填满,导致 kswapd 来不及回收,业务进程被迫挂起自己去回收内存。• 优化建议:手动调大 vm.min_free_kbytes。• 推荐公式: min(总内存 * 1%, 4GB)。• 128GB 内存:建议设为 1GB ( 1048576)。• 256GB+ 内存:建议设为 2GB - 4GB。 • 注意:不宜设置过大(如超过 10GB),否则会导致过早触发回收,且浪费可用内存。
8.2 针对 4.6+ / 5.x / 6.x 新内核
新内核引入了 watermark_scale_factor,默认值为 10 (0.1%),使得水位线间距能随内存大小线性增长。
• 常规场景:保持默认值 10即可。在 256GB 内存机器上,这提供了约 256MB 的缓冲带,远优于旧内核。• 高吞吐/低延迟场景 (如万兆/25G 网络网关、高性能数据库、NVMe 存储节点): • 优化建议:将 vm.watermark_scale_factor调大至 50 (0.5%) 甚至 100 (1%)。• 收益:将缓冲带扩大 5-10 倍,确保在突发流量到来时,kswapd 有足够的时间在后台从容回收内存,彻底杜绝 Direct Reclaim。
8.3 关键监控指标
不要只关注 free 内存的绝对值,而应关注内存回收的行为指标 (来自于 /proc/vmstat):
| pgsteal_direct | 应恒为 0 | |
| pgsteal_kswapd | ||
| pageoutrun |
9. 小结
Linux 内存水位线机制经历了从“固定比例”到“线性扩展”的演进。
• 对于 3.x/4.x 老系统,运维的关键在于根据内存总量手动调整 min_free_kbytes,以人工模拟出足够的缓冲空间。• 对于 5.x/6.x 新系统,内核默认的 scale_factor机制已经能很好地处理大内存场景,通常无需过多干预,但在极端高吞吐场景下,仍可微调该因子以换取更极致的稳定性。
通过理解这一机制,我们可以更精准地排查 K8s 节点驱逐、数据库性能抖动等与内存回收相关的疑难杂症。