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 的建议,说不定又是新形势下满足大企业需求的一个需要呢?