編寫規則的挑戰

回報問題 查看原始碼 夜間 7.4 ,直接在 Google Cloud 控制台實際操作。 7.3 · 7.2 · 7.1 · 7.0 · 6.5

本頁面概略說明特定問題和挑戰 方便您編寫高效率的 Bazel 規則

摘要規定

  • 假設:以正確性、處理量、使用便利性和延遲時間
  • 假設:大型存放區
  • 假設:類似 BUILD 的說明語言
  • 歷史:載入、分析和執行作業之間的硬式分隔已過時,但仍會影響 API
  • 內建函式:遠端執行和快取是硬的
  • 內建函式:使用變更資訊進行正確快速的漸進式建構作業 需要異常的程式設計模式
  • 本質上:避免二次耗時和記憶體用量很困難

假設

下方是針對建構系統產生的假設, 正確性、使用便利性、處理量和大規模的存放區。以下各節將說明這些假設,並提供指南,確保規則以有效的方式編寫。

以正確性、處理量、使用便利性和延遲時間

我們假設建構系統必須首先針對增量建構作業正確運作。針對特定來源樹狀結構的 無論輸出樹狀結構什麼,都應一律是相同的建構 喜歡在第一個估算作業中,這意味著 Bazel 必須知道 傳入特定建構步驟的輸入值,讓它可以重新執行該步驟 (如有) 輸入內容會有什麼變化Bazel 會因錯誤外洩而受限, 並且忽略特定種類的資訊 (例如建構的日期 / 時間), 例如檔案屬性變更沙箱可防止讀取未宣告的輸入檔案,有助於確保正確性。側邊 系統的內建限制、有一些已知的正確問題 其中大多數與 Fileset 或 C++ 規則有關 如要解決關聯問題,可用 Apriori 這類關聯規則學習技術和演算法我們長期致力於解決這些問題。

建構系統的第二個目標是具備高處理量。我們 將部門內部可以做的事永久推進 分配給遠端執行服務如果遠端執行 服務超載,任何人都無法完成工作

接下來是讓客戶更容易使用。在多種可用於遠端執行服務的正確方法中 (或相似的),我們會選擇較容易使用的一種。

延遲時間代表從開始建構到取得預定目標所需的時間 結果 (可能是通過/失敗測試產生的測試記錄),或 系統會顯示訊息,指出 BUILD 檔案有錯字。

請注意,這些目標經常重疊;延遲時間是遠端執行服務的吞吐量函式,而正確性則與易用性相關。

大規模存放區

建構系統需要以大型存放區的規模運作,而大型規模是指無法放入單一硬碟的規模,因此幾乎無法在所有開發人員的電腦上執行完整的檢出作業。中等規模版本 必須讀取及剖析數萬個 BUILD 檔案,並評估 好幾十萬隻青蛙雖然理論上可以在單一機器上讀取所有 BUILD 檔案,但我們尚未能在合理的時間和記憶體內完成這項操作。因此,必須能夠獨立載入及剖析 BUILD 檔案。

類似 BUILD 的說明語言

在本範例中,我們假設的設定語言是 與程式庫和二進位規則宣告中的 BUILD 檔案大致類似 與其相互依存BUILD 檔案可獨立讀取及剖析,我們盡可能避免查看來源檔案 (除了確認檔案是否存在)。

歷史古蹟

Bazel 版本之間的差異會造成挑戰,我們會在下文中說明其中一些差異。

載入、分析和執行作業之間的硬式分隔已過時,但仍會影響 API

從技術層面來說,只要在將動作傳送至遠端執行作業之前,規則就知道動作的輸入和輸出檔案即可。不過,原始 Bazel 程式碼集會嚴格區分載入套件的作業,然後使用設定 (基本上是指令列標記) 分析規則,然後才執行任何動作。這項差異仍然是 Rules API 的一部分 即使 Bazel 的核心已不再需要這項技術 (詳情請見下文)。

這表示規則 API 需要宣告規則 介面 (其擁有的屬性、屬性類型)在某些例外狀況中,API 會允許自訂程式碼在載入階段執行,以便計算輸出檔案的隱含名稱和屬性的隱含值。適用對象 例如,名為「foo」的 java_library 規則會間接產生名為 「libfoo.jar」,可以自建構圖表中的其他規則參照。

此外,規則的分析無法讀取任何來源檔案或檢查動作的輸出內容;相反地,它需要產生部分導向的二元圖表,其中的建構步驟和輸出檔案名稱,僅由規則本身及其依附元件決定。

本質上可解釋

有些內在屬性會讓您難以編寫規則,以下幾節將說明其中最常見的屬性。

遠端執行和快取作業相當困難

遠端執行和快取可藉由下列做法,縮短大型存放區中的建構時間: 與在單一應用程式中執行建構相比,執行建構作業大約是兩條大小 這類機制更為快速然而,它需要執行的規模相當驚人 遠端執行服務專為處理大量要求而設計 其後,通訊協定會謹慎避免不必要的往返作業 為服務端提供不必要的工作

