http://blog.ffwll.ch/2017/08/github-why-cant-host-the-kernel.html

by @智彻

按:Daniel Vetter 是来自 Intel OTC 内核开发。本文对其最近的一篇博客:Why Github can’t host the Linux Kernel Community 做了摘要和评论。

Daniel 说,这篇文章的写作动机来自两个事情:

其一是在 maintainerati 会议 (一个开源软件维护者的业内聚会) 上的讨论。 Daniel 和几个开源软件维护者讨论了如何扩展真正的大规模开源项目,以及 Github 如何强制项目只能以几种特定的方式来扩展。而 Linux 内核开发恰恰使用了不同的模型,这些正是在 Github 上搞开源的维护者不懂的,因此 Daniel 在讨论中解释 Linux 如何运作,以及 Linux 模型为何不同;

其二是在 Daniel 的另一篇博客 Maintainers Don’t Scale 的评论区里,支持最高的评论就是 “……为啥这些恐龙们不去使用现代的开发工具?“。(其实这个问题曾经在笔者心中也盘旋了很久….) 一些内æ ¸社区顶级的维护者,捍卫着传统的邮件列表及补丁的提交方式,抵制使用 Github pull request 的方式。难道真的就是这一小撮掌权者的问题?Daniel 认为不是的。根本原因是,Github 的模式根本支撑不来 Linux 内核这种有着巨大体量的贡献者的开发模式。因此,哪怕是把内核社区的几个子系统移到 Github 上开发都是不可能的。Daniel 说,这不仅和托管 git 的数据有关,而且还和 how pull request, issue 和 fork 这几个 Github 的功能的工作方式有关。(写到这里,我有点儿怀疑这种结论,Github 那么多大型项目,托管整个社区也就罢了,说连个内核子系统都搞不定,就有点儿糊弄人了吧?) [编辑] Github 扩展的方式

Git 牛的地方就是让每个人都可以很方便地 fork 开源项目,在其上创建分支。最后一但有了不错的改进,又可以基于上游主仓库创建一个 pull request,并且得到代码的 review,测试,然后被合并。Github 也很牛,因为它把这些 Git 复杂的方式都用 UI 简单化了,易学易用,让新手很容易去为开源项目做贡献。

最终,一个获得巨大成功的开源项目,没办法在一个代码仓库里,能够利用 tagging, labelling, sorting, bot-herding 和 automating 等手段管理好所有 pull request 和 issue,于是就必须通过把单一代码库拆分成多个可管理的部份组成。更重要的是,项目的各部分因为规模和成熟度的不同,需要不同的规则和流程:非常新的实验性质的库,与主线代码相比,有不同的稳定性和 CI (持续集成) 条件。并且,或许项目里还有一大堆过时的,不会被支持的代码,但是目前还不能删除:这需要把非常大的项目拆分成多个子项目,每个子项目都有自己风格的流程,合并条件,独立的仓库,独立的 pull request 和 issue 的跟踪。一般来说,直到管理的开销成为痛点,庞大的重组成为必要时,项目管理可能需要几十甚至几百全职的贡献者。

几乎所有的 Github 托管项目,解决这个问题时,都是靠把单一的源码树,按照功能区分,拆分成许多不同的项目。通常拆分的结果就是,一些核心代码,加上一堆插件,库,和扩展的代码。所有这些通过某种插件或者安装包管理器再构建到一起,在一些情况下,可能就是直接从其它的 Github 仓库里直接拉取代码构建。

因为几乎所有的大项目都是这么搞的,在这里,Daniel 不再赘述它的好处,而是主要把这种做法的缺点指出来了,这里简单摘要如下:

社区被不必要地割裂了。大多数贡献者只有他们直接贡献代码的仓库,忽略掉了项目的其它部份。这个对他们有好处,但是也会导致在不同的子项目里并行的,重复的工作被及时发现,并且共享成果。
    项目重构和代码共享存在导致低效的障碍:首先你需要为新代码变更,发布核心项目的新版本,然后过一遍所有的其它子项目的代码,然后更新它们,然后再去删除核心项目里的旧的被共享的代码。
    理论上,支持多个子项目版本组合难以为继。必须通过集成测试来保证。
        对同属一个大项目的多个子项目重组很痛苦,因为这需要重组 Git 仓库和决定如何拆分。而在一个单一的代码仓库,重组仅仅是把维护者信息文件重新更新一下。 

	(看到这里,笔者认为,如果一个内部强耦合的代码库,非要因为规模大拆分,是存在这些问题...... 可是确实,项目如果足够大,其实这种耦合关系就可以通过一些流程来规避了。例如,Linux 发行版,不就可以看作是按照上述方式拆分的多个仓库的集合么?当然,Linux 发行版本来就是很多独立开源项目组成的。但是笔者之前工作在 Solaris OS 上,其实就是把弱耦合的项目拆分成多个库了。为了规避接口耦合的问题,才在不同的仓库之间设计了接口稳定性上的约定,如公开接口,私有接口。对公开接口,要支持多版本,EOL 也会存在相应的规则。)
