前不久,微软在 Linux 基金会董事会的代表 Sarah Novotny 认为,由纯文本电邮讨论推动的 Linux 内核开发需要被更好的或替代协作工具取代,以降低门槛引入新的贡献者,维护和维持未来的 Linux。她认为替代工具可以是基于文本的、基于电邮的补丁系统,某种程度上是过去五到十年成长起来的开发者所熟悉的工具。此前 Linus 曾在接受采访时表示很难找到新的 Linux 内核维护者。
Linux 内核的工作方式为什么不能与 GitHub 相匹配?本文作者深入分析了背后的原因。以下为正文。(需要说明的是,本文虽为一篇旧文,但今天看来仍非常有价值。)
前不久,我跟几位出色的项目维护者进行了交流,探讨如何对大型开源项目进行规模扩展,以及 Github 如何强制要求项目采用特定的扩展方式。这里要多提一句,很多习惯于在 GitHub 上托管项目的开发者可能并不了解,其实 Linux 内核的维护模式完全不同。在本文中,咱们就具体看看 Linux 内核的工作方式、与 GitHub 的区别以及这种区别的产生原因。
而讨论这些问题的另一个重要动机,源自我在《维护者不扩展》演讲中发起的讨论,其中认同度最高的问题就是,“……这些老顽固为什么不愿意用现代开发工具?”虽然一部分顶级内核维护者仍然在大力支持邮件列表与 github pull request 等传统方法,但项目中负责图形子系统的贡献者确实更喜欢现代工具,毕竟脚本编写难度更低。问题在于,GitHub 并不支持 Linux 内核所采取的贡献者扩展方式,所以我们没办法简单迁移——甚至连迁移少部分子系统都做不到。当然,托管是 git 数据是没问题的,最大的困难在于 GitHub 上 pull request、issue 以及 fork 的实现方式。
Git 很棒,因为每个人都能够轻松在上面分叉、创建分支以及修改代码。其中的优势也显而易见,为主 repo 创建一项 pull request,然后进行审查、测试与合并。GitHub 同样非常强大,它提供一套 UI,能够让这些复杂的操作变得易于学习、易于上手,并借此大大降低了项目贡献的技术门槛。
但最终总会有一些项目取得巨大的成功,但标记、标签、排序、bot-herding 以及自动化机制的缺失,导致现有托管平台无法满足 repo 在处理大量 pull request 及 issue 方面的需要。为此,必须将项目拆分为更易于管理的形式。更重要的是,根据项目规模与诞生时间的不同,各个部分还需要配合不同的规则与流程:新的实验性 repo 与主体代码之间往往具有不同的稳定性与 CI 规则。另外,我们还可能面对一大堆废弃的插件,早已不受支持但又不能贸然删除。具体来讲,我们需要将庞大的项目拆分成多个子项目,保证每个子项目都拥有自己的流程与运作风格,同时分别纳入独立的标准、pull request 以及 issue 实现风格。光是这项重组工作本身,可能就需要几十甚至上百位全职贡献者的不懈努力……而这,真的有必要吗?
几乎所有托管在 GitHub 上的项目,都需要将其 monorepo 源代码树拆分成多个不同的项目,借此维持正常运转。而各个项目都拥有其独特的功能集。分散化的结果,就是同一个项目中包含多个核心,外加成堆的插件、库以及扩展。所有这些都依靠着某种插件或者软件包管理器被捆绑在一起,在必要时直接从 GitHub repo 中提取内容。
目前几乎所有大型项目遵循的都是这样的治理结构,所以咱们就没必要再赘述这种方式的好处了。相反,我觉得应该强调一下这么做引发的问题:
当然,相当一部分工作其实不需要那么麻烦,也有很多项目都可以轻松完成变更。但无论如何,具体操作都要比对单一 monorepo 直接执行 pull request 要麻烦得多。因此,项目贡献者将倾向于不频繁执行非常简单的重构(例如仅共享一项新功能),这会在一段时间内快速积累起大量提交库存。当然,我们可以使用面向单一函数的 node.js 重构方式,但这相当于是把源代码控制系统由 git 替换成了 npm——npm 也不怎么样,实话实说。
Linux 内核项目,是我所了解的少数几个没有进行过此类拆分的大型项目。在深入探讨 Linux 内核项目的维护方式之前,我们首先需要明确一点——内核开发是一项规模极大的工作,不可能在缺少子项目结构的情况下运行。这也让我不禁想到,git 为什么要采用 pull request 这种结构设计:在 GitHub 上,pull request 可以说是贡献者提交开发成果乃至合并更改的唯一认证途径。但在内核项目这边,即使已经广泛引入了 git,大家仍然习惯将变更以补丁的形式通过邮件列表进行发送。
但事实上,git 从第一个版本开始就在支持 pull request。初始版本确实相当粗糙,而其受众则主要是内核维护者,那时候 git 的诞生,完全是为了解决 Linus Torvalds 维护者团队所面临的实际问题。虽然 git 很好也很实用,但并不适用于单一贡献者:即使在今天,甚至可预见的未来,pull request 仍然主要用于转发面向整个子系统的变更,或是在不同代码之间同步代码重构、乃至以类似的跨领域方式更改子项目。例如,由 Linus 提交的 Dave S. Miller 的 4.12 网络 pull request 就包含来自 600 位贡献者的超过 2000 项提交,外加一大堆来自下游维护者的合并请求。但是,几乎所有补丁程序都由维护者们从邮件列表中获取,而非由补丁作者自行提交。也正是这种将补丁程序提交至内核共享 repo 的实际操作、往往并非由补丁作者本人执行的实际情况,才让 git 在设计层面特别强调分别跟踪提交者与提交作者。
而 GitHub 最大的创新与改进,就是对所有内容都使用 pull request,包括各项个人贡献。但这很明显已经与 git 的最初诞生诉求有所区别。
乍看之下,Linux 内核很像是那种 monorepo,所有东西都被收纳在 Linux 的主 repo 当中。但实际情况真这么简单吗?当然不是:
从概念层面看,这种方法似乎太过复杂,导致每个人的磁盘里都塞上不少跟自己根本没什么关系的管理系统。但总体来说,这种治理结构确实具有一定优势:
这种对重构及代码共享的简化,极大减轻了陈旧技术带来的债务负担。内核中不再需要保留毫无意义的非稳定版 api 说明文档。
简而言之,我认为这是一套更严格也更强大的模式,至少其中保留了后撤至采用多个互不相干 repo 的灵活空间。甚至某些英伟达驱动程序都有自己的专用 repo,与主内核树完全不相交。这类 repo 只涉及一丁点源代码,而且出于法律原因,其中不能包含内核中的任何内容,可以算是个完美的极端案例了。
对,也不对。
乍看之下,Linux 内核确实很像是个 monorepo,因为一切尽皆囊括于其中。相信很多朋友都知道 monorepo 的问题,其规模增长到一定程度后,将再无进一步扩展的可能。但认真分析,我们会发现 Linux 内核项目跟单一 git repo 有着诸多本质区别。其不仅拥有数百个上游子系统与驱动程序 repo,着眼于整个生态系统,来自硬件供应商、各发行版乃至其他基于 Linux 的操作系统与独立产品的主要 repo 更是成千上万。这还不包括各类供个人使用的私有 git repo。
二者之间的关键区别,在于 Linux 内核虽然为所有内容提供一套作为共享命名空间的单一文件层级结构,但出于不同应用需求与关注重点,各 repo 又保持着相互独立。换言之,Linux 内核项目更像是个由众多 repo 构成的 monotree,而非 monorepo。
在深入解释 GitHub 目前为什么无法支持其工作流之前,我们首先需要挑选几个典型案例,解释其在实践运作中的具体特性。先给出结论:所有工作都需要通过维护者之间的 git pull request 完成,这就是最大的痛点。
最简单的情况就是对维护者的层级结构进行渗透式变更,直到各项变更落实在树结构当中。整个过程非常简单,因为其中的 pull request 只需要从一个 repo 转向另一个 repo,所以仅使用现有 GitHub UI 即可完成。
但跨多个子系统的变更则要复杂得多,因为后续出现的 pull request 不再以非循环图刑期睁大眼睛,而是变成了网格结构。第一步就是由所有相关子系统及其维护者对变更进行审查与测试。在 GitHub 流中,相当于同时面向多个 repo 提交 pull request,并在各请求之间共享同一条讨论流。在 Linux 内核中,这一步骤将通过一系列不同的邮件列表,将补丁提交给各维护者手中。
审核的方式也往往无法与合并方式相统一,而需要选择某一子系统作为主子系统并负责接收 pull request,且只能在其他所有维护者都表示同意后才执行路径合并。这里选定的往往是受变更影响最大的子系统,但有时候也可以是负责执行其他工作、但与当前 pull request 相冲突的子系统。有时候,如果变更会影响到整个树结构、而非明确影响少部分文件及目标,项目可能还需要建立一套全新 repo 并指定专项维护者。最近的相关实例就是 DMA 映射树,其目标在于合并以往一直分散在各驱动程序、平台维护者以及架构支持组当中的工作成果。
但有时候,可能同时存在多个既与当前变更存在冲突,又无法通过常规合并方式处理的子系统。一旦出现这种情况,我们无法直接应用补丁程序(即 GitHub 上的 rebasing pull),而需要通过单一提交将仅包含必要补丁的 pull request 推送至全部子系统,从而将其合并至所有子系统树当中。在这种情况下,我们必须建立通行基准,保证各子系统树不会因此出现不相关变更、或者说遭受污染。由于该 pull 只面向特定主题,因此这些分支通常被称为主题分支。
结合实际经历,我曾经参与一个项目,旨在添加代码以实现经由 HDMI 的音频支持功能。这部分代码需要跨越图形与声音驱动程序子系统。来自同一项 pull request 的同一批提交需要同时被合并至英特尔图形驱动程序以及音频驱动程序当中。
作为世界上规模最大的通用型操作系统项目之一,Linux 选择这种方式当然也是经过了充分考虑。Linux 内核同样采用 monotree 单一树状结构,只是此结构极度庞大,甚至需要专门的全新 GVFS 虚拟文件系统为其提供支持。
遗憾的是,GitHub 并不支持这样的工作流,至于在 GitHub UI 中不提供原生支持。虽然只需要简单的 git 工具即可完成此类操作,但之后我们还是得回到邮件列表上的补丁程序,并以手动方式通过邮件执行 pull request。在我看来,这也是 Linux 内核社区决定不向 GitHub 迁移的核心原因。总体来说,虽然也有不少顶级维护者对 GitHub 还有这样或者那样的抱怨,但这些都不是真正的关键技术问题。而且不仅仅是 Linux 内核,一般来说任何规模足够大的项目都 GitHub 上都很难顺利扩展,因为 GitHub 在设计上就限制了项目通过 monotree 进行多 repo 扩展的空间。
说到这里,我想给 GitHub 提一项简单的功能要求:
请通过单一 monotree 对多个 repo 上的 pull request 与 issue 跟踪提供支持。
思路很简单,影响却将极为深远。
首先,我们可能希望在同一组织之内为同一 repo 保留多个分叉版本。看看 git.kernel.org 就能看到,其中的大部分 repo 并不属于个人项目。即使不同的组织可能各自拥有不同的子系统,但硬性要求每个 repo 对应一个组织的作法都相当愚蠢、过度僵化,只会给用户的访问与管理带来不必要的阻碍。例如,在图形领域,我们在 GitHub 上只能为用户空间测试套件、共享用户空间库以及工具与脚本常规集分别提供一套 repo;却无法建立一套整体性的子系统 repo,外加一套专门容纳核心子系统工作 repo 以及分别面向各大型驱动程序的对应 repo。这些完全可以作为多个独立分叉存在,但 GitHub 却不支持。很明显,我们设法的方法更科学,其中每个 repo 都拥有大量分支,其中一个分支负责实现应用功能,而其他分支则可用于支持发布周期内的 bug 修正。
将所有分支整合至同一 repo 中也不可行,因为 GitHub 上 repo 拆分的目的正是将 pull request 与 issues 各自区分开来。
同样的,GitHub 应当允许用户根据实际情况建立起分叉关系。虽然现有设计对从零开始诞生在 GitHub 上的新项目来说还算友好,但 Linux 显然不能这么干——这意味着我们每次只能移动 Linux 中的一个子系统,更不用说目前 GitHub 上早已包含数量庞大且彼此冲突的 Linux repo。
我们有必要将 pull request 同时附加至多个 repo,同时保持一条统一的共享讨论流。现在,GitHub 虽然允许用户将同一 pull request 重新分配至目标 repo 的另一不同分支,但却无法实现同一 pull request 对多个不同 repo 的同时分配。事实上,这种对 pull request 进行重新分配的功能非常重要,因为新的贡献者们只会为他们认定的主 repo 创建 pull request。以此为基础,各 bot 将分别将 pull request 发送至 MAINTAINERS 文件中列出的全部 repo 中的特定文件及变更组处。在与 GitHub 项目人员的交流中,我一直建议他们直接提供这项实现。但从现状来看,只要这一功能仍然能够在各独立项目上通过脚本实现,GitHub 就不会推出真正的标准。
这方面还存在与 UI 相关的问题:对于指向不同分支的 pull request,其对应的补丁列表也可能有所区别。但这不一定就是用户的错,同一套 repo 中往往也已经合并了多项补丁。同样,不同 repo 对于 pull request 的状态也有不同的要求。一位维护者可能倾向于将当前 pull request 交由另一子系统进行处理,因此直接拒绝合并请求;而另一位维护者则决定直接合并。而另一树状结构甚至可能出于旧版本兼容性或供应商分叉版本的考虑,而直接将当前 pull request 无效化。更有趣的是,每个子系统都可能通过多项不同的合并提交而对同一 pull request 进行多次合并。
与 pull request 类似,issues 也可能同时涉及多个 repo,甚至需要进行往来转移。这里我们以 bug 为例,假定从某一发行版的内核 repo 中初次发现并上报了一项 bug。在查验之后,我们证明该 bug 归属于某驱动程序,目前处于最新的开发分支当中,且同时影响到当前 repo、上游主分支以及其他多个分支。
这里我们需要对状态进行再次拆分,因为一次向一套 repo 推送补丁的作法无法快速覆盖到全部 repo。我们甚至需要组织额外的修复方案,借此处理较为陈旧的内核或发行版,包括将一部分已经没有修复必要的 repo 以 WONTFIX 的形式关闭,并在相应子系统 repo 中将其标记为“已成功解决”。
Linux 内核不会登陆 GitHub。但真正重要的是,GitHub 应该学习 Linux 内核项目采取的 monotree 架构思路,此举也将给目前 GitHub 之上的各类大型项目带来显著收益。在我看来,这种架构层面的转换,将为整个 GitHub 带来一种强大且独特的扩展能力与灵活空间。
领取专属 10元无门槛券
私享最新 技术干货