GitHub爲什麼託管不了Linux內核社區?

前不久,微軟在 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 的實現方式。

Github 的擴展之道

Git 很棒,因爲每個人都能夠輕鬆在上面分叉、創建分支以及修改代碼。其中的優勢也顯而易見,爲主 repo 創建一項 pull request,然後進行審查、測試與合併。GitHub 同樣非常強大,它提供一套 UI,能夠讓這些複雜的操作變得易於學習、易於上手,並藉此大大降低了項目貢獻的技術門檻。

但最終總會有一些項目取得巨大的成功,但標記、標籤、排序、bot-herding 以及自動化機制的缺失,導致現有託管平臺無法滿足 repo 在處理大量 pull request 及 issue 方面的需要。爲此,必須將項目拆分爲更易於管理的形式。更重要的是,根據項目規模與誕生時間的不同,各個部分還需要配合不同的規則與流程:新的實驗性 repo 與主體代碼之間往往具有不同的穩定性與 CI 規則。另外,我們還可能面對一大堆廢棄的插件,早已不受支持但又不能貿然刪除。具體來講,我們需要將龐大的項目拆分成多個子項目,保證每個子項目都擁有自己的流程與運作風格,同時分別納入獨立的標準、pull request 以及 issue 實現風格。光是這項重組工作本身,可能就需要幾十甚至上百位全職貢獻者的不懈努力……而這,真的有必要嗎?

幾乎所有託管在 GitHub 上的項目,都需要將其 monorepo 源代碼樹拆分成多個不同的項目,藉此維持正常運轉。而各個項目都擁有其獨特的功能集。分散化的結果,就是同一個項目中包含多個核心,外加成堆的插件、庫以及擴展。所有這些都依靠着某種插件或者軟件包管理器被捆綁在一起,在必要時直接從 GitHub repo 中提取內容。

目前幾乎所有大型項目遵循的都是這樣的治理結構,所以咱們就沒必要再贅述這種方式的好處了。相反,我覺得應該強調一下這麼做引發的問題:

  • 本該統一的社區陷入碎片化。大多數貢獻者只擁有自己直接貢獻的代碼與 repo,而接觸不到其他項目內容。這雖然有助於貢獻者集中注意力,但同時也降低了他們在不同插件及庫之間共享工作成果、以及同其他子項目團隊組織並行開發協作的可能性。而項目的總體負責人,則需要通過一系列腳本、git 子模塊甚至是大量 repo 協同才能完成自己的本職工作。另外,由於總體負責人需要關注所有內容,因此極易被洶湧而來的 pull request 及 issue 所吞沒。任何與當前項目及 repo 拆分方式不一致的協作需求(例如共享構建工具、說明文檔等),都會給相應的維護者帶來巨大的壓力。
  • 即使充分認識到了結構重組的必要性,結構調整與代碼共享也仍會面臨一系列官僚式障礙:首先,大家需要發佈新版本的核心庫,而後瀏覽所有插件並進行更新,接着根據實際情況刪除共享庫中的部分舊代碼。而高度碎片化,往往導致舊代碼刪除執行得不夠徹底。

當然,相當一部分工作其實不需要那麼麻煩,也有很多項目都可以輕鬆完成變更。但無論如何,具體操作都要比對單一 monorepo 直接執行 pull request 要麻煩得多。因此,項目貢獻者將傾向於不頻繁執行非常簡單的重構(例如僅共享一項新功能),這會在一段時間內快速積累起大量提交庫存。當然,我們可以使用面向單一函數的 node.js 重構方式,但這相當於是把源代碼控制系統由 git 替換成了 npm——npm 也不怎麼樣,實話實說。

  • 理論上 ,受支持的版本組合在數量上終將爆炸,徹底破壞掉我們的支持體系。作爲用戶,大家最終必須自行完成集成測試。換言之,對於單一項目,最終將只有其中某些元素的特定組合才能正常運作,或者至少在事實上具有可靠的匹配穩定性。同樣的,這實際意味着我們的 monorepo 已經無法在 git 當中進行管理。或者說……除非我們選擇使用子模,但這還能算是 git 嗎?
  • 對於完整項目,重組其子項目拆分方式同樣非常麻煩。這意味着我們需要重新組織各 git repo 及其拆分思路。在 monorepo 當中,只需要更新 OWNER 或者 MAINTAINERS 文件即可轉移維護權,只要各 bot 運作良好,新的維護者就得被自動標記。但如果您的擴展方案將各不長 repo 拆分成彼此不相交的集合,那麼任何重組操作在難度上都將等同於從零開始將 monorepo 拆分成一組 repo。換句話說,您的項目將始終擺脫不了糟糕的組織結構。

