Linux 内核 MGLRU 页面回收算法深度实战:从多代页面跟踪到生产级调优
一、引言:传统 LRU 为何需要被重新设计
Linux 内核的页面回收(Page Reclaim)子系统长期采用基于 Least Recently Used (LRU) 的双链算法。这一经典设计将页面分为 active 和 inactive 两个列表,通过页面访问的时序来判断其热度。然而随着服务器内存容量从数百 GB 扩展到数 TB,以及工作负载模式的多样化,传统 LRU 暴露出了日益明显的问题:
扫描开销过大:在 1TB 内存的服务器上,传统 LRU 在内存压力下可能需要扫描数百万个页面才能找到可回收目标。页面在活跃/非活跃链表之间"弹跳"(lru bomb)现象严重,导致无用的扫描浪费大量 CPU 周期。
工作集检测精度低:双链模型只有两个"温度级别",无法精确区分不同频率访问的页面。一个短期密集访问的页面可能在内存中停留很久无法被回收,而长期不访问的页面也可能因为偶然的一次访问被误判为热页。
启动时的"冷启动"问题:服务刚启动或新工作集载入时,所有页面热度相同,传统 LRU 需要较长时间才能建立有效的热度排序,期间可能做出错误的回收决策。
MGLRU(Multi-Generational LRU)正是为解决这些问题而生。它由 Google 工程师 Yu Zhao 主导开发,自 Linux 6.1(2022 年 12 月)起合入主线,并在 Google 的大规模生产环境中验证了显著的性能提升——特别是在工作集远大于内存的场景下,页面回收的效率和对系统性能的影响都有了质的改善。
二、核心设计思想:多代页面生命周期模型
MGLRU 的核心洞见是:用多个"代"(Generation)来跟踪页面的年龄,而不是传统 LRU 的简单活跃/非活跃二元判断。每个页面分配时被标记为当前最新的代,随着时间推移和扫描事件的发生,页面会"老去"进入更旧的代。回收时优先从最老的代中寻找候选页面。
2.1 代的距离与页面年龄
MGLRU 将页面的一生划分为多个代。关键概念包括:
- 总代距离(Total Generations):系统中同时存在的代数,通常由
CONFIG_LRU_GEN控制,默认值通常为 2 到 7 代不等。 - 页面年龄(Page Age):页面自最后一次被访问以来经过的时间。活跃页面(有 timestamp)可以直接计算其精确年龄。不活跃页面则通过它所在的代推断其相对年龄。
页面年龄的计算用高精度时间戳(通常由 TSC 或 ARM64 的 cntvct_el0 计数器提供)。在一次扫描事件(mm/vmscan.c 中的老化轮次,aging cycle)中:
页面年龄 = 当前时间戳 - 页面最后访问时间戳
2.2 多代保护机制
相比只有两代的传统 LRU,MGLRU 的多代设计使得:
- 热页永远停留在最新代:频繁访问的页面不会被错误回收。
- 冷页快速老化到旧代:减少无用的扫描开销。
- 真正的工作集边界清晰:最新代和次新代之间的边界就是当前工作集的边界。
Yu Zhao 在提交的论文中指出,在 Google 的数据中心场景下,启用 MGLRU 后系统:
页面回收吞吐量提升 20-35%,工作集页面被误回收率下降 50% 以上,系统尾延迟(p99 latency)改善 15-25%。
三、内核数据结构:`struct lruvec` 与 `struct lru_gen_folio`
MGLRU 在内核中的核心数据结构扩展了原有的 struct lruvec:
struct lru_gen_folio {
/* 代之间的距离(环形缓冲区) */
unsigned long max_seq; /* 当前最大代序号 */
unsigned long min_seq; /* 当前最小代序号(回收目标) */
unsigned long enabled; /* 是否启用 MGLRU(per-node) */
/* 按代组织的不活跃页面列表 */
struct list_head folios[MAX_NR_GENS];
/* 各代的页面计数 */
unsigned long nr_pages[MAX_NR_GENS];
/* 全局时间戳 */
unsigned long timestamps[MAX_NR_GENS];
};
struct lruvec {
/* 原有 LRU 列表(KCompat 保留) */
struct list_head lists[NR_LRU_LISTS];
/* MGLRU 核心 */
struct lru_gen_folio lrugen[NR_LRU_TYPES];
/* ... */
};
关键的函数调用链:
shrink_node() // 节点级别回收入口
└── shrink_node_memcg() // memcg 级别
└── shrink_list() // 类型(ANON/FILE)
└── shrink_folio_list() // 实际回收
└── folio_update_gen() // 页面代更新
└── folio_isolate() // 候选页面隔离
四、算法流程:扫描与回收
4.1 老化轮次(Aging Cycle)
当页面回收需要决定哪些页面可以回收时,老化轮次被触发:
老化触发条件:
1. min_seq 的代中页面数量超过阈值
2. 距离上次老化超过 min_ttl_ms(默认 0,可通过 sysctl 配置)
老化流程:
遍历活跃列表(active list)中的页面
├── 检查页面访问标志(PTE_ACCESSED / young bit)
├── 访问过 → 移到 max_seq(最新代)
└── 未访问 → 留在当前代(seq 不变)
递增 max_seq → 创建新一代
min_seq 的页面变为可回收候选
4.2 回收轮次(Reclaim Cycle)
回收直接针对最老的代(min_seq):
回收触发条件:
1. 内存压力(zone watermark 低于 low)
2. 显式回收请求(如 THP allocation、OOM killer 前置)
回收流程:
从 min_seq 代中取出候选 folio
├── 检查是否被 pin/locked → 跳过
├── 检查最近是否被访问(第二次机会)→ 移到 max_seq
└── 回收:
├── 干净页(clean page)→ 直接释放
├── 脏页→ 写回后释放
└── 匿名页→ swap out
当 min_seq 代的页面大多不可回收时,MGLRU 会临时提升 min_seq,将稍年轻的代也纳入回收候选。这种"代压缩"(generation compaction)机制确保在极端压力下仍有足够回收候选。
五、配置与调优:生产环境实战
5.1 编译时配置
CONFIG_LRU_GEN=y # 启用 MGLRU 框架
CONFIG_LRU_GEN_ENABLED=y # 启动时默认启用
CONFIG_LRU_GEN_STATS=y # 启用调试统计信息
# CONFIG_LRU_GEN_WALKS_MMU # 基于 MMU accessed bit 的扫描(ARM64/x86)
# CONFIG_LRU_GEN_WALKS_PMD # PMD 级别扫描(大页优化)
5.2 sysctl 运行时参数
# 控制是否启用(需在 boot 时开启 CONFIG_LRU_GEN_ENABLED)
sysctl -w vm.lru_gen.enabled=1
# 最小老化间隔(毫秒),0 表示由内部启发式决定
sysctl -w vm.lru_gen.min_ttl_ms=1000
# 查看当前 MGLRU 统计
cat /sys/kernel/debug/lru_gen
5.3 实际调优案例
案例一:Web 服务器内存压缩
对于周期性访问的 REST API 服务器,工作集相对稳定但存在"冷"的长尾页面。启用 MGLRU 后:
# 检查当前代的分布
cat /proc/zoneinfo | grep -A 5 "MGLRU"
# 观察回收效率
vmstat 1 30 # 观察 si/so(swap)频率
sar -B 1 30 # 观察 pgpgin/pgpgout
案例二:大数据分析(Spark/Presto)的突发工作集
当工作集突然从 GB 跃升到 TB 级别时,传统 LRU 需要几十秒才能形成正确的热度梯度,期间大量热页被错误回收。MGLRU 的多代设计加速了这一过程:
# 调整 min_ttl_ms 以匹配工作负载的访问模式
# 高波动性工作负载建议增大(2000-5000)
# 稳定工作负载可保持默认(0 或较小值)
sysctl -w vm.lru_gen.min_ttl_ms=3000
案例三:Kubernetes Pod 内存受限场景
在 memory.limit_in_bytes 严格限制的 cgroup 中,内存压力频发。精细控制 MGLRU 的回收频率可以明显降低频繁回收带来的性能抖动:
# 查看 cgroup MGLRU 统计
cat /sys/fs/cgroup/memory.stat
# 设置 min_ttl_ms 控制回收节奏
# 过高:响应慢,可能导致 direct reclaim
# 过低:回收频繁,CPU 开销增加
sysctl -w vm.lru_gen.min_ttl_ms=500
六、监控与故障排查
6.1 关键性能指标
# MGLRU 统计(debugfs)
grep -r "lru_gen" /sys/kernel/debug/
# 页面回收详细统计
grep -E "pgsteal|pgscan" /proc/vmstat | sort
# 代分布观测(需 CONFIG_LRU_GEN_STATS=y)
cat /sys/kernel/debug/lru_gen_full
典型输出示例(模拟):
node 0, anon 42217088 file 12435456
anon:
gen 0: 8192 42217088 (最新代 - 活跃工作集)
gen 1: 16384 42217088
gen 2: 4096 42217088 (最老代 - 回收候选)
file:
gen 0: 1024 12435456
gen 1: 512 12435456
6.2 常见问题排查
问题一:启用后反而内存压力增大
可能原因:min_ttl_ms 设置过大,老化轮次不频繁,导致大量页面堆积在同一代,回收时突发性大量 IO。
解决:降低 min_ttl_ms 或调高 vm.min_free_kbytes。
问题二:数据库场景下性能下降
某些数据库(如 MySQL、PostgreSQL)有自身的缓冲池管理,MGLRU 的全局视角可能与缓冲池策略冲突。
解决:在 NUMA 场景下,确保使用 vm.zone_reclaim_mode = 0 并测试在不同 min_ttl_ms 下的性能。如问题持续,可在特定 NUMA node 禁用 MGLRU:
# 通过 sysfs 在特定节点禁用
echo 0 > /sys/devices/system/node/nodeX/lru_gen_enabled
问题三:回收吞吐不及预期
检查 PTE accessed bit 翻转是否正常:
# 确认 MMU walker 是否可用
dmesg | grep -i "lru_gen"
# 输出应包含 "lru_gen: using arch MMU accessed bit"
如果 fallback 到 software walker,扫描开销会显著增加。确保内核配置了 CONFIG_LRU_GEN_WALKS_MMU,并在 CPU 支持的情况下(x86 的 A-bit / ARM64 的 young bit)优先使用硬件辅助。
七、性能基准数据对比
以下数据来自 MGLRU 官方测试(基于 Linux 6.1)及社区独立测试:
| 场景 | 传统 LRU | MGLRU (默认) | 提升幅度 |
|---|---|---|---|
| 工作集 = 2x 内存(网页缓存) | 65% 命中率 | 78% 命中率 | +20% |
| 工作集 = 5x 内存(大数据分析) | 41% 命中率 | 62% 命中率 | +51% |
| 启动冷启动建立热度梯度时间 | ~30s | ~8s | -73% |
| 回收 CPU 开销(扫描期间的 KIDLE) | 12% | 7% | -42% |
| 平均内存回收延迟 | 4.2ms | 2.1ms | -50% |
八、总结与未来展望
MGLRU 代表了 Linux 内核页面回收从"粗放双链"走向"精细多代"的重要一步。它的核心价值在于:
- 工作集感知:多代模型更精确地反映了页面的实际访问模式
- 扫描效率:只扫描最老的代,避免无效的全局扫描
- 渐进回收:代之间的平滑过渡避免回收抖动
- 硬件协同:利用 PTE accessed bit 等硬件特性降低开销
- 与 DAMON 的更深联动:利用 DAMON 的工作集分析优化代间距离判断
- AI 驱动的自适应参数调整:根据工作负载特征动态调整
min_ttl_ms和代总数 - ZRAM/ZSWAP 分层感知:区分闪存级内存和 DRAM,在不同层之间实现更精细的页面调度
随着 Linux 6.x 系列中 MGLRU 的不断成熟(6.6 后的默认启用和更完善的 NUMA 支持),这一机制有望成为未来服务器内存管理的标配。对于内存密集型业务场景(数据库、大数据缓存、容器平台),合理利用 MGLRU 可以显著提升系统效能。
目前 MGLRU 仍在继续演进,未来可能的方向包括:
对于工程师而言,理解 MGLRU 的原理不仅是掌握一个内核特性,更是对"如何利用数据结构反映物理世界规律"的一次绝佳示范。当工作集、访问模式、时间局部性这些概念被优雅地映射到多代页面生命周期中时,我们看到的是系统设计之美。

发表评论 取消回复