在容器化大规模部署的今天,运维工程师经常会遇到一个诡异的现象:
登录一台 256G 内存的物理机,free -m 显示可用内存所剩无几,但用通过 meminfo 信息怎么加都不超过几十 GB,剩下数百 GB 不知所踪。这是我 2019 年遇到的真实的问题。
不是进程泄漏,也不是内核 Bug。问题的根源,藏在一个绝大多数人没留意过的角落:Dying memory cgroup。这是 Linux 内核社区持续追踪了五年的 Dying memory cgroup 问题。
在容器化盛行的今天,当我们从用户视角删除 cgroup 后,但在内核视角里,删除一个 memory cgroup 并不代表它立刻消失。任何比 cgroup“活得更久”的内存对象,都可能 pin 住这个 cgroup,让它永远处于 Dying状态。最常见的就是 Page Cache。一个任务在某个 cgroup 里读入了文件页,然后任务退出,cgroup 被删除。但这些文件页还在——它们缓存着业务需要的二进制文件、配置甚至日志,甚至会被下一次启动的任务继续命中。只要这些页面仍然引用着旧 memcg,旧 memcg 就无法真正释放。随着业务版本不断发布、老容器不断销毁,这类“僵尸 cgroup”在系统中堆积如山。它们不仅白白占着内核数据结构的内存开销,更致命的是,它们会让内存回收算法变得极其低效——内核每次回收都要遍历这些早已无主的 cgroup。从 2021 年到 2026 年,我们围绕这个问题展开了长达五年的拉锯战。我们写了上百个不同版本的 Patch,推翻了多个方案,最终在 2026 年的 Linux 7.1 中,正式宣告了这场战役的终结。今天,我们就来复盘这场关于“生命周期”的终极对决。一、罪案现场(2021):问题首次被摆上台面
要理解这个问题有多棘手,先得明白内核里的一对基本关系:memory cgroup(memcg):负责统计和控制一组进程的内存使用量。容器删了,它就该被销毁。
Page Cache:文件在内存里的缓存页。进程读了一个文件,这个页就挂在进程所属的 memcg 账上。
麻烦在于:Page Cache 的生命周期,和进程的生命周期是解耦的。一个任务在某个 cgroup 里读入文件页,任务退出,cgroup 被删除;但这些文件页还会继续存在。匿名页相对简单,进程退出后通常很快被回收;但文件页更复杂,它们可以被不同的 cgroup 共享,而且可能长时间不被回收。尤其是大规模生产环境中,同一个任务会被反复重启,每次重启进入一个新的 cgroup。旧 cgroup 里的文件页还在,新任务又继续命中这些缓存。结果就是:旧 cgroup 被 page cache 死死 pin 住,永远无法释放。2021 年,Muchun Song在社区发出了第一版 RFC。他的核心思路是:让用户页改用 objcg 机制,不再直接长期 pin 住原始 memcg。这个方向并不是凭空来的。此前 slab 对象、非 slab 内核分配、per-CPU 对象等内存类型,已经逐步转向 objcg 机制——它们通过 objcg 来记账和 reparenting,不再直接持有原始 memcg 的引用。但用户页是最后一块硬骨头。问题是,用户页太核心了。把它们也切到 objcg,不只是换一个指针那么简单,而是会影响内存管理的所有热路径——分配、回收、LRU 维护、锁竞争……当时社区讨论的重点不是“这个问题存不存在”,而是“这样做语义上对不对”。有人担心:如果旧 cgroup 的页面被 reparent 到父 cgroup,父 cgroup 会不会变成垃圾场?内存控制行为会不会被改变?这时候,社区核心维护者Johannes Weiner给出了一个后来被反复引用的关键判断:“这不是改变用户语义,而是改变内核内部表示。”
他的逻辑很简单:一个 cgroup 被删除后,用户已经不再对这些页面施加独立控制。今天这些页面仍然挂在旧 memcg 上,只是因为实现上还留着这个壳。把它们 reparent 到父 memcg,并没有改变用户能观察到的任何控制语义,只是移除了一个无意义的 zombie 容器。所以 2021 年的价值很明确——方向被认真讨论,并且基本站住了。但方案还很粗糙,很多工程问题没有完全解决。二、临门一脚的滑铁卢(2022):统计问题挡住去路
到 2022 年,这个系列已经演进到 v6。它不再是早期 RFC,而是一个看起来接近合入的完整 series。当时 Andrew Morton(Linux 内核内存管理子系统 maintainer)已经表示计划把它推进 mm-stable。这一轮离成功非常非常近。但就在最后关头,Yosry Ahmed 站了出来,喊停。他指出的问题非常硬核:cgroup v1 的 non-hierarchical stats 会被破坏。这个问题不像普通 bug 可以轻轻带过。memcg 的统计既是用户接口(memory.stat),也是线上排障定位的重要依据。一个解决 dying memcg 的方案如果会破坏 memory accounting,那本身就还不合理。所以 2022 年这一轮停住了。它解决了“引用生命周期”的问题,但还没有完整解决“统计”的问题。三、走不通的岔路(2023):一次“失败”帮社区划清了边界
2023 年,Yosry Ahmed 提出了另一个方向:memory recharging for offline memcgs。这个方案想解决一个更直观的问题:如果旧 cgroup 的页面后来被新 cgroup 使用,为什么不把这部分内存重新 charge 给真正使用它的 cgroup?从用户角度看,这个想法很自然。尤其是 page cache 这种共享资源,确实经常出现“创建者已经消失,使用者还在继续使用”的情况。但社区很快发现,这其实是打开了另一个更大的潘多拉魔盒。dying memcg 要解决的是:一个已经删除、已经没有控制意义的 cgroup,不应该继续被少量页面 pin 住。而 recharging 试图解决的是:共享内存到底应该算到谁头上。这两个问题相关,但不是同一个问题。更麻烦的是,recharging 几乎不可能做到完全确定——一个 page cache 页可能被几十个容器同时映射,谁是“真正 owner”?没有唯一答案。社区经过长时间讨论,最后形成了共识:recharging 可以作为未来更复杂的 ownership 问题继续研究,但绝不应该阻塞 dying memcg 的确定性修复。这可能是整个五年历程里最有价值的一次“失败”。它帮社区划清了一条至关重要的边界:“如何让已死的 cgroup 消失”和“共享内存该归谁管”是两个独立命题。前者必须解决,后者值得重开一局。四、方向统一(2025):摆在前面的最后一道坎
到 2025 年,社区经过几年的来回拉扯,终于在核心问题上达成了统一。Muchun 这次发出的 RFC,标题已经变成了 "Eliminate Dying Memory Cgroup"。不再是试探,不再是讨论"要不要做",而是明确宣告:我们要彻底干掉这个问题。因为内核在发展。2021 年讨论方案的时候,Linux 的内存回收还是基于传统 LRU;到了 2025 年,MGLRU 已经合入社区,内存管理架构比四年前复杂得多。所以 2025 年这一版的状态很清楚:方向已经锁死,剩下的是工程层面如何适配 MGLRU 的问题。但适配本身不简单,涉及到 reparenting 过程中 LRU 代际信息怎么保持一致性、会不会引入新竞态、会不会影响回收效率——这些都需要一轮又一轮的迭代验证。问题已经被推到了终点线前,但最后这几十米,还需要有人来跑完。五、最终决战(2026):五年长跑的终点线
2026 年,Qi Zheng 基于 Muchun Song 的工作继续推进,从 v1 一直迭代到 v6。针对 MGLRU 的难题,也找到了解决方案,最终方案把用户页纳入 objcg 体系,在 memcg 删除时把相关资源干净地 reparent 到仍然存活的父 memcg。旧 memcg 不再因为少量页面引用而长期残留。也因此,Qi Zheng 成为了 MGLRU 的 Reviewer 之一。这标志着 dying memory cgroup 这个持续多年的问题,终于有了内核主线里的正式答案。六、为什么它花了五年?
五年,Linux 内核没有在等一个更完美的补丁,而是在等一个更清晰的共识。起初,大家花了一整年才确认一件事:这不是谁代码写得不好,而是对象生命周期和控制面生命周期天生就拧着——这题从设计层面就埋下了隐患。后来走过的那条弯路也有意义。2023 年的方案失败了,但它帮所有人看清了一件事:把"让死掉的 cgroup 彻底消失"和"共享内存到底算谁的"这两件事缠在一起,永远得不出答案。必须拆开。最难的不是写代码,而是所有人达成共识。一场持续五年的内核拉锯战,就此落幕。
- 2021 RFC: https://lore.kernel.org/all/20210330101531.82752-1-songmuchun@bytedance.com/
- 2021 v6: https://lore.kernel.org/all/20220621125658.64935-1-songmuchun@bytedance.com/
- 2023 RFC: https://lore.kernel.org/all/20230720070825.992023-1-yosryahmed@google.com/
- 2025 RFC: https://lore.kernel.org/all/20250415024532.26632-1-songmuchun@bytedance.com/
- 2026 v1: https://lore.kernel.org/all/cover.1761658310.git.zhengqi.arch@bytedance.com/
- 2026 v6: https://lore.kernel.org/all/cover.1772711148.git.zhengqi.arch@bytedance.com/