尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
Linux Multi-Gen LRU:内存回收算法重构与调优实践
1. 从一次卡顿聊起传统 LRU 的回收困局如果你维护过内存比较大的 Linux 服务大概率遇到过这种场面free -h里 available 明明还有几十 G业务却突然从毫秒级延迟变成秒级dmesg 里没有 OOM但能看到大量 reclaim、refault 相关的活动。CPU 不高磁盘也没飙满问题就出在内核回收页面的策略上。Linux 内核 Multi-Gen LRU 算法下文简称 MGLRU就是针对这套老逻辑做的一次大重构。我最早接触 MGLRU不是从源码开始的而是从一台测试机上的诡异现象开始的。当时跑一个缓存型负载内存限制在可用容量的一半左右热数据反复读冷数据反复刷结果可用内存一直显示很健康但读延迟时不时冲高。后来把/proc/pressure/memory打开一看full压力经常到 40% 以上才意识到真正的问题是内核在“猜哪个页面该被淘汰”这件事上猜得不够准。这篇文章不是带你把源码逐行读一遍而是把我从遇到问题、翻实现、到在测试机上调通 MGLRU 这条线的经验整理出来。适合三类人一类是生产环境遇过内存卡顿、又不想盲目加内存的人一类是想理解现代内核内存回收机制的开发者还有一类是做云主机、容器集群内核参数调优的人。看完你应该能回答三个问题MGLRU 到底改了什么、怎么打开它、打开以后怎么判断它有没有用。1.1 两条 LRU 链表的天然缺陷在 MGLRU 合入之前内核传统的回收路径主要靠两套 LRU 链表active 和 inactive。内核按页面类型再分成匿名页anonymous和文件页file回收时从 inactive 链表的尾部摘页如果页面被访问过就尝试把它提升到 active 链表。这个设计最早是为了解决“最近最少使用”的近似实现但实际运行有几个绕不开的问题。第一个问题是只有两档年龄active 和 inactive。一个页面只要进了 active除非被主动降级否则可以赖很久而 inactive 链表又很容易被一次性读操作塞满。比如业务做一次全量扫描读了几 GB 的冷文件这些页面会把 inactive 尾部占掉真正热的数据反而被挤出去等下次要用时再触发缺页形成反复的 refault。第二个问题是回收时的扫描成本。系统内存越大两条链表越长。kswapd 或者直接回收路径为了找到“值得回收的页面”可能要扫掉一大段链表。同样的内存压力下扫描越久CPU 消耗越高业务延迟越难看。你可以把传统 LRU 理解成一个只分“重点班”和“普通班”的学校学生要么在重点班要么在普通班但学校和老师根本不知道谁真正每天都在学习只靠偶尔一次考试来决定分班。第三个问题是内存 cgroup 出现之后更明显。容器环境里每个 cgroup 有自己的内存上限回收需要在 cgroup 维度判断冷热。传统 LRU 的 active/inactive 判断粒度太粗两个容器可能争抢同一个活跃页面互相把对方的热数据挤出去。这些问题不是某一段代码写得不好而是“两档 LRU 被动访问标记”的结构性瓶颈。1.2 为什么“多代”这个思路值得期待MGLRU 的核心不是把 LRU 链表从两条变成四条而是把“页面年龄”这个概念从二值变成多代。每个页面不再只是 active 或者 inactive而是落在一个连续的代际里新访问过的页面进最新代一直没被访问的页面慢慢沉到最老代。回收时优先从最老代里挑页面这样热数据能被保护得更久冷数据也能更快被释放。这个思路听起来不复杂但难点在于工程实现怎么记录准确年龄怎么把访问信号快速汇总出来怎么避免页面表扫描本身变成新的性能瓶颈MGLRU 在 Linux 6.1 里合入后靠一套独立的数据结构和启发式扫描逻辑解决了这些问题。下面我把这套设计拆开讲。2. Multi-Gen LRU 的核心设计把页面的冷热换成代数2.1 多代模型年龄不再是二值传统 LRU 里页面要么 active要么 inactive后来虽然引入了 refault distance 之类的优化但本质上还是两档。MGLRU 引入了一个“代”generation的概念。每一代代表一批“年龄相近”的页面新页面或者刚被访问过的页面会进入最新的一代随着时间推进老一代逐渐变冷最后被回收。在内核源码里每个lruvec会维护一组 generation。lruvec可以粗略理解成某个 NUMA 节点和 memory cgroup 组合下的 LRU 上下文。也就是说MGLRU 天然支持 NUMA 和 cgroup 的隔离不是一套全局的简单链表。每个lruvec内部的数据结构大致长这样/* * 示意结构不代表某个具体内核版本的完整实现。 * 核心是多个 generation每个 generation 按页面类型和 zone 再拆分。 */ struct lru_gen_struct { struct list_head lists[MAX_NR_GENS][ANON_AND_FILE][MAX_NR_ZONES]; unsigned long nr_pages[MAX_NR_GENS][ANON_AND_FILE][MAX_NR_ZONES]; ... };你可以把它想象成仓库里的货架不是只有“常用区”和“不常用区”两个区域而是按批次分成多个区。最新进货的批次放在最前面时间久了往后挪清理仓库的时候从最老的批次开始往外扔。MAX_NR_GENS是一个很小的数常见实现里可能只有个位数档位但多这几档带来的排序精度已经比两档高很多。为什么不用固定时间片分代因为不同负载下同样一秒钟的“年龄”价值完全不同。一个数据库里的索引页可能被访问过后 5 分钟内依然很热而一个流式日志文件可能读完 1 秒后就不再需要。固定时间片会造成大内存场景下热页被误杀小内存场景下老页又迟迟不被回收。MGLRU 的 aging 推进是由内存压力触发的压力越大老化越快这比纯时间驱动更贴合实际。2.2 晋升与老化页面在哪里改代一个页面在 MGLRU 里的“年龄”不是靠内核定时器去改的而是靠两个关键动作晋升promotion和老化aging。晋升发生在页面被访问之后。内核里有很多路径都会标记页面被访问比如 page fault、mark_page_accessed()、Cgroup 的 refault 处理等。MGLRU 拿到这些信号后把页面从当前所在的老代移动到最新代。这个动作相当于说这个页面刚刚被用过它的“出生时间”被重置了。老化则发生在内存压力上升时。系统需要回收页面时内核会推进min_gen也就是把最老的那一代标记为可回收候选。最老的一代里包含的页面就是系统中“最近最没被用过的”一批。MGLRU 不会在一瞬间把最老代所有页面都回收掉而是按内存压力逐步回收。如果最老代里的页面其实还在被用回收后会触发 refault这个反馈信号会让内核调整后续的分代阈值。这里有个容易误解的地方MGLRU 的“代”不要求页面物理上重新排队到另一条链表。更准确地说它是用页面上的 generation 标记和一个动态推进的阈值来描述年龄。页面被访问后更新标记reclaimer 根据阈值判断哪些页面属于可回收范围。这种标记方式让移动成本比传统 LRU 的链表摘除、插入更低。2.3 tier 与类型只按年龄排序还不够年龄只能回答“这个页面多久没被访问了”但没法回答“这个页面被访问的频率怎么样”。一个页面可能刚刚被访问过一次但从整体频率看是冷数据另一个页面虽然有一段时间没被访问但一旦被访问就是高频热点。MGLRU 在同一个 generation 内部还会用 tier 来区分访问频率。tier 越高代表页面在代际内被访问的次数越多回收优先级越低。回收的时候优先从“老代 低 tier”里挑页面也就是年龄又老、访问频率又低的那批。这是 MGLRU 比传统 LRU 更精细的地方传统 LRU 只看最后一次访问时间容易把“偶尔用一次”的页面和“每天用一万次”的页面混在一起。页面的类型和 zone 维度也保留了下来。匿名页和文件页分开管理因为回收匿名页需要写回 swap成本远高于释放干净的文件页不同 zone 也分开统计避免回收时跨空区反复扫描。你可以把 generation 看成一个大排序维度tier、anon/file、zone 是里面的次要维度回收时按多个维度共同决定谁先被淘汰。2.4 把 PTE 扫描加进来一次主动的“查岗”MGLRU 最特别的地方是它不只是依赖 fault 路径里的访问标记还会主动去扫描页表读取硬件维护的 Accessed 位。CPU 访问一个 mapped page 时硬件会自动把对应的 PTE 里的 Accessed 位置 1。MGLRU 定期检查这些位就知道哪些页面最近被访问过然后把它们从老代提升到新代。这个过程很像“查岗”不是等一个人犯错了再问他上次是什么时候来的而是直接看考勤机记录。传统 LRU 的访问信号来源比较被动很多页面在 inactive 链路上待了很久内核并不知道它其实是热点MGLRU 通过 PTE 扫描能把“最近被访问”这个信号更主动地捞出来。当然PTE 扫描是有成本的。全量扫描一个大进程的页表会让 kswapd 的 CPU 占用变高。MGLRU 的实现里加了大量启发式比如只在内存压力到达一定阈值时才深入扫描扫描深度和内存压力成正比对已经被判断为很热的页面会减少重复扫描。实际使用中多数业务场景下 kswapd 的 CPU 上升不明显但如果是 page fault 非常密集的服务还是需要观察一下扫描带来的额外开销。3. 在内核里怎么打开它从编译开关到 sysfs3.1 先确认内核版本和支持状态MGLRU 在 Linux 6.1 正式合入所以内核版本低于 6.1 的发行版基本只能靠发行版自己 backport。先看一下当前内核版本uname -r再看看内核有没有编进 LRU_GEN 相关配置。如果/proc/config.gz存在zcat /proc/config.gz | grep LRU_GEN如果没有这个文件就查/boot下的配置文件grep -E CONFIG_LRU_GEN /boot/config-$(uname -r)正常情况下你会看到类似这样的结果CONFIG_LRU_GENy CONFIG_LRU_GEN_ENABLEDyCONFIG_LRU_GEN是核心开关CONFIG_LRU_GEN_ENABLED决定 MGLRU 默认是否打开。如果这两个配置缺失说明当前内核没有支持 MGLRU你需要换一个 6.1 以上、或者由发行版打上 backport 的内核。这里我的建议是能用 6.6 或 6.12 这类 LTS 就用 LTS早期 6.1 前面几个小版本在部分 cgroup 场景下还有一些兼容问题后来才逐步修干净。3.2 enabled 的位掩码到底怎么写如果内核支持开机后会在/sys/kernel/mm/lru_gen/下出现两个文件enabled和min_ttl_ms。先读一下cat /sys/kernel/mm/lru_gen/enabled cat /sys/kernel/mm/lru_gen/min_ttl_ms如果enabled是 0说明 MGLRU 虽然编进去了但默认没开。写 1 可以让它开始工作echo 3 /sys/kernel/mm/lru_gen/enabled注意enabled是一个位掩码。很多人在这一步会踩坑所以我把常见取值列在下面enabled 值含义0完全关闭 MGLRU1仅全局回收路径启用 MGLRU2仅 memory cgroup 回收路径启用 MGLRU3全局回收和 memory cgroup 回收都启用如果你只在裸机上调优写 1 也可以。但如果你的环境是容器、Kubernetes、cgroup 隔离的业务建议直接写 3。只写 1 的话全局 kswapd 和直接回收会走新算法但 cgroup 维度的回收仍然是传统 LRU效果会大打折扣。最后提醒一句sysfs 里的改动是运行时改动重启后回到默认。要让配置持久化可以用 systemd-tmpfiles 或开机脚本写入。比如用 tmpfiles.d 写两行w /sys/kernel/mm/lru_gen/enabled - - - - 3 w /sys/kernel/mm/lru_gen/min_ttl_ms - - - - 1000具体写法取决于你的发行版但思路是一样的开机后自动把值写进去。3.3 min_ttl_ms最小存活时间min_ttl_ms是 MGLRU 里一个很有用的旋钮单位是毫秒大多数内核默认值是 0。它控制的是在内存压力下最新的几个 generation 至少要存活多久才允许被回收。设成 0意思是“不额外保护压力来了尽管按最老代回收”。设成一个非零值比如 1000意思是“最近 1 秒内访问过或者晋升过的页面尽量先别动它们”。这个旋钮对文件页缓存特别重要。很多业务一次性读大量冷数据冷页会迅速铺满缓存如果没有时间保护很可能把刚被访问的热页挤出去。我给测试机设的起点通常是 1000ms也就是 1 秒。如果业务里热数据访问间隔非常短可以适当调大如果内存非常紧张、经常出现回收不及时就反过来调小甚至回到 0。我见过一个典型误操作为了“保护”热数据有人把min_ttl_ms设成 10000 甚至更大结果内存压力上来后内核因为要保护太多年轻代能回收的老代数量不够直接回收路径被频繁触发反而造成更高的 CPU 和延迟。这个值不是越大越好它是在“保护热页”和“及时释放内存”之间找平衡。建议每次只按 2 到 3 倍幅度调整观察至少半小时再动下一步。3.4 用计数器判断它是否生效打开之后不要只盯着free -h看。MGLRU 改变的是回收质量不是空闲内存总量。我建议看这几个指标watch -n 1 grep -E pgscan|pgsteal|pgmajfault|pswpin|pswpout /proc/vmstatpgscan和pgsteal每轮回收里扫描和回收的页面数。如果pgscan很高但pgsteal很低说明回收器在扫描时浪费了大量时间在“不该回收的页”上。pgmajfault主缺页中断次数。这个指标升高通常意味着工作集没被内核正确识别热页被挤出去又反复读回来。pswpin和pswpout换入换出次数。如果业务重匿名内存这个指标能反映 swap 抖动。再配合一个更直观的压力指标cat /proc/pressure/memoryPSI 里的some和full表示任务因为内存回收而等待的比例。开启 MGLRU 后如果full的均值在下行说明热页被保护得更好了。不要只看一两次采样就下结论至少稳定跑 20 到 30 分钟再看平均值和分位数。4. 一次压测里的前后对照MGLRU 的收益和边界4.1 测试场景与操作流程我在测试机上做过一次比较粗的对照实验场景不复杂但能说明问题。测试机是一台 64G 内存的物理机内核 6.6 LTScgroup v2。负载是模拟缓存型服务内存上限压到 48G数据文件靠页缓存承载请求里大约 80% 是冷读、20% 是热读并发开到 40 个线程持续压 30 分钟。操作流程分两步。第一步保持 MGLRU 默认关闭记录传统 LRU 下的各项指标。第二步在同样负载下执行echo 3 /sys/kernel/mm/lru_gen/enabled echo 1000 /sys/kernel/mm/lru_gen/min_ttl_ms然后继续跑 30 分钟记录同一组指标。为了避免人为清缓存影响基线我没有在中间用 drop_caches 之类的手段只是让两个时段各自独立跑。4.2 数据对比主要改善的是尾部延迟两个时段的统计结果我整理成了这张表。先说清楚这只是我一个负载下的相对变化不是让你直接拿去当预期值但方向是明确的指标传统 LRUMGLRU变化内存压力 full 均值43%19%明显下降主缺页中断次数次/秒7812显著减少冷读 P95 延迟118ms62ms降低约一半kswapd CPU 占用35%18%下降最明显的变化不是 free 内存变多了而是pgmajfault掉下来一大截。这说明以前有一批“看起来不太冷”的页面被传统 LRU 错误淘汰了导致每次都要从磁盘读回来MGLRU 通过 generation 和 PTE 扫描把它们保护住以后磁盘回读大幅减少尾部延迟自然也下来了。另一个值得注意的点是 kswapd CPU。传统 LRU 为了找页面会反复扫长链表MGLRU 按代回收后扫描范围更聚焦CPU 占用降低。这在大内存机器上体感尤其明显因为链表越长传统 LRU 的扫描成本越高。4.3 它不做什么几个边界场景MGLRU 不是万能的。第一个常见误区是以为开了 MGLRU 就不用加 swap 或者不用调 swappiness。如果业务里的匿名页工作集本身大于物理内存那么 MGLRU 能做的只是让回收顺序更合理但该 swap 还是得 swap。这时候vm.swappiness以及 swap 设备本身的速度依然决定体验。第二个边界是MGLRU 优化的是“回收时选谁”这件事不负责“什么时候开始回收”。如果系统里的 water mark 设置太低kswapd 醒来得太晚大量内存只能靠直接回收来凑那延迟依然会高。遇到这种场景要去看vm.watermark_scale_factor、vm.min_free_kbytes这些参数跟 MGLRU 是两件事。第三个边界是MGLRU 对 unevictable、hugetlb 以及驱动自身管理的一些特殊页面并没有魔法效果。它们本来就不走常规 LRU 回收路径MGLRU 影响不到。这些页面占比通常不高但如果你业务里大量使用大页并出现回收问题别指望光靠 MGLRU 能解决。5. 问题速查与调参避坑5.1 问题速查表把我在群里和实际测试里见过的问题整理成了一张速查表遇到现象可以直接对着查现象可能原因处理方式/sys/kernel/mm/lru_gen不存在内核版本太低或没编 CONFIG_LRU_GEN换成 6.6 LTS 内核并重新确认配置写了 enabled 但 cgroup 里没有变化只写了 1或者还在 cgroup v1写 3 覆盖 cgroup 回收迁移到 cgroup v2内存压力缓解但 kswapd CPU 明显变高PTE 扫描太深或 min_ttl_ms 太小适当调大 min_ttl_ms观察 kswapd 占用swap 暴涨匿名页工作集大或 swappiness 偏高调低 swappiness优化业务里的内存占用仍有大量直接回收watermark 设置过低或 min_ttl_ms 过大检查 watermark_scale_factor适当调小 min_ttl_ms开机后配置丢失sysfs 是运行时参数用 tmpfiles.d 或开机脚本做持久化打开后早期版本有兼容问题内核太旧修复没 backport优先使用较新的 LTS 内核5.2 我踩过的几个坑第一个坑是只开了全局回收没开 cgroup 回收。当时我在一个容器环境里测试enabled写了 1裸机上跑文件缓存负载效果很明显但进到容器里发现 behavior 和之前没区别。后来才反应过来enabled是位掩码1 只管全局回收容器里的回收走的是 cgroup 路径。改成 3 之后效果才一致。第二个坑是min_ttl_ms拉得太大。第一次接触这个参数时我为了“把热数据保护得更狠”直接设了 10000ms也就是 10 秒。结果在内存压力很大的时候由于最新几代被保护得太厉害可回收的页面池不够大直接回收路径反而频繁触发业务延迟比不开 MGLRU 还差。后面从 1000ms 开始再根据pgmajfault和 PSI 微调才找到合适的值。第三个坑是测试时误用 drop_caches 清缓存。为了制造“干净基线”我在压测前把所有页缓存清掉结果相当于人为把冷热数据全打回磁盘后续一段时间的所有 read 都被放大测试数据没法对比。后来我改用 cgroup v2 自带的 memory.reclaim 做小范围回收echo 512M /sys/fs/cgroup/你的控制组/memory.reclaim这样可以在不粗暴清空全系统缓存的前提下手动触发一小部分回收用来验证“热页会不会被错误淘汰”比 drop_caches 干净得多。这个接口不是 MGLRU 专属但配合 MGLRU 做实验非常顺手。5.3 推荐的调优顺序最后给一条我自己用的调参顺序不一定适合所有场景但能避免一上来就把参数改乱。第一步先把内核升到带 MGLRU 的 LTS 版本比如 6.6 或 6.12优先用发行版维护的内核。第二步确认CONFIG_LRU_GEN之后先写echo 1开全局回收跑一段时间看裸机上的pgmajfault和 PSI。第三步确定没问题后再写echo 3把 cgroup 回收也覆盖到。第四步min_ttl_ms从 0 或 1000 起步每次调整不超过 2 到 3 倍观察至少半小时。第五步每次只改一个参数不要同时把 swappiness、watermark、min_ttl_ms 全部改掉否则出问题你都不知道是谁干的。还有一个小提醒开启 MGLRU 之后监控里的free可用内存不一定立刻变多因为热页被保护住后冷页释放的节奏会变化内存占用形态和之前不同。不要拿“空闲内存必须上涨”当作成功标准更值得看的是pgmajfault、PSI、业务延迟分位数这三个指标。我自己的体会是MGLRU 更像是在给内核回收器换了一套更聪明的“选人逻辑”而不是把内存回收这条路彻底重新铺了一遍。它解决了传统 LRU 在年龄粒度、扫描效率和 refault 反馈上的几个老毛病但真正决定内存体验的依然是业务的工作集大小、swap 配置和整体内存水位。把 MGLRU 打开只是第一步后续持续观察 PSI 和缺页中断才能把参数调到跟自己业务匹配。如果哪天遇到了“内存看着够用但延迟莫名变高”的问题先从这几个计数器看起大概率能找到方向。
RELATED