此時通訊協定要求建構系統知道所有輸入內容的輸入內容 並採取相應行動接著,建構系統會計算不重複的動作 指紋,並要求排程器進行快取命中如果系統在快取中找到了所需資料 排程器會回覆輸出檔案的摘要;主要是 稍後將透過摘要解決不過,這會對 Bazel 規則施加限制,因為 Bazel 規則需要預先宣告所有輸入檔案。

如要使用變更資訊來進行正確且快速的漸進式建構作業,就必須採用不尋常的程式碼模式

如上所述,為了確保正確性,Bazel 需要知道進入建構步驟的所有輸入檔案,以便偵測該建構步驟是否仍為最新版本。套件載入和規則分析也是如此,我們設計的 Skyframe 可處理這類一般情況。SkyFrame 是一種圖形程式庫和評估架構 目標節點 (例如 'build //foo with 這些選項'),並細分為 接著,系統會評估及組合這些組成部分 結果。在這個程序中,Skyframe 會讀取套件、分析規則,並執行動作。

在每個節點中,Skyframe 會追蹤特定節點用於計算自身輸出的確切節點,從目標節點一路到輸入檔案 (也是 Skyframe 節點)。在記憶體中明確呈現這類圖表,可讓建構系統準確找出哪些節點受到輸入檔案的特定變更 (包括建立或刪除輸入檔案) 影響,並以最少的工作量,將輸出樹狀結構還原至預期狀態。

在這個過程中,每個節點都會執行依附元件探索程序。每個節點都可以宣告依附元件,然後使用這些依附元件的內容宣告更多依附元件。原則上,這會對應到每個節點的執行緒模型。但中型建構包含數百個 使用現行的 Java 程式,成千上百個 SkyFrame 節點不容易 技術 (由於歷史因素,目前我們已使用 Java 沒有輕量的執行緒,也沒有連續性)。

相反地,Bazel 會使用固定大小的執行緒集區。不過,這也代表如果節點宣告的依附元件尚未可用,我們可能必須在依附元件可用時終止該評估,並重新啟動 (可能在另一個執行緒中)。這反過來說,節點不應過度執行這項操作;以序宣告 N 個依附元件的節點可能會重新啟動 N 次,耗費 O(N^2) 的時間。相反地,我們的目標是 依附元件 有時需要重新整理程式碼 限制重新啟動次數。

請注意,這項技術目前無法在規則 API 中使用;相反地,規則 API 仍是使用載入、分析和執行階段的舊版概念定義。不過,最基本的限制是 必須先完成架構才能追蹤 對應的依附元件無論建構系統實作或撰寫規則所使用的語言為何 (兩者不一定相同),規則作者都不得使用會略過 Skyframe 的標準程式庫或模式。若是 Java,意味著避免使用 java.io.File 和 和兩者的任何程式庫支援這些低階介面依附元件注入功能的程式庫,仍需要正確設定 Skyframe。

這強烈建議避免將規則作者暴露在全語言執行階段中 一開始就做出決定意外使用這類 API 的危險範圍極大: 過去有幾個 Bazel 錯誤是由使用不安全的 API 的規則所導致, 但這些規則是由 Bazel 團隊或其他領域專家撰寫。

避免平方時間和記憶體用量很困難

更糟的是,除了 Skyframe 強制規定的要求、使用 Java 的歷史限制,以及規則 API 過時的問題外,在任何以程式庫和二進位規則為基礎的建構系統中,不小心引入平方時間或記憶體消耗量,都是基本問題。這裡共有兩個 造成二次記憶體消耗量的常見模式 二次耗用的時間)。

  1. 圖書館規則鏈結 - 以程式庫規則 A 的鏈結為例,依附於 B、依附於 C,且 依此類推然後,我們要計算 例如 Java 執行階段類別路徑 每個程式庫。簡單來說,我們可能會採用標準名單導入方式。不過 這已造成二次型記憶體消耗量:第一個程式庫 包含一個項目,在類別路徑中,第二項和第三個三項等等 開啟,總共有 1+2+3...+N = O(N^2) 個項目。

  2. 二元規則取決於相同程式庫規則: 假設有一組二進位檔都依附於同一個程式庫 例如,如有多個測試規則 程式庫程式碼假設在 N 項規則中,有一半的規則是二元規則。 其他半型程式庫規則現在,假設每個二進位檔都會 針對程式庫規則的遞移性關閉計算所計算的部分屬性,例如 Java 執行階段類別路徑,或 C++ 連結器指令列舉例來說,它可以展開 C++ 連結動作的指令列字串表示法。不適用 N/2 元素的複本是 O(N^2) 記憶體。

自訂集合類別,避免二次複雜

Bazel 主要是對這兩種情況造成影響,因此我們推出一組 可以藉由自訂的集合類別,有效壓縮記憶體中的資訊 避免在各個步驟中複製內容這些資料結構幾乎全都 稱為語意 depset (在內部實作中也稱為 NestedSet)。大部分 可減少 Bazel 過去幾年的記憶體用量 改用 Depset 來取代先前使用過的內容

很抱歉,使用 depset 無法自動解決所有問題;特別是,即使只是在每個規則中重複執行 depset,也會導致二次平方的時間耗用。在內部,NestedSet 也有一些輔助方法 以促進與一般集合類別互通;很遺憾 不小心將 NestedSet 傳送到其中一種方法,導致 並重新引入二次記憶體消耗量