依附元件管理

回顧之前的頁面,我們會不斷重複說明一個主題:管理自己的程式碼相當簡單,但管理依附元件會更加困難。依附元件有很多種,例如工作有時取決於依附元件 (例如「在版本發布之前,先推送說明文件」),以及有時出現依附元件 (例如「I 必須有最新版本的電腦視覺程式庫才能建構我的程式碼)。有時在某些情況下,程式碼集的另一個部分具有內部依附元件,有時甚至有程式碼或資料的外部依附元件由其他團隊 (例如貴機構或第三方) 擁有。但無論如何,「我一定要先有這項能力」的概念,就是在設計系統中一再重複出現,而管理依附元件可能就是建構系統。

處理模組與依附元件

使用以 Artifact 為基礎的建構系統 (例如 Bazel) 的專案會分為多個模組,這些模組會透過 BUILD 檔案來表達彼此的依附元件。正確規劃這些模組和依附元件,對建構系統的效能和維護工作都有很大的影響。

使用精細的自訂模組和 1:1:1 規則

建構以成果為基礎的建構時,第一個問題是決定個別模組要包含多少功能。在 Bazel 中,「模組」是用來指定 java_librarygo_binary 等可建構單位的目標。最後,整個專案都可以在單一模組中納入單一 BUILD 檔案,並透過遞迴方式覆蓋該專案的所有來源檔案。在另一個情況下,幾乎每個來源檔案都能歸入各自的模組中,讓每個檔案都能在 BUILD 檔案中列出需要的所有檔案。

大多數專案都發生在這些極端值之間,而選擇對於效能和可維護性也有很大的平衡。對整個專案使用單一模組可能表示您不必新增外部依附元件,也不必輕觸 BUILD 檔案,但是這代表建構系統必須一律建構整個所有專案。這表示它無法平行處理或發布建構的部分,也無法快取已經建構的部分。相反地,每個檔案的單一模組則相反:建構系統對於快取的排程和排程步驟具有最大彈性,但工程師每次變更時,都必須花費更多心力來維護依附元件清單哪些檔案參照了參考檔案。

雖然確切的精細程度會因語言而異 (但通常不只語言),Google 傾向於通常會使用以工作為基礎的建構系統撰寫而成的小型模組。Google 的一般典型二進位檔通常取決於成千上萬的目標,即使是中小型團隊可以在程式碼集內擁有數百個目標。像 Java 這樣的內建內建封裝方法的語言很類似,每個目錄通常都含有一個套件、目標和 BUILD 檔案,而後者則是另一個以 Bazel 為基礎的建構系統,則稱為 1:1:1 規則)。如果套件的封裝方式較弱,通常會為每個 BUILD 檔案定義多個目標。

小型建構目標的優勢在於可開始大規模顯示,因為這類機器採用較快的分散式建構作業,而且需要重新建構目標的頻率較低。這些優勢在測試進入映像檔後更具有吸引力,因為精細的目標意味著建構系統更為聰明,僅執行一小部分可能受任何指定測試影響的測試變更。由於 Google 相信使用目標越小的系統優勢,因此我們投入了大量心力,自動開發 BUILD 檔案來避免減輕開發人員的負擔,進而減少衍生作業的進展。

其中某些工具 (例如 buildifierbuildozer) 可與 buildtools 目錄中的 Bazel 搭配使用。

最小化模組瀏覽權限

Bazel 和其他建構系統可讓每個目標指定瀏覽權限:指定哪些目標可能會依賴其目標。目標可以公開,因此工作區中的任何其他目標都可以參照目標;不公開;在此情況下,您只能在相同的 BUILD 檔案中參照;或只有其他明確定義的目標清單顯示。顯示設定與依附元件大致相反:如果目標 A 想依賴目標 B,目標 B 必須讓目標 A 能夠看見。和大多數的程式設計語言一樣,最佳做法是盡可能降低可見度。一般來說,這些 Google 團隊必須要能代表 Google 所有團隊都能使用的廣泛資料庫,相關目標才會公開。如果團隊需要先與其他人協調,再使用他們的程式碼,相關用戶端的瀏覽權限就會列在許可清單中。每個團隊的內部實作目標僅限於團隊擁有的目錄,而且大多數的 BUILD 檔案都只有一個目標並非私人目標。

管理依附元件

模組必須能夠互相參照。將程式碼集拆分為精細模組的好處,就是您必須管理這些模組中的依附元件 (不過工具可以協助您自動化)。傳遞這些依附元件通常成為 BUILD 檔案中的大部分內容。