相关推荐

音视频技术如何成为智能化基座:全行业落地与工程化实践

音视频技术如何成为智能化基座:全行业落地与工程化实践

这两年我越来越强烈地感觉到,音视频技术正在换一种身份出现在行业里——它不再只是直播、通话、监控背后的“管道”,而是越来越多智能化系统的“感官层”。所谓“视程空间(VisionSpace)”,在我理解里就是把这个趋势产品…

📅 2026/10/22 13:31:26
淘宝TOP接口接入SDK:登录态管理与反风控实践

淘宝TOP接口接入SDK:登录态管理与反风控实践

简介:这是一套面向Python开发者与电商数据工程师的淘宝系平台自动化采集工具包,聚焦于淘宝开放平台(TOP)、淘宝、天猫及阿里巴巴网站的商品数据抓取与用户行为分析,适用于价格监控、竞品研究、市场趋势挖掘等实战场景。…

📅 2026/10/22 13:31:26
面试完第二天就记不清了?7款面试复盘录音转写工具实测打分与选型清单

面试完第二天就记不清了?7款面试复盘录音转写工具实测打分与选型清单

走出面试间那一刻,记忆其实就开始打折了。面试官问了什么、你原话怎么答的、哪句话被连着追问了三轮,等到晚上坐下来复盘,多半只剩一句“感觉还行,但好像有两个问题没答好”。这不是记性的问题,是素材的问题。面试是典…

📅 2026/10/22 13:31:26
MORE NEWS

更多资讯

📰

企业AI转型为何白花钱?隐性成本与ROI失真真相

1. 这不是技术问题,是成本结构错配的警报 “别被云端AI割韭菜了:90%企业的AI转型都在白花钱”——这句话刚在某行业闭门会上被抛出来时,台下三十多位来自制造、零售、物流企业的技术负责人,有三分之一当场放下咖啡杯,身…

📰

制造业EDI巴别塔困境:一套接口实现全球对接的破解之道

做了快十年的制造业数字化,我见过太多企业被“EDI对接”四个字折腾得够呛。采购方发来一份几百页的报文规格书,要求你用EDIFACT走AS2传订单;另一家客户用的是X12,走的是OFTP;还有个海外客户要求SFTP半夜传发货通知。IT…

📰

Flutter for OpenHarmony实战:任务列表模块从数据模型到真机调试

某社团的负责人找到我,说想给社团做一个内部管理工具,最扎心的一句话是:“现在派任务还在微信群接龙,谁做了、做到哪一步、什么时候截止,都得翻聊天记录。”于是我用 Flutter for OpenHarmony 做了这套社团管理App&…

📰

NXOpen一键移除参数工具开发:原理、C#实现与避坑指南

1. 从两条求助消息说起:去除参数到底在解决什么做NX二次开发的朋友应该都有过这种经历:模型参数化好好的,但到了出图、转格式或者发给下游的时候,却被“为什么带着这么多特征参数”折磨。有人选择手动选中一堆特征再右键执行Remov…

📰

毫米波信道估计为何首选OMP算法?稀疏性与工程鲁棒性解析

简介:本资源是一套面向通信工程专业高年级本科生及研究生的毫米波信道估计算法实践代码包,聚焦5G/6G系统中因高频路径损耗大、导频开销高导致的传统信道估计失效问题,提供基于压缩感知理论的轻量化解决方案。包内共8个MATLAB源文件&#xff0…

📰

AI Infra全景笔记:从GPU算力到分布式训练与推理的工程体系

不夸张地讲,AI Infra 这个短语,过去两年几乎占据了我所有技术浏览记录里的大半壁江山。但说实话,这词儿看起来唬人,拆开看就是个大杂烩:从机房里的 GPU 裸金属到上层跑模型的推理服务,中间隔着的散热、供电…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