原力注入

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 的触发时机。

Image

这三条水位线(WMARK_HIGH、WMARK_LOW、WMARK_MIN)直接决定:

  • • 页面分配是否会触发回收
  • • kswapd 是否启动或退出
  • • 系统是否进入内存紧张或崩溃态
  • • NUMA node 间如何维持内存平衡

理解水位线机制是分析 内存压力、性能抖动、OOM、kswapd 频繁唤醒 等问题的基础。本文将统合 Linux 3.x 到 6.x 的演进过程,帮助读者全面掌握这一核心机制。


2. Watermark 的作用与整体工作机制

2.1 三条水位线简述

无论内核版本如何演进,三条核心水位线的语义保持不变:

水位线定义 (Relationship)语义触发行为
WMARK_HIGHMIN + 2 * distance
高水位。表示"内存充足"。
kswapd 达到 high 后休眠,不再回收。
WMARK_LOWMIN + distance
中水位。表示"需要异步回收"。
kswapd 被唤醒,开始后台 reclaim。
WMARK_MIN
Base (min_free_kbytes 转换)
最低水位。表示"严重不足"。
内存分配路径进入 direct reclaim,必要时触发 OOM。

核心公式:

  • • 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. 1. 基准间距:WMARK_MIN / 4 (兼容旧逻辑)
  2. 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/ZoneManaged PagesMIN (实际)LOW (实际)HIGH (实际)
N0 DMA
3,973
15
18
22
N0 DMA32
388,293
1,544
1,930
2,316
N0 Normal
32,501,579
129,259
161,573
193,888
N1 Normal
33,020,821
131,324
164,155
196,986

(注: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. 1. Node 0, DMA Zone (极小区域):
    262,144 * (3,973 / 65,914,666) = 15.8 -> 15
    (实际值: 15 —— 完全一致)
  2. 2. Node 0, DMA32 Zone (较小区域):
    262,144 * (388,293 / 65,914,666) = 1,544.1 -> 1,544
    (实际值: 1,544 —— 完全一致)
  3. 3. Node 0, Normal Zone (大区域):
    262,144 * (32,501,579 / 65,914,666) = 129,259.4 -> 129,259
    (实际值: 129,259 —— 完全一致)
  4. 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. 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) 完全一致
  • 2. Node 0, Normal Zone (大规模):
    • • 实际 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 页,源于浮点取整误差)
  • 3. Node 1, Normal Zone (大规模):
    • • 实际 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. 关键差异总结

    特性
    3.x / 早期 4.x
    5.x / 6.x
    水位线间距算法
    固定为 MIN / 4
    max(MIN/4, 内存总量 * scale_factor)
    watermark_scale_factor不存在存在
     (默认 10,即 0.1%)
    min_free_kbytes 上限
    64 MB
    256 MB
    抗抖动能力
    弱 (需手动调优)
    强 (自适应)
    内存分层支持
    不支持
    支持 (WMARK_PROMO)

    7. 常用命令速查

    7.1 查看当前水位线

    cat /proc/zoneinfo

    7.2 查看/修改 min_free_kbytes

    cat /proc/sys/vm/min_free_kbytes
    # 修改为 1GB

    sysctl -w vm.min_free_kbytes=1048576

    7.3 查看/修改 scale_factor (仅 4.6+)

    cat /proc/sys/vm/watermark_scale_factor
    # 修改为 0.2%

    sysctl -w vm.watermark_scale_factor=20

    8. 最佳实践建议

    基于上述机制的分析与实战验证,我们整理了针对不同内核版本的调优最佳实践。

    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
    后台内存回收次数
    正常增长是合理的,代表 kswapd 在正常工作。
    pageoutrun
    触发 kswapd 的次数
    过于频繁的增长可能意味着内存抖动。

    9. 小结

    Linux 内存水位线机制经历了从“固定比例”到“线性扩展”的演进。

    • • 对于 3.x/4.x 老系统,运维的关键在于根据内存总量手动调整 min_free_kbytes,以人工模拟出足够的缓冲空间。
    • • 对于 5.x/6.x 新系统,内核默认的 scale_factor 机制已经能很好地处理大内存场景,通常无需过多干预,但在极端高吞吐场景下,仍可微调该因子以换取更极致的稳定性。

    通过理解这一机制,我们可以更精准地排查 K8s 节点驱逐、数据库性能抖动等与内存回收相关的疑难杂症。