[编辑] 为什么要有 Pull Request?

Daniel 说,Linux 内核是他知道的几个没有像之前所说的那样拆分的项目之一。Linux 内核是一个巨型项目,没有子项目规划的话,也没办法运转。因此,看一下为什么 Git 需要 Pull Request 也是必要的:在 Github 上,Pull Request 是贡献者的代码得到合并的真正方式。但是,内核改动则是通过提交 patch 到邮件列表,甚至在 Git 被内核项目使用之后。

但在 Git 非常早期版本,Pull Request 就被支持了。最初的 Pull Request 的使用者,是内核维护者们,Git 早先就是为了解决 Linus Torvalds 的维护内核的问题的。毫无疑问,Pull Request 非常有必要,而且有用,但是,它不是为了处理独立贡献者的代码改动的:直到今天,Pull Request 被用来转发整个子系统的代码变化,或者同步代码的重构,或者横跨多个子项目之间,同步交割代码的改动。举个例子,在 4.12 网络子系统维护者 Dave S. Miller 的 Pull Request,被 Linus 提交: 这个提交里包含了两千多的,来自 600 个独立贡献者的代码提交,并且还有一堆代码合并和来自下一级维护者的 pull request。但是,这里面所有的补丁,都是从邮件列表里挑选出来,由维护者提交,而不是由原作者提交。Linux 内核流程的古怪之处就是,原作者不提交代码到代码库。这也是为啥 Git 独立地跟踪记录提交者和作者。

Github 的创新和改进就包括了,到处使用 Pull Request,Pull Request 下放给了独立贡献者。但是,这并不是 Pull Request 最早被创造出来的目的。
[编辑] Linux 内核扩展的方式

初看 Linux 就是一个单体的仓库,所有东西都被塞进 Linus 的主仓库。但是实际上相差甚远:

    用 Linus Torvalds 主仓库运行 Linux 的人很少。如果他们运行 upstream 内核,通常都是用稳定内核分支。但更多的是用 Linux 发行版,这些发行版通常会有额外的补丁和 backport,并且甚至不是 kernel.org 托管的,而通常会是完全不同的组织。或者,他们用硬件制造商那里拿到的内核,与主仓库相比,这里常常有非常多的改动。 

        除了 Linus 自己,没有人直接在 Linus 的仓库里做开发。每个子系统,甚至是大的驱动,有它们自己的 Git 代码库,及自己的邮件列表去跟踪补丁提交和问题讨论,这些子系统都是相互独立的。 

	    跨子系统的工作,是在 linux-next 的集成代码树里做的,它通常包含了上百个 Git 分支,这些分支来自不同的 Git 代码库。 

	        所有的这些都通过 MAINTAINERS 的文件和 get_maintainers.pl 的脚本来管理,它可以告诉任意给定的代码片段相关的一切,谁是维护者,谁是代码 review 者,哪里是 Git 仓库,使用哪个邮件列表,哪里去报告 bug。这个工具不仅仅是根据文件的位置,也通过捕捉代码的一些特征来确定跨子系统的改动,例如,设备树的处理,kobject 的层级结构,都会被合适的专家处理。 

		Daniel 认为,这种方式有有如下好处 (自然是和前面 Github 上的子项目拆分相比的),摘要如下:

		    重新组织子项目的拆分超级得容易,只需要更新 MAINTAINERS 文件,随后创建新的 Git 仓库就可以了。
		        跨子系统的 Pull Request 讨论和问题的讨论,非常非常容易在子项目之间重新分派,只需在邮件回复里增加要 Cc: 的子系统的邮件列表。类似地,做跨子系统的工作,也会非常容易的协同,因为同一个 Pull Request 可以被提交到多个子项目,并且只有一个全局的讨论。
			    跨子系统的工作,不需要多项目之间任何发布的协同。直接在自己的仓库里修改全部代码即可。
			        它也不妨碍你创建自己的实验性改动,这是多仓库重要好处之一。直接在自己的 fork 里增加代码就好,没有人强迫你把代码改回去,或者把你的代码放到单一的仓库,因为没有中心仓库。 

				(说了这么多,Github 唯一的问题就是不支持跨仓库的工作流呗....)
