注:本文英文原文在google開發者工具組的博客上[需要FQ],以下是我的翻譯,歡迎轉載,但請尊重作者版權,注名原文地址。
之前兩篇文章分別介紹了Google 分布式軟件構建系統Blaze相關的為了提供對存儲在雲端的源碼的訪問支持而定制的文件系統和構建系統是如何工作的。這篇文章在前兩篇文章的基礎之上介紹了一個在大規模集群上面分布式高效率執行構建步驟的系統[譯者注:就是Blaze]。正如你看到的,源文件系統和構建系統的細節對於我們實現快速高效的分布式構建是非常重要的。所以在介紹構建步驟如何分布式執行的機制之前,來關注幾個重點。
首先,構建系統是基於內容的,系統內部對於輸入和輸出是通過基於內容的摘要來標記的,而不是文件和時間戳(例如Make)。這意味者通過比較內容摘要就能知道內容是否是相同的,構建系統在根據行為圖來執行構建操作時,會把這些摘要在內部記錄下來。在構建過程中為大量的源代碼計算內容摘要會很耗時,主要時間都花在讀取文件上。通過在內容提交的時候計算並存儲摘要就可以避免這個問題,之后直接通過文件擴展屬性來提供給構建系統。
另外,構建系統通過讀取BUILD文件來構建一張依賴關系的圖,然后使用依賴關系圖去構建一個 構建行為 或構建步驟的圖。通過使用行為的輸出作為其他行為的輸入來構建這個依賴關系圖。依賴關系必須是完整指定的,而且不能有動態的依賴檢測。內容摘要和指定的完整依賴意味着行為可以通過函數來表示。在這個 函數模型中 ,函數的輸入是內容摘要和環境(環境變量,命令行選項),函數是把輸入轉化成輸出的工具或腳本,函數執行結果就是輸出。這些函數式的構建行為是構建工作的原子單元,從輸入轉換到輸出是對系統透明的。這意味着構建行為是不用知道語言和工具的,例如,可能是編譯C++單元,編譯java文件,鏈接成一個可執行文件,甚至是跑一個單測。
下面是一個行為圖的舉例。行為的輸出,例如CC行為輸出是search.o,成為其他行為(本例子中是LD)的輸入。
第三,我們需要每個構建行為只能使用它顯示聲明的輸入。這意味者同樣的行為,使用同樣的輸入執行多次,結果在二進制級別是相同的。這保證了兩次構建時的輸入內容如果完全相同,那就不需要再去執行本次構建行為,因為構建結果不會改變。這似乎聽起來是合理的,但實際上一個行為可能依賴於除了顯示聲明的輸入文件之外的東西,例如系統頭文件的內容或者是當天的時間(考慮下由C語言預處理展開的__DATE__宏,或者是每個jar文件中嵌入的時間戳)。我們讓工具來執行 不受外界影響 的行為並對一些文件類型做預處理來覆蓋時間戳來避免這個問題。
現在我們繼續介紹如何使用這些函數式的行為來執行分布式構建
每個構建行為是自給自足的,原子的,所以是 便攜的,可以帶着輸入發送到其他機器上執行。這對於Google很有意義,因為我們正好在數據中心有很多機器,意味着我們可以把構建行為分發到成千上萬台機器上去執行。這種模型下所有的構建行為都可以分布式執行,並發度只受行為樹的寬度限制。
在很多機器上分布式的執行構建行為使得構建變快,但是我們發現機器上執行的工作都是重復的,因為很多開發人員構建的代碼都是相同的。構建行為自身的函數式特性--相同的輸入條件下,輸出也是一模一樣的--意味者我們可以很容易並正確地緩存和復用構建結果。我們計算整個請求(命令行和輸入)的摘要來做為緩存的鍵,所以不可能“疏忽”了某些東西而錯誤的命中緩存結果。還記得輸入文件是通過內容摘要來描述的嗎?這表示即使對於巨大的內容,計算緩存的鍵也是相對容易的。當構建行為已經准備好遠程執行時,首先計算緩存的鍵。如果沒有命中緩存,這個行為會被執行並會在結果返回給用戶的時候進行緩存。當命中緩存,就直接使用緩存中的結果。為了讓緩存的結果看起來是真正執行過的,我們也會把標准輸入和標准錯誤輸出也緩存起來然后進行回放。
當改動提交到代碼源里,進行第一次構建時,每個改動點影響的行為會耗時久一些,因為這些行為需要重新執行,但因為構建行為是分布式並發執行的,所以這種耗時並不是很顯著的。在很多情況下,例如C++中空格和注釋的改動,這種不同的輸入仍然產生二進制級別上相同的結果。由於構建系統是基於內容的,所以這種情況會導致后續的行為依然能夠命中緩存,所以提供了另外一種避免重復構建的方法。整體來說,最后緩存命中率超過90%。這意味即使是“干凈”地重新構建也是大部分利用的之前構建的結果,所以會非常快。也可以這樣理解,這些代碼的改動沒有影響到最終發布的庫和可執行文件。
分布式構建並重復利用構建行為是如此成功地加速了構建過程,以至於我們不可避免的遇到了另外的問題。一個大工程的干凈的構建可能會產生幾個G的輸出,這些構建通常只花費了數分鍾而我們每天構建上萬次,這導致分布式構建產生的數據對我們的網絡和本地磁盤I/O造成了相當大的壓力,本系列最后一篇會介紹我們是如何解決這個問題的。