Google分布式構建軟件之二:構建系統如何工作


分布式軟件構建第二部分:構建系統如何工作
注:本文英文原文在google開發者工具組的博客上[需要翻牆],以下是我的翻譯,歡迎轉載,但請尊重作者版權,注名原文地址。
上篇文章中提到了在Google,所有的產品都是從頭開始構建的。這篇文章會更深入的介紹Google的構建系統[即Blaze]是如何工作的,並介紹讓軟件構建過程更快的方法。在后續的文章里,我們會解釋如何利用這種確定的信息來在大規模集群之上進行分布式的軟件構建並在開發者之間共享構建結果。

問題:Google是如何描述驅動構建和測試的依賴關系的呢?

在Google我們把代碼划分成叫做包[package]的小單元。可以把包理解為一個目錄,這個目錄里面包含了源文件和一個描述文件,描述文件中指定了如何將源文件轉換成構建的輸出。這個描述文件叫做 BUILD,一個目錄中存在這個BUILD文件,就可以把這個目錄當作一個包。

所有的包都在同一個文件樹下面,使用從文件樹的根到包含BUILD文件的目錄的相對路徑來做為這個包的全局唯一的標示。這說明包名和目錄名之間是一一對應的關系。

在BUILD文件中我們用規則[rules]來描述構建后包的輸出。使用包名和規則名稱可以唯一的標示這條規則。我們把這兩者的結合叫做標簽[label],我們使用標簽來描述規則之間的依賴關系。

來看一個具體例子:

/search/BUILD:
cc_binary(name = ‘google_search_page’,
       deps = [ ‘:search’,
 	              ‘:show_results’])

cc_library(name = ‘search’,
       srcs = [ ‘search.h’,‘search.cc’],
       deps = [‘//index:query’])

/index/BUILD:

cc_library(name = ‘query’,
       srcs = [ ‘query.h’, ‘query.cc’, ‘query_util.cc’],
       deps = [‘:ranking’,
               ‘:index’])

cc_library(name = ‘ranking’,
       srcs = [‘ranking.h’, ‘ranking.cc’],
       deps = [‘:index’, 
               ‘//storage/database:query’])

cc_library(name = ‘index’,
       srcs = [‘index.h’, ‘index.cc’],
       deps = [‘//storage/database:query’]) 

這個例子展示了兩個BUILD文件。第一個BUILD文件描述了//search 這個包,包含一個可執行文件和一個庫。第二個BUILD文件描述了包含幾個庫的//index包。name屬性來命名規則,deps屬性來描述規則之間的依賴關系。使用冒號來分隔包名和規則名。如果某條規則所依賴的規則在其他目錄下,就用"//"開頭,如果在同一目錄下,可以忽略包名而用冒號開頭。這樣可以清晰的看到各個規則之間的依賴關系。如果你仔細看了上面的例子,可以看到幾個規則都依賴於//storage/database:query 這個規則,說明依賴之間構成了一個有向圖。這個有向圖必須是無環的,這樣我們就可以梳理出構建各個目標的順序了。

下圖是上面描述的依賴關系的一個圖形化展示:

graphical_dep

可以看到,跟基於make的構建系統不同,我們引用的是抽象的實體而非具體構建的輸出結果。實際上,cc_library 這樣的規則並不需要產出任何輸出,它就是一個簡單的從邏輯上組織源文件的方式。為了讓我們的構建系統以更快的速度構建,這一點尤其重要。盡管各個cc_library規則之間有依賴關系,但我們可以用任意順序編譯所有的源文件。編譯 'query.cc'和'query_util.cc'時,我們只需要依賴頭文件'ranking.h'和'index.h'。實際上我們可以同時編譯這些源文件,除非我們依賴於其他規則的輸出文件。

為了真正執行構建所需的步驟,我們把每個規則分解成一個或多個實際的步驟,稱之為"行為"[actions]. 可以把行為理解為一條命令和輸入/輸出文件。一個行為的輸出可以是另外一個行為的輸入。這樣所有的行為就形成了由行為和文件組成的二分圖。為了構建目標所需要做的就是從這個二分圖的葉子節點--那些不需要任何行為來創建就已經存在的源文件--遍歷到根節點並順序執行這些行為。這保證在執行一個行為之前,相關的所有輸入文件都已經就位了。

一個小規模目標組成的行為二分圖的例子如下,行為是綠色,文件是黃色。
bipartite_action_graph

同時我們確保每個行為對應的命令行參數中的文件名稱都使用的是相對路徑--源文件使用從源文件目錄層級中根目錄開始的相對路徑。同時由於我們知道一個行為所有的輸入和輸出文件,所以可以很容易的遠程執行任何的行為:只需要把所有的輸入文件拷貝到遠程機器,然后在遠程機器上執行命令,再把輸出文件拷貝回用戶機器。后續的文章中我們會介紹一些使遠程構建更高效的方法。

所有的這些跟 make 所做的事情沒有太大區別。最重要的區別是我們不是以文件為單元來確定依賴關系,這就讓構建系統在決定行為的執行順序時,有更大的自由度。在一次構建過程中並發度越高(即行為二分圖寬度越寬),執行時間就越短。在Google,通過簡單的增加機器就可以擴展構建系統。如果我們數據中心的機器足夠多,那么一次構建的時間就主要由行為二分圖的高度來決定。

上面描述的是在Google,一次干凈的構建是如何進行的。在實際中大多數開發過程中的構建是增量的。這意味着一個開發人員在兩次構建中間只修改了小部分源文件,這種情況下構建整個行為圖是很浪費的,所以我們只執行跟上次構建相比,輸入文件發生變化的的行為。為了達到這個目標,我們跟蹤了每次執行行為時輸入文件的內容摘要。上篇博客中提到,我們跟蹤源文件的內容摘要並使用這個摘要來跟蹤對文件的修改。前文描述的FUSE文件系統把每個文件的摘要當作一個擴展的屬性。這種設計下構建系統很容易就能拿到一個文件的摘要並且能夠讓我們不用執行那些輸入文件沒有改變的行為。

本系列的第三部分會介紹如何使遠程執行真正的高效甚至讓它提高構建的速度!

PS:從英文原文中看到的一些回復:

根據BUILD文件產生Makefile是很容易的,而從Makefile推導出BUILD文件是幾乎不可能的。因為BUILD文件提供的依賴是更高級別的抽象單元(目標和包),這是很難從基於文件的依賴關系中推倒出來的。

Gilbert說,文中提到的構建系統,會有兩個問題:

  1. 把實際不需要的依賴庫包含進來,除非用工具來自動化的檢查
  2. 在低級別的模塊中庫的使用會有效的聚合依賴,但是它也會讓依 賴圖不准確,這就會導致過度構建--例如,你修改了一個庫的目標文 件,那么依賴於這個庫的所有的可執行文件都需要重新鏈接,但實際上 這些可執行文件可能並不需要這個目標文件。

nyork的答復是:
1確實是一個問題,在問題2的場景下,確實是一種潛在的過度構建。
構建系統和基礎設施是跟語言無關的,這是一個非常重要的一點。構建系統應該能夠支持C++,Java, shell腳本等。唯一的要求就是執行所需要的底層工具(編譯器,連接器等)必須遵守確定的規則(后續的文章會有更多介紹)。完整的定義依賴關系,構建系統就不需要理解每個語言的語義了,這就意味者我們不需要花費CPU時間(和IO)來在工作站上讀取並解析源代碼。

后續的文章介紹了我們如何使用BUILD文件提供的信息來讓構建過程尤其快速和高效,這也說明了為什么相比而言,過度構建根本就不是問題。

回到本系列目錄


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM