[译] jemalloc归档事后剖析

51 阅读10分钟

前言

今年(2025年)7月2日,大名鼎鼎的C/C++内存管理器jemalloc在github上变成了归档状态。所谓归档状态即其git仓库停止更新。 事后,jemalloc最初的缔造者Jason Evans发表了回忆文章,细数jemalloc二十年的历史,以下是原文的译文

原文

jemalloc内存分配器最初构思于 2004 年初,至今已公开使用约 20 年。得益于开源软件许可的特性,jemalloc 将无限期地保持公开可用。但其上游的积极开发工作已经结束。本文将简要介绍 jemalloc 的发展历程,重点介绍每个阶段的成功与失败,并进行一些回顾性评论。

第0阶段:Lyken

2004年,我开始着手开发Lyken编程语言,用于科学计算。Lyken最终走向了死胡同,但其手动内存分配器在2005年5月已经基本完成。(原本计划利用其特性的垃圾回收器从未完成)2005年9月,我开始将该分配器集成到FreeBSD中,并在2006年3月将其从Lyken中移除,转而使用对系统分配器功能的轻量级封装。

为什么要在投入了这么多精力之后,又从 Lyken 中移除内存分配器呢?原因在于,当分配器集成到 FreeBSD 之后,人们发现系统分配器唯一缺少的功能是跟踪分配量以便触发线程垃圾回收的机制。而这可以通过使用线程特定数据和 dlsym(3) 的轻量级封装来实现。有趣的是,多年后,jemalloc 甚至添加了 Lyken 所需要的统计信息收集功能。

第一阶段:FreeBSD

早在 2005 年,向多处理器计算机的过渡就已经开始。FreeBSD 拥有 Poul-Henning Kamp 出色的 phkmalloc 内存分配器,但该分配器不支持并行线程执行。Lyken 的分配器似乎是一个显而易见的扩展性改进方案,在朋友和同事的鼓励下,我集成了后来被称为 jemalloc 的分配器。然而,事情并没有那么简单!集成后不久,jemalloc 在某些负载下,特别是KDE 应用程序带来的负载下,出现了严重的内存碎片问题。正当我以为一切即将完成时,这个实际应用中的失败让我开始质疑 jemalloc 的可行性。

简而言之,碎片化问题源于使用了统一的内存分配方式(即没有按大小分类)。我最初是从 Doug Lea 的 dlmalloc 函数中汲取灵感,但缺少那些经过实战检验的、能够避免许多严重碎片化问题的启发式方法。随后,我进行了大量的研究和实验。最终,当 jemalloc 被纳入 FreeBSD 发行版时,其布局算法已经完全改变,采用了按大小划分的区域,正如2006 年 BSDCan 会议上发表的 jemalloc 论文中所述。

第一阶段 1.5:火狐浏览器

2007 年 11 月,Mozilla Firefox 3 即将发布,但内存碎片化问题依然悬而未决,尤其是在 Microsoft Windows 系统上。由此,我们开始了与 Mozilla 合作开发内存分配的一年。将 jemalloc 移植到 Linux 平台轻而易举,但移植到 Windows 平台则截然不同。jemalloc 的官方源代码位于 FreeBSD libc 库中,因此我们实际上是 fork 了 jemalloc,并添加了可移植性代码,同时将所有与 FreeBSD 相关的代码合并到上游。整个实现仍然位于一个文件中,这降低了 fork 维护的难度,但在开发过程中的某个阶段,实现的复杂度确实超出了单个文件的合理范围。

多年后,Mozilla 的开发者们对上游 jemalloc 做出了重大贡献,试图摆脱他们自己的分支版本。然而,Mozilla 的基准测试始终表明,分支版本的性能优于上游版本。我不知道这是由于过度拟合到局部最优解,还是性能确实出现了退步,但这仍然是我对 jemalloc 最失望的事情之一。

第二阶段:Facebook

2009 年我刚加入 Facebook 时,惊讶地发现 jemalloc 在 Facebook 基础设施中普及的最大障碍竟然是内存检测。关键的内部服务面临着一个尴尬的局面:它们依赖 jemalloc 来控制内存碎片,但工程师们却需要使用tcmalloc和gperftools 中的堆分析工具 pprof 来调试内存泄漏。兼容 pprof 的堆分析功能正是 jemalloc 1.0.0 版本的主要更新内容。

jemalloc 的开发迁移到了GitHub,并在接下来的几年里断断续续地持续进行,以应对出现的问题和机遇。其他开发者也开始贡献重要的功能。3.0.0 版本引入了完善的测试基础设施以及 Valgrind支持。4.x 系列版本引入了基于衰减的清除(purging)机制和 JSON格式的遥测数据(Telemetry)。5.x 系列版本从“数据块(chunks)”过渡到“扩展(extents)”,为更好地与 2 MiB 的超大页面进行交互铺平了道路。

更具争议的是,我在 5.0.0 版本中移除了对 Valgrind 的支持,因为它维护起来非常复杂(涉及许多隐蔽的组件),而且在 Facebook 内部也几乎没人用它;其他工具,比如pprof和MemorySanitizer占据了主导地位。我当时几乎没有收到关于 Valgrind 支持的反馈,所以就推断它没有被使用。现在看来,事实并非如此。特别是Rust语言直接将 jemalloc 集成到了编译后的程序中,而且我认为 Rust 开发者和 Valgrind 开发者之间存在一些重叠。这引起了用户的不满。jemalloc 被从 Rust 二进制文件中移除的时间可能比正常的开发进程要早得多。

Facebook 的内部遥测数据令人叹为观止,能够利用来自众多服务的性能数据来指导内存分配器的开发,无疑是一大福音。过去十年中速度最快的两个内存分配器(tcmalloc 和 jemalloc)都受益于这些数据,这绝非偶然。即使是像快速路径优化这样“简单”的事情,在掌握了聚合的 Linux perf数据后也更容易做好。像避免碎片化这样更复杂的问题依然存在,但如果成千上万个不同的工作流程运行良好,没有出现异常的性能下降,那么进行更改很可能是安全的。jemalloc 因其与 Facebook 基础设施的紧密集成而受益匪浅,在性能、弹性和行为一致性方面都得到了显著提升。此外,jemalloc 自身集成的统计报告功能正是在这种无处不在的遥测环境下应运而生的。事实证明,它对 jemalloc 的开发以及非 Facebook 应用程序的调优/调试带来的益处远远超过了所需的实现工作量。

在 Facebook 的最后一年,我被鼓励组建一个小型 jemalloc 团队,以便我们能够处理一些原本难以完成的重大任务。除了显著的性能提升之外,我们还实现了持续集成测试和全面的遥测功能。2017 年我离开 Facebook 后,jemalloc 团队在我的同事 Qi Wang 的领导下,继续出色地完成了数年的开发和维护工作,几乎完全没有我的参与。从提交记录可以看出,许多其他成员也做出了卓越的贡献。

第三阶段:Meta阶段

在 Facebook 更名为 Meta 前后,jemalloc 的开发性质发生了显著变化。Facebook 的基础设施工程部门减少了对核心技术的投入,转而更加注重投资回报率。这一点在 jemalloc 的提交历史中显而易见。特别是,早在 2016 年,原则性大页面分配 (HPA) 的种子就已经埋下!HPA 的开发工作持续了几年,之后逐渐放缓,最终停滞不前,因为各种调整层层叠加,却缺乏必要的重构来维护代码库的健康。最近,这一特性的发展轨迹彻底崩盘。由于我已经多年没有密切参与其中,所以这种惋惜之情有所减轻,但由于 Meta 近期的变动,我们现在没有专人负责 jemalloc 的长期开发,也没有人着眼于其通用性。

我不想过多纠缠于这些戏剧性事件,但或许值得一提的是,尽管大多数相关人员都出于善意,但jemalloc最终还是落入了Facebook/Meta手中,结局令人唏嘘。企业文化会随着内外压力而发生转变。人们常常发现自己身处进退两难的境地,主要选择只有三种:

  1. 在极端压力下做出糟糕的决定;
  2. 在极端压力下屈服;
  3. 被抛弃。

作为个体,我们有时拥有足够的影响力来延缓组织的衰落,甚至可能促成个别组织的复兴,但我们谁也无法阻止不可避免的结局。

我仍然非常感谢我的前同事们在 jemalloc 项目上所做的出色工作,以及 Facebook/Meta 长期以来投入的大量资金。

第四阶段:停滞期

接下来怎么办?就我而言,jemalloc 的“上游”开发已经结束了。Meta 的需求与外部用户的需求早已不再契合,它们各自独立发展才是更好的选择。如果我要重新参与,第一步至少需要数百小时的重构工作来偿还累积的技术债务。而且,我对后续的工作内容并不抱有足够的兴趣,因此不愿投入如此高昂的前期成本。或许其他人会基于现有dev分支或 5.3.0 版本(该版本已经发布三年了!)创建可行的分支。

在上述章节中,我提到了一些特定阶段的失败,但尽管我的职业生涯专注于开源开发,仍然有一些普遍性的失败让我感到惊讶。

正如前文所述,移除 Valgrind 引发了一些不满。但问题的根源在于对外部用途和需求缺乏了解。如果我知道 Valgrind 对某些人来说很重要,我或许会与他人合作,努力保留它。再举个例子,我可能在长达两年的时间里完全不知道 Android使用了 jemalloc 作为内存分配器。几年后,直到事后才知道它已被替代。

尽管 jemalloc 的开发完全公开透明(并非 Facebook 内部封闭),但该项目始终未能发展壮大,吸引其他组织的主要贡献者。Mike Hommey 领导的 Mozilla 将 Firefox 迁移到上游 jemalloc 的努力几乎失败。其他人尝试过渡到基于CMake 的构建系统的努力也多次受阻,最终未能完成。我从 Darwin的惨痛经历中了解到,内部封闭的开源项目无法蓬勃发展(HHVM就是一个重现的例子),但 jemalloc 要想作为一个独立项目蓬勃发展,仅仅依靠开放开发是不够的。

jemalloc 对我来说是一个意想不到的尝试,因为 25 年来我一直坚定地支持垃圾回收机制,而不是手动内存管理。我个人很高兴能再次参与到垃圾回收系统的开发中,但 jemalloc 本身也是一个极具成就感的项目。感谢所有让这个项目如此精彩的人,包括合作者、支持者和用户。