多代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中的页面生命周期:

  1. 新页面:被分配到当前最新的代(max_seq)
  2. 被访问:如果页面属于较老的代,可能被提升到新的代
  3. 老化:随着时间推移,页面所在的代会逐渐变老
  4. 回收:从最老的代(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会扩展到更老的代。这种策略的优雅之处在于:

  1. 优先回收最老代的页面(这些页面最不可能被重新访问)
  2. 只有当老代页面不够时,才考虑新代的页面
  3. 扫描深度可调,避免过早回收新页面导致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在大规模内存系统上的表现:

场景传统LRUMGLRU提升
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

维度传统LRUMGLRU
扫描复杂度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:

  1. 扫描效率:按代扫描避免了全局线性遍历,在超大内存系统上优势更加明显
  2. 回收准确率:基于多代时间线的回收决策,显著减少refault
  3. 延迟稳定性:大幅降低内存紧张时的延迟尖峰
  4. 自适应能力:内置的反馈环使系统能根据工作负载动态调整

随着Linux内核的持续发展(已经有v2版本的改进),MGLRU将进一步与机器学习辅助的页面预测、CXL内存分层等新技术融合。可以预见,MGLRU将成为未来十年Linux内存管理的基石机制。

对于正在维护大规模基础设施的团队来说,升级到支持MGLRU的内核版本,并合理配置min_ttl_ms等参数,是成本最低、收益最明确的内存优化手段之一。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部