[编辑] Linux 的模式: monotree with multiple repositories

有人可能会质疑,Linux 的模式看起来像是一个,本文开头说过的,有扩展问题的单体代码库。接下来,Daniel 在文章中,花了很多篇幅解释了,Linux 其实是 ** 单体代码树,多个代码仓库 (monotree with multiple repositories) ** 的模式。

Daniel 还以内核社区的维护者们之间的 Pull Request 工作流为例,说明 Linux 这种模式在 Github 里为啥不行,摘要如下:

简单的场景就是在内核维护者的层级结构中扩散代码改动,直到改动最终落地到了可以最终做软件发布的代码树里。对 Github 来说,这个很容易,直接用 Pull Request UI 就可以做到。

更有意思的是,跨越多个子系统的改动,因为 Pull Request 的工作流会成为一个无环图及其变体组成的 Mesh 网格。第一步是让代码改动都被所有子系统的维护者 Review 和测试。在 Github 工作流里,这个将是一个 Pull Request 同时提交到多个代码库,只要有一个单一的讨论线索,并在所有的仓库里共享。在内核社区里,这个是靠补丁同时提交给一大堆邮件列表和维护者来实现的。

内核这种 Review 方式,通常还不是代码最终合并的方式。而是在所有其它子系统维护者同意的情况下,多个子系统其中的一个被确定为接受 Pull Request 的方式。通常,被选的子系统是受到改动影响最多的子系统,但是有时候,也可以是因为某个子系统正在做的工作与这个 Pull Request 有冲突。有时候,还可以是一个全新的代码仓库和其维护者被确立。这通常发生在改动的功能跨越了整个代码树,不是很容易地在一个地方,一个目录下的被一些文件囊括的情形。最新的例子就是 DMA mapping tree (DMA 映射项目的代码树),该项目试图将各种驱动,平台维护者,还有处理器架构支持的改动都在一个项目里完成。

但是,有时候,多个子系统的代码改动存在冲突的情况,导致它们都需要不小的努力去解决合并冲突。在这种情况下,这种跨子系统的补丁,通常不能直接被接受 (相当于一个 Github 上的 rebase 的 Pull Request),而是 Pull Request 经过修改,使这些补丁对所有子系统都通用,然后再合并到所有子系统。为了避免无关的改动影响到某个子系统,共同的基线是非常重要的。由于这种 Pull Request 是为了一个特定的 Topic 而存在的,这些代码分支也通常被叫做 Topic 分支。

Daniel 还举了微软公司的 OS 项目的例子,说它是一个单体代码树。并且根据他和微软的人交流,这个代码树大到需要微软写一个 GVFS 的虚拟文件系统来支持更高效的开发。(笔者认为,这个例子略有些不恰当,微软的 OS 不光有内核,还有用户态的许多其它代码,而这里的内核代码树就是内核代码,从更全局和更平等的 Linux 发行版的角度看,Linux 发行版其实是多个代码树了........ 恰恰是 Daniel 在前面抨击 Github 上用多代码树划分子项目的方法......)
[编辑] 亲爱的 Github,轮到你了...

很不幸的是,Github 不能支持前面讨论的跨子系统的工作流,至少在原生的 UI 是不能的。当然,这些工作可以用原始的 git 命令完成,但是不得不回到通过邮件列表发 patch 的方式,Pull Request 也得回到邮件,然后手工合并。在 Daniel 看来,这是唯一的原因,为什么 kernel 社区不能搬到 github 上。也还有一些小问题 (真的是小问题么?),一些顶级的维护者对 Github 极度地反对,但这个不是一个技术问题。并且,不只是 Linux 内核,它是所有巨型的 Github 项目要扩展所面对的一般问题,因为 Github 没有给他们一个扩展到多个仓库,但是保持一个单体代码树的选择。

