Linux 内核页面回收与交换子系统深度工程实战

从 LRU 链表到 kswapd,从 swap 到 zswap,从碎片整理到 OOM —— 全方位解析 Linux 内核如何管理"内存不够用"的问题。


一、为什么内存永远不够用

现代 Linux 服务器动辄配备数百 GB 内存,却依然会在高负载场景下遭遇内存压力。理解页面回收(Page Reclaim)和交换(Swap)子系统,是从"能用"到"好用"的必经之路。

内存分配请求并非总能成功。当空闲内存降至阈值( watermark )时,内核必须回收页面以腾出空间。这个过程如果处理不当,会导致系统卡顿、服务超时甚至触发 OOM Killer 杀死关键进程。

本文覆盖以下核心内容:

  • 页面生命周期与 LRU 算法
  • 直接回收 vs 异步回收(kswapd)
  • Swap 子系统架构与性能权衡
  • zswap / zram 压缩交换技术
  • 内存碎片整理与页面迁移
  • 实战调优参数与排错方法

二、页面生命周期与 LRU 算法

2.1 页面的"冷热"之分

Linux 内核维护两条 LRU(Least Recently Used)链表来区分页面的活跃程度:

  • Active List(活跃链表):近期被访问过,暂不回收
  • Inactive List(非活跃链表):近期未被访问,优先回收

一个页面最初进入 Inactive List。如果它在被回收前被再次访问(通过硬件缺页异常或软件标记),就会被提升到 Active List。这本质上是二次机会算法(Second Chance)的实现。


Page Access Timeline:

  分配 ──→ Inactive List ──→ [被访问] ──→ Active List
                                │                    │
                      [未被访问,扫到]          [老化降级]
                                ↓                    ↓
                           回收出清 ←───────── Inactive List

2.2 文件页与匿名页的分化

内核对不同类型的页面采用不同的回收策略:

类型 来源 回收代价 回收方式
文件页(File Page) 磁盘文件映射 低(丢弃或写回) 直接丢弃脏页写回
匿名页(Anonymous Page) malloc/mmap 高(需写入 swap) 写入 swap 设备
Slab 页 内核对象缓存 中 释放对象
页缓存 文件读取缓冲 低 直接丢弃

文件页回收的代价远低于匿名页:干净的文件页可以直接丢弃(下次从磁盘重新读取),脏页写回磁盘即可;而匿名页没有后备存储,必须写入 swap 区域才能回收。

2.3 /proc/sys/vm/swappiness:回收偏好权衡

swappiness 控制内核在文件页和匿名页之间的回收倾向,取值 0-200:

  • swappiness=0:除非没有文件页,否则不回收匿名页(适合数据库服务器)
  • swappiness=60:默认值,平衡回收
  • swappiness=100:文件页和匿名页等比例回收
  • swappiness=200:优先回收匿名页(适合桌面/交互场景)

实际生产中,高性能数据库(如 PostgreSQL、MySQL InnoDB)常将 swappiness 设为 1-10,以尽量减少冷数据页被换出。


三、回收触发机制:kswapd 与 Direct Reclaim

3.1 水位线(Watermark)体系

每个内存区域(Zone)设置三条水位线:


 ┌──────────────────────────────────────────────────┐
 │                    Zone Size                      │
 │                                                  │
 ├── HIGH ──────────────────────────────────────────┤  ← 充足,kswapd 休眠
 │                                                  │
 ├── LOW ───────────────────────────────────────────┤  ← kswapd 开始回收
 │                                                  │
 └── MIN ───────────────────────────────────────────┘  ← 触发直接回收
     (pages_min)
  • HIGH:内存充裕,回收线程休眠
  • LOW:唤醒 kswapd 异步回收
  • MIN:触发直接回收(阻塞分配者)

3.2 kswapd:异步回收守护进程

每个 NUMA 节点运行一个 kswapd 守护进程。当空闲内存降至 LOW 水位时,kswapd 被唤醒,按预设步长(vm.min_free_kbytes)回收页面,直到恢复到 HIGH 水位。

kswapd 的工作流程:


kswapd 唤醒
    │
    ├── 检查每个 Zone 的水位
    │
    ├─→ Zone 内存 < LOW?
    │       │
    │       ├─Yes─→ 执行页面回收(按 swappiness 比例)
    │       │       ├── 扫描 Inactive List
    │       │       ├── 判断页面是否可回收
    │       │       ├── 回收文件页 / 换出匿名页
    │       │       └── 重复直到恢复到 HIGH
    │       │
    │       └─No─→ 继续下一个 Zone
    │
    └── 所有 Zone 充裕 → 进入睡眠

3.3 Direct Reclaim:同步直接回收

