依附元件管理

回報問題 查看原始碼

瀏覽上頁時,一個主題會不斷重複:管理自己的程式碼相當簡單,但是管理其依附元件變得更困難。具有各種依附元件:有時工作會有相依關係 (例如「在將版本標記為已完成之前,先推送說明文件」),有時也有依附元件 (例如「我需要有最新版本的電腦視覺程式庫來建構我的程式碼」)。有時,您擁有程式碼依附元件的其他部分,或程式碼本身 (或您其他團隊)。但無論如何,「我需要先做到這個概念」的概念就是在建構系統的設計中反覆出現,而管理依附元件可能是建構系統最基本的工作。

處理模組和依附元件

使用以成果為基礎的建構系統的專案 (如 Bazel) 會細分為一組模組,其中模組透過 BUILD 檔案互相表示依附元件。正確組織這些模組和依附元件,對於建構系統的效能以及維護所需的工作量都有很大的影響。

使用精密模製和 1:1:1 規則

建構依據構件的建構中,第一個問題是決定個別模組應納入的功能數量。在 Bazel 中,模組會以指定可建構單元的目標表示,例如 java_librarygo_binary。在一個極端的情況下,您可以將一個 BUILD 檔案放在根層級,並遞迴地使用該專案的所有來源檔案,以將整個專案納入單一模組中。在另一個極端,幾乎所有來源檔案都可以成為各自的模組,有效地讓每個檔案在 BUILD 檔案中依附於彼此所依附的檔案。

大多數專案都落在極端情況之間,而選擇涉及效能與維護能力之間的取捨。對整個專案使用單一模組可能代表不必新增 BUILD 檔案,除非新增外部依附元件,但這意味著建構系統必須始終一次建構整個專案。這表示它無法平行處理或發布版本的部分,也無法快取已建立的部分。每個模組的單一模組則相反:建構系統對快取與排程步驟的彈性具有最大彈性,但工程師每次修改檔案時,都需花費更多心力維護依附元件清單。

雖然確切的精細程度會因語言而異 (通常甚至在某種語言當中),但 Google 傾向於比模組更小、通常在以工作為基礎的建構系統中編寫的模組更少。Google 的一般實際工作環境二進位檔通常取決於數萬個目標,即使是中型團隊,在程式碼集內,也能擁有數百個目標。以 Java 這類語言含有強烈封裝包裝的語言,每個目錄通常包含單一套件、目標與 BUILD 檔案 (褲子是另一個以 Bazel 為基礎的建構系統,稱為 1:1:1 規則)。採用較包裝包裝的語言經常會為每個 BUILD 檔案定義多個目標。

規模較小的建構目標優點會迅速開始大規模顯示,因為這類版本會加快分散式建構的速度,且較不具有重建目標的需求。更精細的目標,表示在測試執行完圖片後,建構系統就能產生更大的效益,因為建構系統僅可執行受特定變更影響的有限測試子集,會更有利於進行。Google 深信,使用小型目標對系統的好處有益,因此我們投注心力開發工具,以自動管理 BUILD 檔案,避免對開發人員造成負擔,使工作減少。

buildtools 目錄中的 Bazel 可使用其中部分工具 (例如 buildifierbuildozer)。

最小化模組顯示設定

Bazel 與其他建構系統允許每個目標指定瀏覽權限,也就是用來決定其他目標可能需要依賴的屬性。私人目標只能在自己的 BUILD 檔案中參照。目標可將範圍限定於明確定義 BUILD 檔案清單的目標,或者在公開瀏覽權限中,為工作區中的每個目標授予更廣泛的瀏覽權限。

與大部分的程式設計語言一樣,最好盡可能提高瀏覽權限。一般來說,只有在 Google 團隊擁有廣泛使用的程式庫時,Google 團隊才會將目標設為公開。 如果團隊要求客戶在使用程式碼前 與他們協調,將會把客戶目標的許可清單列為目標 。每個團隊的內部實作目標僅限於該團隊擁有的目錄,而大多數 BUILD 檔案都只有一個私人目標。

管理依附元件

模組必須要能彼此參照。將程式碼集分成精細模組的缺點是,您必須管理這些模組之間的依附元件 (但工具可協助您自動執行這項操作)。表示這些依附元件,通常是 BUILD 檔案中的大量內容。

內部依附元件

在分成精細模組的大型專案中,大部分的依附元件都可能是內部,也就是在同一個來源存放區中定義並建構的其他目標。內部依附元件與外部依附元件的不同之處在於,內部依附元件是以原始碼建構,而不是在執行建構作業時以預先建構的構件下載。這也表示內部依附元件沒有「版本」的概念:目標及其所有內部依附元件一律會在存放區的同一修訂版本/修訂版本中建構。您應針對內部依附元件妥善處理一個問題,就是如何處理遞移依附元件 (圖 1)。假設目標 A 依附於目標 B,目標 B 依附於共同程式庫目標 C。目標 A 是否可以使用目標 C 中定義的類別?

