多代LRU(MGLRU)深度实战:Linux内核内存回收机制革新
引言
Linux内核的内存管理系统一直在不断演进。从传统的基于 active/inactive 双链表的LRU算法,到引入反向映射(reverse mapping),再到SLUB分配器的成熟,每一次改进都带来了显著的内存管理效率提升。
2022年,Linux 6.1引入了Multi-Generation LRU(MGLRU),这是近年来内存管理子系统最重要的变革之一。由Google工程师Yu Zhao主导开发的MGLRU,从根本上重新设计了页面回收的扫描策略,通过引入"代(generation)"的概念,大幅降低了扫描开销,提升了内存紧张时的系统响应能力。
本文将深入剖析MGLRU的设计原理、核心数据结构、扫描算法和性能表现,并给出在生产环境中的调优实践。
1. 传统LRU的局限性
1.1 双链表模型的问题
传统的Linux LRU使用两个链表来区分页面的冷热程度:
- Active list:最近被访问过的页面
- Inactive list:较长时间未被访问的页面
页面在两个链表之间移动,遵循以下规则:
- 页面第二次访问时,从 inactive 提升到 active
- 活跃太久且近期未访问的页面,从 active 降级到 inactive
- 回收时优先扫描 inactive list 的末尾
这个模型最大的问题是扫描开销。当内存紧张时,kswapd需要扫描大量页面来判断哪些可以回收,但很多页面可能已经被访问过(被标记为PG_referenced),需要多次扫描才能确定其最终的活跃状态。
1.2 Refault问题的困境
传统LRU面临的一个经典问题是页面再故障(page refault):一个页面被成功回收后,很快又被访问,不得不从磁盘重新读取。这意味着回收决策是错误的——该页面比预期的更"热"。
传统解决方案是在inactive list头部维护一个refault distance统计,但这是一种被动的补偿机制,无法从根本上解决问题。
1.3 大内存系统的扫描瓶颈
在现代服务器动辄拥有数百GB甚至数TB内存的背景下,线性扫描LRU列表的开销变得不可接受。扫描100GB内存中的页面可能需要数百毫秒,在这期间应用线程被阻塞,导致明显的延迟抖动。
2. MGLRU核心设计
2.1 代(Generation)概念
MGLRU的核心创新是引入"代"来替代active/inactive的二元划分。每个页面不再简单地标记为活跃或非活跃,而是带有时间戳(timestamp)和代数(generation number):
- gen:页面所属的代号,越大表示越新
- 年龄(age):页面在当前代中停留的时间,单位为jiffies
系统维护多个代(通常8个),每个代维护自己的LRU列表。页面根据其访问频率和最近访问时间被分配到不同的代中。
2.2 数据结构概览
MGLRU的核心数据结构是lruvec的扩展版本lru_gen_struct:
struct lru_gen_folio {
/* 每一代的LRU列表 */
struct list_head folios[MAX_NR_GENS];
/* 每一代的最大年龄(用于决定何时降级) */
unsigned long max_seq;
/* 最小年龄阈值 - 小于此年龄的页面不被回收 */
unsigned long min_seq;
/* 每个代当前的页面计数 */
unsigned long nr_pages[MAX_NR_GENS];
/* 代的利用率/延迟直方图 */
unsigned long timestamps[MAX_NR_GENS];
};
对于页面本身,MGLRU利用struct page中的现有字段来存储代信息:
// 使用page->flags的特定位来编码代号
#define LRU_GEN_MASK ((BIT(LRU_GEN_WIDTH) - 1) << LRU_GEN_PGSHIFT)
#define LRU_REFS_MASK ((BIT(LRU_REFS_WIDTH) - 1) << LRU_REFS_PGSHIFT)
这种零额外内存开销的设计是MGLRU的一大优势。
2.3 页面提升与降级
MGLRU中的页面生命周期:
- 新页面:被分配到当前最新的代(max_seq)
- 被访问:如果页面属于较老的代,可能被提升到新的代
- 老化:随着时间推移,页面所在的代会逐渐变老
- 回收:从最老的代(min_seq)开始回收
关键参数:
/* 最大代号,通常与页面flags中的位数相关 */
#define MAX_NR_GENS 4 // 编译时可配置,通常4~8代
/* 最小保留代数 - 此年龄以下的页面不会被回收 */
#define MIN_NR_GENS 2 // 默认至少保留2代不被回收
3. 扫描算法详解
3.1 迭代器模式
MGLRU的扫描采用迭代器模式,而非传统的从头到尾线性扫描。核心思想是:
/* 简化的扫描逻辑 */
for (gen = min_seq; gen < MAX_NR_GENS; gen++) {
/* 扫描指定代中最老的页面 */
list_for_each_entry_reverse(folio, &lrugen->folios[gen], lru) {
/* 根据页面年龄和引用状态决定是否回收 */
if (can_reclaim_folio(folio, gen, &scanned, &reclaimed))
reclaim_folio(folio);
/* 达到回收目标后停止 */
if (reclaimed >= target)
goto done;
}
}
与传统的线性扫描相比,MGLRU按代扫描的优势在于:
- 有序性:代内页面按时间排序,老页面自然在链表尾部
- 可控性:可以精确控制扫描的代数范围
- 增量性:扫描可以从任意代恢复,不需要从头开始
3.2 跨代扫描策略
当最老代的页面不够回收时,MGLRU会扩展到更老的代。这种策略的优雅之处在于:
- 优先回收最老代的页面(这些页面最不可能被重新访问)
- 只有当老代页面不够时,才考虑新代的页面
- 扫描深度可调,避免过早回收新页面导致refault
/* 自适应扫描深度调整 */
if (too_many_workingset_refaults())
min_seq--; // 放宽回收条件,允许回收更年轻的页面
else
min_seq++; // 收紧回收条件,减少不必要的扫描
3.3 工作集保护
MGLRU内置了强大的工作集(workingset)保护机制。通过追踪页面的再故障率,系统可以自适应地调整扫描策略:
- 如果大量页面在短时间内发生refault,说明回收过于激进,自动提升min_seq
- 如果refault率很低,说明回收策略合适,可以适当降低min_seq提高效率
- 这种反馈机制使MGLRU在各种工作负载下都能保持合理的回收行为
4. 性能分析
4.1 扫描效率提升
Google的测试数据显示,MGLRU在大规模内存系统上的表现:
| 场景 | 传统LRU | MGLRU | 提升 |
|---|---|---|---|
| 64GB内存扫描延迟 | ~45ms | ~8ms | ~5.6x |
| 256GB内存扫描延迟 | ~180ms | ~15ms | ~12x |
| 页面回收准确率 | 72% | 89% | ~24% |
| refault率 | 12% | 3% | ~75% |
4.2 延迟稳定性
MGLRU对系统延迟稳定性的改善尤为显著:
- p99延迟降低:在内存紧张时,传统LRU可能导致数百ms的停顿,MGLRU将这个值控制在10ms以内
- 抖动减少:扫描时间的方差大幅下降,系统响应更加可预测
- 尾延迟改善:对于延迟敏感的应用(如RPC服务),MGLRU将p999延迟降低了一个数量级
4.3 内存利用率
更智能的回收策略意味着:
- 在相同内存压力下,MGLRU能保持更多页面而不触发不必要的swap
- 页面缓存(page cache)的命中率更高,因为"误杀"更少
- 文件系统缓存不会被过度频繁回收,减少磁盘I/O
5. 生产环境调优指南
5.1 启用MGLRU
现代Linux系统(内核 ≥ 6.1)默认已启用MGLRU。可以通过sysctl进行配置:
# 检查MGLRU是否启用
cat /sys/kernel/mm/lru_gen/enabled
# 输出: Y (启用)
# 查看当前配置
cat /sys/kernel/mm/lru_gen/min_ttl_ms
# 输出: 0 (默认关闭TTL保护)
5.2 关键参数调优
min_ttl_ms - 最小生存时间(毫秒)
# 推荐值:
# - 数据库/延迟敏感应用:1000(1秒)
# - 一般Web服务:100(100毫秒)
# - HPC/批处理:0(不保护)
echo 1000 > /sys/kernel/mm/lru_gen/min_ttl_ms
这个参数控制页面在两次扫描之间的最短保护时间,防止被过早回收。
多代配置:
/* 查看可用的代配置 */
cat /sys/kernel/mm/lru_gen/max_seq // 当前最大代号
/* min_seq控制回收的下限,系统自动调整 */
cat /sys/kernel/mm/lru_gen/min_seq // 当前最小代号
5.3 监控与观测
MGLRU提供了丰富的统计信息:
# 查看各代的页面分布
cat /sys/kernel/mm/lru_gen/stats
# 典型的输出示例:
# node0-gen0: 1024 pages, 4MB
# node0-gen1: 2048 pages, 8MB
# node0-gen2: 512 pages, 2MB
# node0-gen3: 128 pages, 0.5MB
# node0-refault: 42 (1.2%)
5.4 与cgroup的集成
MGLRU与cgroup内存控制器良好集成:
# 在cgroup v2中使用MGLRU
mkdir /sys/fs/cgroup/myapp
echo "512M" > /sys/fs/cgroup/myapp/memory.high
# cgroup内的页面回收同样使用MGLRU
# 无需额外配置
6. 与现有机制的对比
6.1 MGLRU vs 传统LRU
| 维度 | 传统LRU | MGLRU |
|---|---|---|
| 扫描复杂度 | O(n),与总页面数线性相关 | O(g * m),与代数和代内页面数相关 |
| 回收准确率 | 受限于有限的引用位信息 | 基于多代时间线,更精准 |
| 延迟波动 | 内存紧张时波动大 | 相对稳定,可预测 |
| 内存开销 | 仅active/inactive两个列表 | 多代列表,但无额外per-page开销 |
| 并发控制 | 全局锁竞争激烈 | 降低GC持有锁的时间 |
6.2 MGLRU vs DAMON
DAMON(Data Access MONitoring)是另一种内存访问监控方案。两者不是竞争关系而是互补:
- DAMON:提供精确的访问信息采样,适合需要细粒度访问分析的场景
- MGLRU:轻量级页面回收引擎,专注于高效的回收决策
- 配合使用:DAMON可以为MGLRU提供更精确的访问数据,进一步提升MGLRU的决策质量
Linux 6.8+ 开始引入 DAMON_LRU_SORT,这是DAMON与MGLRU协同的典范实现:
# 启用DAMON辅助的LRU排序
echo damon_lru_sort > /sys/modules/damon_lru_sort/parameters/enable
7. 源码走读:核心回收路径
7.1 shrink_node入口
MGLRU的回收入口与内存压力检测紧密关联:
// mm/vmscan.c
static void shrink_node(pg_data_t *pgdat, struct scan_control *sc)
{
// 检查是否使用MGLRU路径
if (lru_gen_enabled()) {
lru_gen_shrink_node(pgdat, sc);
} else {
// 回退到传统LRU
shrink_lruvec(lruvec, sc);
}
}
7.2 lru_gen_shrink_node核心逻辑
static void lru_gen_shrink_node(pg_data_t *pgdat, struct scan_control *sc)
{
struct mem_cgroup *memcg = sc->target_mem_cgroup;
do {
struct lruvec *lruvec = mem_cgroup_lruvec(memcg, pgdat);
// 尝试从最老代回收
if (lru_gen_memcg_reclaim(lruvec, sc))
break;
// 如果回收不足,扩展到更老的代
cond_resched();
} while (!sc->nr_reclaimed || can_scrink(memcg, sc), 3);
}
7.3 页面世代判定
判断页面是否应该被回收的关键逻辑:
static bool isolate_folio(struct lruvec *lruvec, struct folio *folio,
struct scan_control *sc)
{
// 1. 检查TTL保护
if (folio_is_young(folio))
return false; // 页面太年轻,跳过
// 2. 检查访问引用
if (folio_test_referenced(folio)) {
// 页面近期被访问,清除引用位后跳过
folio_clear_referenced(folio);
return false;
}
// 3. 检查世代年龄
if (lru_gen_folio_age(folio) > sc->min_seq)
return false; // 页面所在代不够老
// 4. 通过所有检查,可以回收
return true;
}
8. 实践案例
8.1 大规模数据库场景
某生产环境数据库服务器(512GB内存)启用MGLRU后:
- Buffer pool 的 p99 延迟从 85ms 降至 12ms
- 不必要的fsync减少75%(因为page cache回收更明智)
- TPC-C吞吐量提升8%
8.2 Kubernetes容器平台
在K8s集群中部署MGLRU后的效果:
- 容器OOM频率降低62%
- 容器冷启动延迟降低35%(因为页面不被误回收)
- 节点级内存碎片减少,相关回收开销降低40%
8.3 冷热数据分离场景
大数据平台的冷热数据分层存储中:
- 热数据页面几乎不被回收(即使有大量冷数据被扫描)
- 冷数据页面被快速识别和回收
- 整体内存利用效率提升约20%
9. 总结与展望
MGLRU通过引入"代"的概念,从根本上优化了Linux内核的页面回收机制。相比传统LRU:
- 扫描效率:按代扫描避免了全局线性遍历,在超大内存系统上优势更加明显
- 回收准确率:基于多代时间线的回收决策,显著减少refault
- 延迟稳定性:大幅降低内存紧张时的延迟尖峰
- 自适应能力:内置的反馈环使系统能根据工作负载动态调整
随着Linux内核的持续发展(已经有v2版本的改进),MGLRU将进一步与机器学习辅助的页面预测、CXL内存分层等新技术融合。可以预见,MGLRU将成为未来十年Linux内存管理的基石机制。
对于正在维护大规模基础设施的团队来说,升级到支持MGLRU的内核版本,并合理配置min_ttl_ms等参数,是成本最低、收益最明确的内存优化手段之一。

发表评论 取消回复