插曲:爲什麼存在 Pull Request 這種東西

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 內核的擴展之道

乍看之下,Linux 內核很像是那種 monorepo,所有東西都被收納在 Linux 的主 repo 當中。但實際情況真這麼簡單嗎?當然不是:

  • 幾乎沒有人會使用 Linux 運行 Linus Torvalds 的主 repo。雖然 Linus 對於上游應用來說可以算是最穩定的內核選項之一,但大多數用戶實際上是在自己的發行版中運行內核,因此所需要的內核通常還包含其他補丁程序與反向移植代碼,甚至並未被託管在 kernel.org 網站上。這就形成了一種完全不同的組織結構。或者,不少用戶也會使用由硬件供應商提供的內核(適用於 SoC 以及幾乎所有 Android 平臺),而且與“主”repo 相比,這些內核往往包含規模可觀的增量元素。
  • 除了 Linus 本人之外,已經沒有人在 Linus 的 repo 上開發項目。時至今日,每一種子系統,包括各類大型驅動程序,都擁有自己的 git repo、自己的郵件列表、以及用於跟蹤提交併處理問題的獨立體系。
  • 跨子系統工作在 linux-next 集成樹之上實現,其中包含來自衆多不同不長 repo 的數百個 git 分支。
  • 所有這些多到瘋狂的元素,都需要通過 MAINTAINERS 文件與 get_maintainers.pl 腳本進行管理。以此爲基礎,我們才能面對任意給定的代碼段,通過腳本瞭解誰是對應的維護者、誰需要對此進行審查、正確的 git repo 在哪裏、要使用哪份郵件列表以及如何與在哪裏上報 bug。其中不僅包含準確的文件位置,同時還提供代碼模式以保證跨子系統的具體主題(例如設備樹的處理或 kobject 層級結構)都由合適的專家負責處理。

從概念層面看,這種方法似乎太過複雜,導致每個人的磁盤裏都塞上不少跟自己根本沒什麼關係的管理系統。但總體來說,這種治理結構確實具有一定優勢:

  • 對項目內容進行重新整理與子項目拆分的過程非常簡單,只需要更新 MAINTAINERS 文件即可完成。當然,大家可能需要創建一個新的 repo、新的郵件列表以及新的 bugzilla,所以實際操作過程往往並沒這麼輕鬆。但正如 GitHub 直接提供簡單的 fork 按鈕,這方面問題只靠 UI 中的一點設計即可解決,不是什麼大事。
  • 我們可以在各子項目之間非常輕鬆地重新分配關於 pull request 及問題的討論內容。大家只需在回覆當中調整 Cc: list 即可。同樣的,跨子系統的各項工作也更易於協調,因爲您可以將同一請求提交至多個子項目;而且面向存儲在不同郵件列表歸檔中的郵件地址,您只需要一項整體討論(可以使用 Msg-Ids: tags 在郵件列表線程處理內添加所有人的標籤),就能夠向成千上萬個不同的收件箱發送消息。這一切不僅降低了子項目主題與代碼層面的討論難度、避免了碎片化傾向,同時也讓代碼共享與重構的收益更易被項目的全體參與者所理解與承認。
  • 跨子系統工作不需要任何形式的發佈操作。大家只需要更改代碼即可,所有代碼都將存儲在同一 repo 當中。另外,這也是一種遠比拆分 repo 更爲強大的治理模式:對於破壞性重構,大家仍可將其強制分發至多個發行版當中。例如在存在大量用戶的情況下,您可以立即對其 repo 做出變更,而不必有協調統籌方面浪費太多時間。