遞移依附元件

圖 1. 遞移依附元件

就基礎工具而言,這沒有任何問題;B 和 C 在建構時都會連結到目標 A,所以 C 中定義的任何符號都是 A 已知。Bazel 使用了多年來的成果,但隨著 Google 的發展,我們開始看到問題。假設 B 經過重構,所以不再需要依賴 C。如果 B 依附於 C 的依附元件,則 A 以及透過 B 依附元件使用 C 的任何其他目標都會中斷。實際上,目標的依附元件會成為公開合約的一部分,且永遠無法安全地變更。這意味著,隨著時間的增加,Google 的建構作業會開始減緩運作速度。

Google 最終在 Bazel 中推出了「嚴格傳輸模式」來解決這個問題。在這個模式下,Bazel 會偵測目標是否直接參照該符號,而不直接依賴該符號,如果是,則會失敗並發生錯誤,以及可用於自動插入依附元件的殼層指令。在整個 Google 的整個程式碼集中推出這項變更,並重構我們數百萬個建構目標的每組建構目標,以明確列出其依附元件,都是多年來的努力,但值得一試。我們的建構速度現在變得更快了,因為目標的不必要的依附元件較少,而工程師也能夠移除不需要的依附元件,而不必擔心破壞這些依附目標的破壞性目標。

與往常一樣,強制執行嚴格的遞移依附元件會需要權衡取捨。由於建構程式庫經常必須在許多地方明確列出 (而非不小心提取),因此工程師需要更頻繁地列出建構檔案,而工程師必須投入更多心力在 BUILD 檔案中加入依附元件。因此,我們開發了各種工具,可以自動偵測許多缺少的依附元件並將其新增至 BUILD 檔案,讓開發人員完全不用介入處理。然而,儘管沒有這類工具,我們也發現,在程式碼集規模擴充的情況下,取捨取捨是非常有利的做法:在 BUILD 檔案中明確新增依附元件是一次性費用,不過只要建構目標存在,即可處理隱含的遞移依附元件,可能會引發持續的問題。根據預設,Bazel 對 Java 程式碼會強制執行嚴格的遞移依附元件

外部依附元件

如果依附元件不是內部使用,則必須是外部依附元件。外部依附元件是指在建構系統外建構和儲存的成果上的外部依附元件。依附元件會直接從構件存放區匯入 (通常是透過網際網路存取),而且按原樣使用,而非從來源建構。外部依附元件和內部依附元件最大的差異之一,就是外部依附元件有版本,且這些版本獨立於專案的原始碼之外。

自動與手動依附元件管理

建構系統可允許手動或自動管理外部依附元件的版本。手動管理時,buildfile 會明確列出希望從成果存放區中下載的版本,且通常會使用語意版本字串 (例如 1.1.4)。當自動管理時,來源檔案會指定可接受的版本範圍,然後建構系統一律會下載最新的版本。舉例來說,Gradle 允許將依附元件版本宣告為「1.+」,以指定任何次要或修補版本的依附元件 (只要主要版本為 1)。

小型的自動依附元件對於小型專案來說很方便,但通常都是針對具有重要規模的專案進行災難復原,或是有多位工程師共同處理的方案。自動管理依附元件的問題是,您無法控製版本更新的時間。您無法確保外部各方不會取得破壞性更新 (即使對方聲稱使用語義版本管理),因此,如果一天可以建構的建構可能會失敗,且難以偵測變更或復原至工作狀態,因此可能會破壞下一個版本。即使建構未中斷,也可能發生無法追蹤的細微行為或效能變更。

相較之下,由於手動管理的依附元件需要變更來源控管,因此您可以輕鬆發現及復原這些依附元件,並建議您查看舊版存放區,以便使用舊版依附元件進行建構。Bazel 必須手動指定所有依附元件的版本。即使是中等規模的資源調度,手動管理版本負擔也十分值得,希望它能夠帶來穩定的穩定性。

單一版本規則

不同版本的程式庫通常會由不同的構件表示,因此理論上,在建構系統中,無法以不同的名稱同時宣告同一個外部依附元件的不同版本。如此一來,每個目標都能選擇要使用的依附元件版本。這會造成許多問題,因此 Google 針對程式碼集的所有第三方依附元件強制執行嚴格的單一版本規則

允許多個版本最大的問題是菱形依附元件問題。假設目標 A 依附於目標程式庫 B 和外部程式庫的 v1。如果目標 B 之後重構為在相同外部程式庫的 v2 中新增依附元件,目標 A 將會中斷,因為它現在隱含地依附於同一個程式庫的兩個不同版本。實際上,將目標中的新依附元件新增至具有多個版本的第三方程式庫是不安全的,因為該目標的任何使用者已經能夠依附於不同的版本。遵循「單一版本規則」之後,這個衝突就成了不可能的。如果目標在第三方程式庫中新增依附元件,任何現有的依附元件都將位於該版本上,因此可以順利共存。