当分配请求到达时,若 MIN 水位以下的内存不足以满足分配,分配进程自身必须同步执行回收。这是性能杀手——业务线程被阻塞在内存回收上,导致延迟尖刺。

直接回收的典型症状:

  • sar -B 中 pgscank 和 pgscand 高频增长
  • iostat 中 swap 设备写入量突增
  • 应用 P99 延迟飙升

避免 direct reclaim 的策略:

  • 合理设置 vm.min_free_kbytes
  • 增加物理内存或减少内存超配
  • 使用内存 Cgroup 限制内存使用

四、Swap 子系统深度剖析

4.1 Swap 的不可替代性

尽管常有人建议"禁用 swap",但 swap 实际提供多重价值:

  1. 容纳冷内存:长时间不用的匿名页可以被换出,腾出物理内存给热数据和文件缓存
  2. 防止 OOM:提供最后的安全网,避免直接杀进程
  3. 休眠(Hibernate)支持:内存镜像保存到 swap
  4. 真正的问题不是"要不要 swap",而是"swap 有多快"。

    4.2 Swap 分区 vs Swap 文件

    Linux 支持两种 swap 存储:

    
    # Swap 分区:性能最优,无文件系统开销
    mkswap /dev/nvme0n1p4
    swapon /dev/nvme0n1p4
    
    # Swap 文件:灵活,可扩展
    dd if=/dev/zero of=/swapfile bs=1M count=8192
    chmod 600 /swapfile
    mkswap /swapfile
    swapon /swapfile
    

    Swap 文件在现代内核(≥4.0)上性能接近分区,因为内核通过 swap_info_struct 中的Extent映射直接定位磁盘块,绕过了文件系统缓存层。

    4.3 Swap Slot 分配算法

    内核使用基于集群(Cluster)的 Swap 分配策略:

    
    Swap Area Layout:
    
    ┌─────────┬───┬───┬───┬─────────┬───┬───┬───┬───┐
    │ Cluster │ 0 │ 1 │ 2 │ Cluster │ 3 │ 4 │ 5 │ 6 │
    │   A     │   │   │   │    B    │   │   │   │   │
    └─────────┴───┴───┴───┴─────────┴───┴───┴───┴───┘
      ← 8 pages →          ←     8 pages      →
    
    分配策略:从当前位置顺序分配,减少磁盘寻道
    连续页面尽量分配到同一 Cluster
    

    五、zswap 与 zram:压缩交换技术

    5.1 zswap:压缩缓存层

    zswap 不是独立的 swap 设备,而是在匿名页写入真正 swap 设备之前,先压缩存储在内存中的缓存层。

    
    匿名页 → zswap 压缩 → [命中率高,大部分停留内存]
                                  │
                        ┌── 驱逐时 ─┘
                        ↓
                  写入真正的 Swap 设备
    

    zswap 的工作流程:

    1. 内核准备换出匿名页
    2. 通过 zswap 压缩页面(默认使用 zstd/lz4 压缩)
    3. 压缩数据存入内存中的 zswap Pool
    4. 当 zswap Pool 达到上限(max_pool_percent),或页面长时间未访问时,才写入底层 swap 设备
    5. 核心优势:大量的"轻度压力"场景下,页面只被压缩而不真正写磁盘,减少 I/O 压力。

      5.2 zram:内存中的压缩块设备

      zram 创建一个基于 RAM 的压缩块设备,可以作为 swap 设备使用:

      
      # 创建 zram swap 设备
      modprobe zram num_devices=1
      echo zstd > /sys/block/zram0/comp_algorithm
      echo 8G > /sys/block/zram0/disksize
      mkswap /dev/zram0
      swapon /dev/zram0 -p 10  # 高优先级
      

      zram 与 zswap 的对比:

      特性 zswap zram
      本质 缓存层(前端) 独立 swap 设备
      后备存储 需要真实 swap 设备 无(压缩率在内存中)
      双重压缩风险 可能(zswap + swap 都用压缩) 无
      内存使用 动态(按使用率增长) 固定分配
      适合场景 有物理 swap 设备时叠加加速 无 physical swap / 嵌入式

      5.3 压缩算法选择

      zram/zswap 支持的压缩算法:

      
      ┌──────────┬────────────┬──────────┬────────────────┐
      │ Algorithm│ Compression│ Speed    │ Use Case       │
      ├──────────┼────────────┼──────────┼────────────────┤
      │ zstd     │ 高 (2.8:1) │ 中等     │ 通用首选       │
      │ lz4      │ 低 (2.1:1) │ 极快     │ 延迟敏感       │
      │ lzo-rle  │ 低 (2.0:1) │ 快       │ 嵌入式         │
      │ zstd-le  │ 较高       │ 中等     │ 较高压缩率优先 │
      │ 842      │ 低         │ 快(Accel)│ 有硬件加速器   │
      └──────────┴────────────┴──────────┴────────────────┘
      
      建议:通用场景用 zstd,延迟敏感用 lz4
      

      六、内存碎片整理与页面迁移

      3.1 外部碎片问题

      经过长时间运行后,物理内存可能散布着大量不连续的小页面块,导致无法满足大页(HugePage)或大内存块的连续分配请求。

      
      内存碎片示意(长时间运行后):
      
      [页:Used][页:Free][页:Used][页:Free][页:Used][页:Free]...
                          ↑
                  没有连续的大块空闲内存
      

      3.2 页面迁移(Page Migration)

      内核通过内存规整(Memory Compaction)解决外部碎片:扫描内存区域,将可移动页面迁移到新位置,将空闲页面聚合为连续块。

      
      规整前:
      ┌───┬───┬───┬───┬───┬───┬───┬───┐
      │ U │ F │ U │ F │ M │ F │ M │ F │
      └───┴───┴───┴───┴───┴───┴───┴───┘
           F=Free  U=Unmovable  M=Movable
      
      规整后:
      ┌───┬───┬───┬───┬───┬───┬───┬───┐
      │ U │ M │ M │ F │ F │ F │ U │ F │
      └───┴───┴───┴───┴───┴───┴───┴───┘
                    连续3个空闲页 ↑
      

      关键代码路径(简化):

      
      // mm/compact.c 核心逻辑
      enum compact_result compact_zone(struct zone *zone, struct compact_control *cc)
      {
          // 1. 扫描 Zone,分类页面为 Movable/Unmovable/Free
          // 2. 分离前后页面:将 Movable 页移到前端
          // 3. 后端形成连续空闲块
          // 3. 后端形成连续空闲块
      }
      

      3.3 Transparent Huge Pages(THP)与碎片

      THP 依赖连续的物理内存(2MB / 1GB)。碎片整理能为 THP 创造条件。

      THP 模式选择:

      
      # 禁用 THP(某些数据库推荐)
      echo never > /sys/kernel/mm/transparent_hugepage/enabled
      
      # 按需启用(默认 madvise)
      echo madvise > /sys/kernel/mm/transparent_hugepage/enabled
      # 然后应用通过 madvise(MADV_HUGEPAGE) 标记需要巨页的区域
      
      # 始终启用(激进,可能增加延迟)
      echo always > /sys/kernel/mm/transparent_hugepage/enabled
      

      实战建议:数据库类工作负载(MongoDB、PostgreSQL)通常建议禁用 THP,因为 THP 的延迟尖刺和碎片整理成本超过收益;而 HPC、大内存分析应用则受益于 THP。


      七、OOM Killer:最后的防线

      当所有回收手段耗尽,仍然无法满足分配请求时,OOM Killer 被触发。它根据评分算法选择"最该死"的进程杀掉。

      7.1 OOM Score 计算

      每个进程的 OOM 评分存储在 /proc//oom_score,计算基于:

      • 内存占用总量(RSS + Swap)
      • 运行时间(越长得分越低)
      • 是否持有重要资源(硬件设备、关键锁)
      • oom_score_adj 调整值(-1000 到 +1000)

      7.2 保护关键进程

      
      # 将关键进程标记为不可杀
      echo -1000 > /proc/$(pidof your_critical_service)/oom_score_adj
      
      # systemd 服务文件配置
      [Service]
      OOMPolicy=continue
      OOMScoreAdjust=-900
      

      7.3 OOM 事件分析

      
      # 查看上次 OOM 事件
      dmesg | grep -i "out of memory" | tail -20
      
      # 查看 OOM Killer 日志
      journalctl -k | grep "oom-kill"
      

      八、实战调优:/proc/sys/vm 关键参数

      8.1 核心参数速查表

      参数 默认值 说明 建议
      swappiness 60 匿名页回收倾向 DB: 1-10, Web: 30-60
      min_free_kbytes 自动计算 预留内存,避免 direct reclaim 大内存机器按 ~0.5-1% 设置
      vfs_cache_pressure 100 dentry/inode 缓存回收倾向 文件服务器: 50, DB: 150-200
      dirty_ratio 20 脏页占系统内存比例上限 写密集: 40-50
      dirty_background_ratio 10 后台刷脏页触发比例 写密集: 20-30
      overcommit_memory 0 内存超分配策略 DB: 2(严格审计)
      compact_memory - 触发内存规整 手动调试时用

      8.2 推荐配置模板

      通用文件服务器:

      
      # /etc/sysctl.d/99-memory.conf
      vm.swappiness = 10
      vm.vfs_cache_pressure = 50
      vm.dirty_ratio = 40
      vm.dirty_background_ratio = 10
      vm.overcommit_memory = 0
      vm.min_free_kbytes = 2097152  # 2GB on 64GB machine
      

      高性能数据库(PostgreSQL / MySQL):

      
      vm.swappiness = 1
      vm.overcommit_memory = 2
      vm.dirty_ratio = 15
      vm.dirty_background_ratio = 5
      vm.nr_hugepages = 2048  # 2MB huge pages
      # 禁用 THP
      echo never > /sys/kernel/mm/transparent_hugepage/enabled
      

      九、监控与问题排查

      9.1 关键监控指标

      
      # 实时查看页面回收活动
      sar -B 1 5
      
      # 输出列:
      # pgpgin/s, pgpgout/s, fault/s, majflt/s,
      # pgfree/s, pgscank/s (kswapd扫描), pgscand/s (直接回收扫描),
      # pgsteal/s (已回收页面)
      
      # 查看 swap 使用详情
      swapon --show
      cat /proc/swaps
      
      # 查看内存碎片情况
      cat /proc/buddyinfo
      cat /proc/pagetypeinfo
      
      # 查看 zswap 统计
      cat /sys/kernel/debug/zswap/*
      

      9.2 排查流程

      
      内存压力排查决策树:
      
      应用延迟飙升?
          │
          ├── 是 ──→ sar -B -- pgscand/s 高?
          │              │
          │              ├─Yes─→ Direct Reclaim 频繁,需加大 min_free_kbytes
          │              │       或扩容内存
          │              │
          │              └─No──→ swap 使用增长?
          │                         │
          │                         ├─Yes─→ 匿名页换出过多,检查 swappiness
          │                         │       考虑增加 RAM 或启用 zram
          │                         │
          │                         └──No──→ 检查文件缓存压力
          │                                   vfs_cache_pressure 是否合适?
          │
          └── ──→ 检查是否有进程内存泄漏
                    查看 /proc/<pid>/smaps 分析内存映射
      

      9.3 使用 eBPF/BCC 监控回收延迟

      
      # reclaimlat.py - 监控页面回收延迟 (基于 eBPF)
      from bcc import BPF
      
      prog = """
      #include <uapi/linux/ptrace.h>
      #include <linux/sched.h>
      
      BPF_HISTOGRAM(reclaim_latency, u64);
      
      int kretprobe__try_to_free_pages(struct pt_regs *ctx) {
          u64 latency = bpf_ktime_get_ns() - start_time;
          reclaim_latency.increment(bpf_log2l(latency / 1000)); // μs
          return 0;
      }
      """
      
      b = BPF(text=prog)
      reclaim_latency = b["reclaim_latency"]
      reclaim_latency.print_log2_hist("reclaim latency (us)")
      

      十、前沿技术演进

      10.1 DAMON:数据访问监控框架

      DAMON(Data Access MONitor)是 Linux 内核 5.15+ 引入的现代内存访问监控框架,可以:

      • 以页粒度采样监控内存访问模式
      • 为智能页面回收提供精确依据
      • 替代启发式的 LRU 扫描
      
      # 查看 DAMON 是否启用
      grep CONFIG_DAMON /boot/config-$(uname -r)
      
      # 使用 damo 工具监控某进程的访问模式
      damo record $(pidof my_app) &
      sleep 30
      damo report heats  # 生成内存访问热度图
      

      10.2 MGLRU:多代 LRU 算法

      MGLRU(Multi-Generational LRU)是 Google 贡献的下一代 LRU 算法,引入页面"代龄"概念,替代简单的 Active/Inactive 二分法,更精确地判断页面的冷热状态,减少误回收。

      
      # 检查当前内核是否支持 MGLRU
      cat /sys/kernel/mm/lru_gen/enabled
      
      # 启用 MGLRU(需要内核支持)
      echo 7 > /sys/kernel/mm/lru_gen/enabled
      

      总结

      Linux 内存回收与交换子系统是一个精密的协作体系:

      1. LRU 算法管理页面冷热分类,swappiness 决定回收偏好
      2. kswapd 负责异步回收,direct reclaim 是最后的同步手段
      3. Swap 提供匿名页的换出空间,zswap/zram 用压缩换性能
      4. 内存规整解决外部碎片,为连续分配创造条件
      5. OOM Killer 是兜底机制,需要合理配置保护关键进程
      6. 调优的核心思路不是追求"零 swap 使用",而是理解业务特性后,找到回收延迟、I/O 压力和内存利用率之间的最优平衡点。没有放之四海而皆准的配置,只有理解原理后针对场景的精准调优。


        延伸阅读:本文聚焦匿名页回收与交换机制。关于 Slub 分配器、页表管理与 HugePage 的实现细节,可参考本系列之前的《Linux 内核 Slub 分配器深度分析与调优实战》和《KVM 虚拟化:VMX 与 vCPU 深度工程实战》。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部
0.361918s