內部依附元件

在大型專案中細分為精細模組的專案,大多數的依附元件都可能是內部的;也就是在相同原始碼存放區中定義及建構的另一個目標。內部依附元件與外部依附元件的不同之處在於,這些依附元件是從來源建構,而不是在執行建構作業時做為預先建構的構件下載。這表示內部依附元件沒有「版本」,也就是說,目標及其所有依附元件一律會在存放區的同一個修訂版本/修訂版本中建立。處理內部依附元件時,必須如何處理一個問題,就是要如何處理傳輸依附元件 (圖 1)。假設目標 A 取決於目標 B,而目標 B 仰賴通用的程式庫目標 C。目標 A 是否能使用目標 C 中定義的類別?

大眾運輸依附元件

圖 1. 大眾運輸依附元件

就相關工具而言,這種做法沒有問題; B 和 C 在建立時會與目標 A 連結,因此 C 中定義的所有符號皆為 A。Bazel 多年來都允許這種做法,但隨著 Google 的規模不斷成長,我們也遇到了問題。假設 B 已重構,不再需要使用 C。如果 B 的 C 依附元件與 C 的依附元件已移除,則 A 與透過 B 依附元件利用 C 的任何其他目標都會中斷。實際上,目標的依附元件已成為其公開合約的一部分,而且永遠無法變更。這表示依附元件的累積隨著時間逐漸增加,而 Google 開始的建構作業會開始減緩速度。

Google 最近在 Bazel 中導入「嚴格傳輸相依模式」,藉此解決這個問題。在這個模式下,Bazel 會偵測目標是否嘗試直接參照符號,而不直接結合;如果有,則會發生錯誤及殼層指令,可用於自動插入依附元件的 Google Ads 新帳戶重新申請驗證。我們改用 Google 的完整程式碼集來實施這項異動,並將數以百萬計的建構作業目標重構為具體,而這些做法很多年來都很重要,但價值就很高。由於我們的目標不再是不必要的依附元件,因此我們的建構速度比以往更快,工程師可以移除不需要的依附元件,不必擔心會影響到依賴這些依附元件的目標目標。

如往常,強制執行嚴格的傳輸依附元件會涉及取捨。這會讓建構檔案變得較為複雜,因為常用的程式庫現在必須明確列出在許多位置,而不是在事件中提取,且工程師需要花費更多心力,在 BUILD 檔案中加入依附元件。的 Google Ads 新帳戶重新申請驗證。我們開發開發的工具可以自動偵測許多缺少的依附元件,並將其新增至 BUILD 檔案,完全無需開發人員介入,因此有助於減少這個失能。然而,即使沒有這類工具,我們也發現權衡與程式碼集越大相配:明確將依附元件加進 BUILD 檔案是一次性費用,但無需處理只要建構目標已存在,隱性間接依附元件可能會導致持續發生的問題。根據預設,Bazel 會在 Java 程式碼中強制執行嚴格的傳輸依附元件

外部依附元件

如果依附元件不是內部的,則必須是外部元件。外部依附元件是指在建構系統之外建構及儲存的構件。依附元件會直接從構件存放區匯入 (通常透過網際網路存取),並依原樣使用,而不是從原始碼開始建構。外部依附元件和內部依附元件的主要差異之一,就是外部依附元件有對應的版本,且這些版本與專案的原始碼無關。

自動管理與手動依附元件管理

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