遞移的外部依附元件

處理外部依附元件的遞移依附元件可能並不容易。許多構件存放區 (例如 Maven Central) 允許構件指定存放區中其他構件的特定版本。根據預設,Maven 或 Gradle 等建構工具會以遞迴方式下載每個遞移依附元件,也就是說,如果在專案中新增單一依附元件,總計可能會下載數十個成果。

這個做法相當方便:在新的程式庫中新增依附元件時,必須追蹤每個程式庫的遞移依附元件並手動加入所有程式庫的一大難題。不過,這也存在很大的缺點:由於不同的程式庫可能依附於同一第三方程式庫的不同版本,因此這個策略絕對違反單一版本規則,並導致鑽石依附元件問題。如果您的目標取決於兩個使用相同依附元件不同版本的外部程式庫,則無從得知哪個程式庫。這也表示,如果新版本開始提取部分依附元件的衝突版本,更新外部依附元件可能會導致整個程式碼集看似不相關。

因此,Bazel 不會自動下載遞移依附元件。可惜的是,目前沒有銀色項目符號。Bazel 的替代方法是需要一個全域檔案,其中會列出存放區中每個依附元件的依附元件,以及該存放區用於整個依附元件的明確版本。幸好,Bazel 提供的工具會自動產生這類檔案,其中包含一組 Maven 成果的遞移依附元件。這項工具可以執行一次,以便為專案產生初始 WORKSPACE 檔案,接著您就可以手動更新該檔案,以調整每個依附元件的版本。

再次強調,要做的選擇便是方便,而且可以靈活調整。小型專案可能會比較不想要自行管理遞移依附元件,而且也可能能夠使用自動轉換依附元件。隨著機構與程式碼集的增加,這項策略的吸引力越來越低,衝突和意料之外的結果也越來越常見。大規模來說,手動管理依附元件的成本遠低於處理自動依附元件管理問題所產生的費用。

使用外部依附元件快取建構結果

外部依附元件通常由發布穩定版本的第三方提供,可能未提供原始碼。有些機構也會選擇將某些程式碼當做構件提供,讓其他程式碼可做為第三方而非以內部依附元件的方式使用。如果構件建構速度較慢,但下載速度較快,理論上就會加快建構速度。

不過,這也引來許多負擔和複雜性:必須負責建構每個構件並上傳至構件存放區,而客戶必須確保其能更新至最新版本。偵錯也會變得越來越困難,因為系統將不同部分從存放區中的不同位置建構,而不再有來源樹狀結構的一致檢視畫面。

如要解決構件所花的時間較長,最好的方法就是使用支援遠端快取的建構系統,如前文所述。這類建構系統會將每個建構作業產生的構件儲存至工程師間共用的位置,因此如果開發人員依賴的是最近才由他人建構的成果,則建構系統會自動下載該成果,而不是加以建構。這可提供直接受益於成果的全部效能優勢,同時仍確保版本與始終從同一來源建構一樣一致。這是 Google 內部使用的策略,而 Bazel 可以設為使用遠端快取。

外部依附元件的安全性和可靠性

取決於第三方來源的構件,本身必定有風險。第三方來源 (例如成果存放區) 停止運作時,可用性可能會因此受到影響,因為如果整個版本無法下載外部依附元件,則建構作業可能會停止運作。另外也有一個安全性風險:如果第三方系統遭到攻擊者入侵,攻擊者可能會用自己設計的設計,將參照的成果替換為前述的成果,讓對方能夠在建構作業中插入任意程式碼。只要將您依賴的任何構件鏡射到您控制的伺服器上,並阻止建構系統存取 Maven Central 等第三方構件存放區,即可解決這個問題。而需要權衡的是,這些鏡像需要耗費資源和資源來維護,因此是否使用這些鏡像的選擇往往取決於專案的規模。您也可以在來源存放區中指定每個第三方構件的雜湊值,這樣即使完全造成負擔,也能完全防止安全性問題,如果成果遭到破壞,則會導致建構失敗。另一個另一個問題是解決整個問題的方法,就是將專案的依附元件提供給供應商。當專案提供依附元件時,專案會將其與專案原始碼一起檢查為來源控管,無論是來源或二進位檔。這樣就能有效將所有專案的外部依附元件轉換為內部依附元件。Google 內部使用這個方法,將整個 Google 參照的所有第三方程式庫都檢查到 Google 來源樹狀結構根目錄的 third_party 目錄中。不過,這只適用於 Google,因為 Google 的原始碼控管系統經過特別設計,能夠處理極為龐大的單體式,因此供應商可能不適合所有機構使用。