简而言之,Daniel 提出了一个他认为简单的新特性需求给 Github:

   请支持 Pull Request 和 issue 跟踪管理跨多个不同的,基于单体代码树的代码仓库。

   非常简单的想法,但是有巨大的影响。

   Daniel 不但给出了核心想法,还给出了一些建议的细节,摘要如下:
   [编辑] 仓库和组织

   首先,需要支持同一个组织可以拥有同一个代码库的多个派生仓库。以 git.kernel.org 为例,上面大多数代码库不是个人的。

   在一个代码库里使用多个分支不能替代这个需求,因为之所以要拆分代码库,主要就是想让 Pull Request 和 issue 彼此隔离。

   还有就是,需要可以基于既成事实(历史)去建立代码库派生关系。对新项目来说,这个不是问题。但以 Linux 搬迁为例,这是个问题:一次要把 Linux 所有子系统都搬过来,并且,Github 上已经存在大量的 Linux 代码库,彼此间没有正确的 Github 派生关系。
   [编辑] Pull Request

   Pull Request 需要能同时提交到多个代码库,但是还可以保一个讨论线索。一次提交给所有代码库以外,还要能够重新指派 Pull Request 到某个代码库的不同分支。

   并且,Pull Request 的状态需要因每个代码库而不同。一个代码库的维护者或许关闭了这个 Pull Request 而不去合并,因为维护者们一致同意其中一个子系统拉取这个 Pull Request,而那个维护者将会合并和关闭 Pull Request。另一个代码树或许要按照无效的请求去关闭这个 Pull Request,因为这个 Pull Request 不适合这个旧版本的代码库,或者某个特定厂商的派生代码库。甚至更有意思的是,一个 Pull Request 或许会被合并很多次,在每个子系统的代码库里,且使用不同的合并提交 (Commit ID)。
   [编辑] Issues

   和 Pull Request 类似,问题跟踪也需要按照多个代码库隔离,且需要有移动的能力。一个例子就是,一个 bug 可能先报告给了一个发行版的内核代码仓库,然后经过 bug 分析,这个驱动的 bug 也在最新的开发分支存在,因此这个 issue 不但和这个代码库有关,还需加上主线 upstream 分支,或许更多的代码仓库。

   Issue 状态应该在不同代码库独立,因为一次在一个仓库的 push,不会理解在所有的仓库可用。甚至,backport 到老内核和发行版,可能需要更多的工作,而一些代码库可以决定不值得修这个 bug,以 WONTFIX 关闭这个 bug,但是同时在其它代码库里它标识为成功解决。(笔者之前的公司,这些功能都有啊。的确这是一个商业软件的缺陷跟踪系统必须有的功能)
[编辑] 总结: 单体代码树,不是单体代码库 (Monotree, not Monorepo)

Linux 内核没打算搬到 Github 上去。但是在 Github 上支持搬迁“单体代码树,不是单体代码库”这种类型的项目到 Github 上,将会是对所有存量的å·¨型项目非常大的好处。
[编辑] Tradeoff 无处不在

Daniel 的文章到此为止,接下来是笔者谈一谈自己的看法,一家之言,仅供参考。

首先,因为 Github 的设计问题,把紧耦合的代码库强行拆分成多个子项目确实是一个大问题。但是,对原本就是松耦合的代码之间,拆分的好处也是明显的。大家可以按照各自的方向,节奏,去开发和维护。广义的看,Glibc 和 Linux 内核的关系就是这样,之所以运转良好,稳定的系统调用接口是关键。因此,代码库的划分边界应该以是否存在稳定的协议和接口为前提。

其次, Github 作为世界上最大的同性社交平台, 解决的是个体开源程序爱好者的需求问题。因此,在管理模型上,Github 不支持“单体代码树,不是单体代码库” 这种大型开源项目需要的模式也不奇怪。作为产品的定位来说,或许当前 Github 的设计,正是它成功的一个原因。正是它才让 Git 从少数 Linux 内核开发者手里迅速传播到整个程序员群体。

最后,以 KISS (keep it simple stupid) 的原则看,对主流用户足够傻瓜化,是 Github 的最大优势。但随着 Github 的影响越来越大,越来越多的企业和组织开始在其上托管项目,或许 Daniel 的建议值得认真考虑?至少,企业用户才有可能付出真金白银呢。

看来,不单在技术上我们面临各种 tradeoff,在产品的发展方向上,也是一æ ·的。谁知道这篇文章所诟病的 Github 的缺点,不是当初 Github 产品经理的深度思考后的决定呢?而 Daniel 的建议,说不定又是新形势下满足大企业需求的一个需要呢?