自動管理的依附元件對小型專案來說可能很方便,但一般而言,專案往往是針對規模不大的專案,或是經過多位工程師開發的災害。自動代管依附元件的問題是,當版本更新時無法控管。沒有任何方法無法保證外部廠商不會進行破壞更新 (即使他們使用語意化版本,也意味著如果一天無法工作,其建構作業可能會無法正常運作,而且無法輕易偵測偵測到的問題已變更為復原至運作狀態。即使建構未中斷,還是可能難以追蹤細微的行為或效能變更。

相對地,由於手動管理依附元件需要在來源控制中做出變更,因此您可以輕鬆找到及復原這些存放區,您也可以查看較舊的存放區版本,以使用舊版依附元件建構。Bazel 需要手動指定所有依附元件的版本。即便是中等規模,手動管理版本帶來的負擔也有助於提供穩定性。

單一規則

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

允許使用多個版本最大的問題是 the 形依附元件問題。假設目標 A 取決於目標 B 和外部程式庫的 v1。如果稍後重構了目標 B,將相同外部程式庫的 v2 新增為依附元件,則目標 A 將會損毀,因為其現在隱含取決於同一個程式庫的兩個不同版本。實際上,將目標新增至新的程式庫到多個具有不同版本的第三方程式庫就沒有任何安全疑慮,因為該目標的使用者可能已經取決於不同的版本。遵循單一版本規則可避免這種衝突。如果目標在第三方程式庫中加入依附元件,現有的依附元件就已在同一個版本中,因此可以同時存在。

暫時性外部依附元件

處理外部依附元件間接相依性的工作可能特別困難。許多構件存放區 (例如 Maven Central) 可讓構件指定存放區中其他構件的特定版本依附元件。根據預設,Maven 或 Gradle 等工具通常會以遞迴方式下載每個遞迴依附元件,這表示在專案中新增單一依附元件可能會導致總計下載數十項構件。

這種做法真的非常方便:對新程式庫新增依附元件時,追蹤每個程式庫的大眾運輸依附元件,並手動新增,是相當艱鉅的任務。但也還有一個缺點:由於不同的程式庫可能 depend 賴同一個第三方程式庫的不同版本,因此這個策略一律違反單版規則,這會導致 the 鑽穩定性問題。如果目標依賴於使用不同依附元件不同版本的兩個外部程式庫,就無法得知您可以取得哪一個版本。這也表示,如果更新新版依附元件開始提取與其衝突的某些依附元件,整個程式碼集在看起來應該要看起來不相關。

因此,Bazel 不會自動下載傳輸依附元件。然而,沒有一種銀條項目符號,也就是 Bazel 替代方案,是需要一個全域檔案來列出存放區中的每個外部依附元件,以及在存放區中用來使用該依附元件的明確版本。幸好,Bazel 提供的工具可以自動產生,內含一組 Maven 構件的依附元件。這項工具只需執行一次,就能為專案產生初始 WORKSPACE 檔案,接著您可以手動更新該檔案,藉此調整每個依附元件的版本。

但這裡的選擇之一是便利性與擴充性之間的選擇。小型專案可能會無意自行管理傳輸依附元件,且或許能透過自動傳輸依附元件。隨著機構和程式碼集成長,這個策略也越來越吸引人,而衝突和非預期的結果也越來越常見。在大規模上,手動管理依附元件的費用遠低於處理自動依附元件管理問題所造成的影響。

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

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

但是,這也會帶來大量負擔及複雜度:使用者必須負責建構每個構件,然後將這些成果上傳至構件存放區,用戶端必須確保他們與 101}最新版本。偵錯也變得更加困難,因為系統是根據存放區的不同點建構系統的不同部分,而原始碼的樹狀結構也不再固定。

如要解決建構時間過長的成果問題,更好的方法就是使用支援遠端快取的建構系統,如前文所述。這類建構系統會將產生的所有成果儲存至各個工程師共用的多個位置,因此如果開發人員仰賴最近建構的構件,建構系統會自動下載而不是建構。這樣可以直接仰賴構件,並享受與使用相同來源建構時一致的一致性。這是 Google 內部使用的策略,並將 Bazel 設為使用遠端快取。

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

視第三方來源的成果而定,固然具有風險。如果第三方來源 (例如構件存放區) 故障,則可用性風險會受到影響,因為如果無法下載外部依附元件,整個建構項目可能會暫時停止。此外,安全性風險也有影響:如果第三方系統遭到攻擊者入侵,攻擊者可能會利用自己本身的設計來取代參照的成果,藉此在程式中植入任意程式碼的 Google Ads 新帳戶重新申請驗證。只要將所依賴的構件複製到您控制的伺服器,並禁止建構系統存取 Maven Central 等第三方構件存放區,就能減少這兩個問題。優缺點是這些鏡子投入心力和資源,因此選擇是否使用這兩項專案,往往取決於專案的規模。您完全不需要解決安全性問題,只要在原始碼存放區中指定各個第三方構件的雜湊值,就能妥善防止這類問題發生。如果這些映像檔遭到竄改,將導致建構失敗。另一個方法是全面解決專案的依附元件。當專案以其依附元件為基礎時,系統即會以來源或二進位檔的形式,將原始碼與專案原始碼進行比對。實際上,該專案的所有外部依附元件都會轉換為內部依附元件。Google 會在內部使用這個方法,檢查所有 Google 內部參照的第三方程式庫是否列於 Google 來源樹狀結構的 third_party 目錄中。不過,這種做法只適用於 Google,因為 Google 的來源控管系統是專門處理極為大型的單體式系統,因此可能不是所有機構都能使用這項服務。