這種對重構及代碼共享的簡化,極大減輕了陳舊技術帶來的債務負擔。內核中不再需要保留毫無意義的非穩定版 api 說明文檔。

  • 這種方式雖然看似強硬,但並不會阻止大家創建自己的實驗性添加項,而這也是多 repo 設置的核心優勢之一。您可以將代碼直接添加到自己的分支當中,之後再也不必費心打理——不會有人強迫您將代碼撤回,或者將其推送至特定 repo 或者是主組織,因爲這套體系中根本就不存在中央 repo。這樣的設計思路聽起來頗有道理,實際表現也着實不錯,畢竟 Android 硬件供應商 repo 中那數以百萬計的代碼行已經充分證明了這一點。

簡而言之,我認爲這是一套更嚴格也更強大的模式,至少其中保留了後撤至採用多個互不相干 repo 的靈活空間。甚至某些英偉達驅動程序都有自己的專用 repo,與主內核樹完全不相交。這類 repo 只涉及一丁點源代碼,而且出於法律原因,其中不能包含內核中的任何內容,可以算是個完美的極端案例了。

這聽起來就像一場超大型 monorepo 的噩夢!

對,也不對。

乍看之下,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 並不支持這樣的工作流,至於在 GitHub UI 中不提供原生支持。雖然只需要簡單的 git 工具即可完成此類操作,但之後我們還是得回到郵件列表上的補丁程序,並以手動方式通過郵件執行 pull request。在我看來,這也是 Linux 內核社區決定不向 GitHub 遷移的核心原因。總體來說,雖然也有不少頂級維護者對 GitHub 還有這樣或者那樣的抱怨,但這些都不是真正的關鍵技術問題。而且不僅僅是 Linux 內核,一般來說任何規模足夠大的項目都 GitHub 上都很難順利擴展,因爲 GitHub 在設計上就限制了項目通過 monotree 進行多 repo 擴展的空間。

說到這裏,我想給 GitHub 提一項簡單的功能要求:

請通過單一 monotree 對多個 repo 上的 pull request 與 issue 跟蹤提供支持。

思路很簡單,影響卻將極爲深遠。

Repo 與組織

首先,我們可能希望在同一組織之內爲同一 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

我們有必要將 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 進行多次合併。

Issues

與 pull request 類似,issues 也可能同時涉及多個 repo,甚至需要進行往來轉移。這裏我們以 bug 爲例,假定從某一發行版的內核 repo 中初次發現並上報了一項 bug。在查驗之後,我們證明該 bug 歸屬於某驅動程序,目前處於最新的開發分支當中,且同時影響到當前 repo、上游主分支以及其他多個分支。

這裏我們需要對狀態進行再次拆分,因爲一次向一套 repo 推送補丁的作法無法快速覆蓋到全部 repo。我們甚至需要組織額外的修復方案,藉此處理較爲陳舊的內核或發行版,包括將一部分已經沒有修復必要的 repo 以 WONTFIX 的形式關閉,並在相應子系統 repo 中將其標記爲“已成功解決”。

總結:要 Monotree,不要 Monorepo

Linux 內核不會登陸 GitHub。但真正重要的是,GitHub 應該學習 Linux 內核項目採取的 monotree 架構思路,此舉也將給目前 GitHub 之上的各類大型項目帶來顯著收益。在我看來,這種架構層面的轉換,將爲整個 GitHub 帶來一種強大且獨特的擴展能力與靈活空間。

英文原文

Why Github can’t host the Linux Kernel Community

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章