開源項目的編譯優化實踐

Zilliz 公司以 “重新定義數據科學” (Reinvent Data Science)爲願景,專注於研發利用新一代異構計算的開源數據科學軟件。隨着各項目的蓬勃發展,我們對於持續集成、持續交付、持續部署(CI/CD)都提出了更高的要求。本文是 CI/CD 系列的開篇,重點介紹持續集成的編譯優化實踐。

 

| 問題與挑戰

在編譯構建過程中我們遇到以下幾個問題:

1) 編譯時間較長

項目每天都要完成上百次的代碼集成,面對幾十萬行的代碼量,開發人員進行小的 feature 改動都有可能會導致工程的全量編譯,需要花費超過一個小時或者更長時間,顯然讓人難以接受。

2) 編譯環境複雜

項目代碼在不同的操作系統(CentOS、Ubuntu 等)、底層依賴庫(GCC、LLVM、CUDA 等)、硬件架構等環境下進行編譯,並且各編譯環境下生成的編譯產物都很有可能無法在其他平臺下使用。

3) 項目依賴關係複雜

當前項目編譯所涉及的各功能組件依賴以及第三方依賴不下三四十個,項目發展時常帶來依賴關係的變動,難免會遇到依賴衝突問題。依賴之間的版本控制過於複雜,更新依賴版本容易導致影響其他組件業務。

4) 第三方依賴下載緩慢或無法下載

網絡延遲或者第三方依賴倉庫不穩定等問題所導致資源下載緩慢或訪問失敗,嚴重影響代碼集成構建。

 

| 主要思路

對項目的依賴關係進行解耦。將依賴關係複雜的組件進行拆分,通過不同的倉庫進行版本管理,通過配置文件的形式來組織各組件的版本信息、編譯選項、依賴關係等信息。配置文件加入到組件倉庫進行版本管理,隨着項目迭代進行更新。

實現組件間的編譯優化。根據配置文件所記錄的依賴關係、編譯選項等信息去拉取相關組件代碼進行編譯,編譯後生成的二進制產物以及對應編譯產物的歸檔清單進行統一標記打包,上傳到私有倉庫進行集中存儲。在組件以及該組件所依賴的其他組件未發生改動時,通過歸檔清單對編譯產物進行回放,起到了編譯緩存效果。網絡延遲或者第三方依賴倉庫不穩定等問題,可通過在內部搭建私有化倉庫或者使用多鏡像倉庫去解決。

實現組件內部的編譯優化。選擇針對於特定語言的編譯緩存工具,將編譯過程中的編譯產物進行緩存並打包上傳到私有倉庫進行集中存儲。舉個例子,就 C/C++ 編譯而言,可以選擇 CCache 這類編譯緩存工具來緩存 C/C++ 編譯的中間產物,編譯完成後對 CCache 本地緩存進行歸檔上傳。諸如此類的編譯緩存工具只是對發生改動的代碼文件進行編譯後逐一緩存,對未發生變動的代碼文件命中對應的編譯產物進行拷貝,使得它可以直接參與到最終編譯。

保證編譯環境一致性。由於編譯產物的生成對於系統環境變化較爲敏感,在不同的操作系統、底層依賴庫等環境下都可能會出現未知的錯誤,因此我們需要根據系統環境變化對編譯產物緩存進行標記歸檔。我們所接觸的系統環境千差萬別,很難通過某幾個維度對其進行歸類,因此我們引入了容器化技術,統一編譯環境,從而解決這類問題。

 

| 實施關鍵點

  • 項目依賴關係解耦

對於項目依賴關係的解耦,並沒有一個統一的定義。項目內部依賴關係通常是根據業務需求、技術棧選型、部署方式等方面去考慮的。項目外部依賴關係通常是根據第三方依賴庫與內部組件的依賴性來確定的。在組件間存在編譯方式、編譯選項等方面有強依賴關係的第三方依賴庫,選擇連同組件業務代碼一起編譯。對項目中能夠共用的第三方依賴庫,形成統一的獨立倉庫進行集中編譯。

 

  • 組件間編譯優化

對於組件間的工程編譯優化分爲以下工作:

1. 開發人員提交修改的組件業務代碼觸發項目的代碼集成,獲取該組件倉庫中的配置文件,根據依賴關係獲取上下游依賴組件的版本信息(Git Branch or Tag、Git Commit ID)和編譯選項等信息,構建依賴關係圖。

2. 依賴關係檢查。針對組件之間出現的循環依賴、版本衝突等問題進行報警。

3. 依賴關係扁平化處理。依賴關係圖進行深度優先遍歷(DFS)排序,重複依賴的組件實現前置合併。

4. 對每個組件的版本信息、編譯選項等信息生成一個哈希值,再通過 MerkleTree 算法生成包含有該組件依賴關係的加密哈希值(Root Hash),該加密哈希值與組件名稱等信息組合成爲該組件的唯一標籤信息。 

5. 根據組件的唯一標籤信息去檢查私有倉庫是否存在該組件的編譯產物歸檔文件。如果命中已存在的編譯產物歸檔文件,則解壓編譯產物歸檔文件,獲取歸檔清單文件進行編譯產物的回放;如未命中,則對組件進行編譯,生成的編譯產物和清單文件進行標記歸檔並上傳至私有倉庫。

 

  • 組件內部編譯優化

 

對於組件內部的工程編譯優化分爲以下工作:

1. 將編譯組件代碼所需系統環境依賴加入到 Dockerfile。通過 Hadolint 工具對 Dockerfile 進行合規檢查,確保鏡像符合 Docker 的最佳實踐。

2. 根據項目迭代版本號(項目版本號 + 構建版本號)、操作系統等版本信息進行編譯環境鏡像的構建。

3. 通過鏡像啓動用於構建編譯環境的容器,並將鏡像 ID 通過環境變量的形式傳入到容器中。獲取鏡像 ID 命令如 “ docker inspect '--type=image' --format '{{.ID}}' repository/build-env:v0.1-centos7 ”。

4. 針對技術棧選擇合適的編譯緩存工具對代碼進行編譯緩存。進入到容器內部進行代碼集成與編譯,根據鏡像 ID 去檢查私有倉庫是否存在針對於編譯緩存工具的編譯緩存。如果命中已存在的編譯緩存,則直接下載並解壓到指定目錄。編譯環境下的所有組件都編譯完成後,再將編譯緩存工具生成的編譯緩存通過項目迭代版本號、鏡像 ID 等信息統一標記打包並更新上傳至私有倉庫。

 

  • 構建方案再優化

 

最初我們構建的鏡像體積過於臃腫,增加了磁盤和網絡資源開銷,還使得部署時間越來越長。對此我們有以下幾點建議:

1. 選擇最精簡的基礎鏡像來降低鏡像體積,如:alpine、busybox 等。

2. 減少鏡像層數。所需的環境依賴儘量做到能夠複用。合併指令,可以用 "&&" 將多條命令連接起來。

3. 清理鏡像構建的中間產物。

4. 充分利用鏡像緩存構建。

 

方案實施一段時間後,隨着編譯緩存增加導致私有化倉庫的磁盤和網絡資源開銷加大,並且部分編譯緩存利用率不高。對此我們有以下幾點建議:

1. 定期清理緩存文件。通過腳本等形式對私有化倉庫進行定期檢查,對於一段時間未發生變動且下載量不高的緩存文件進行清理。

2. 有選擇的進行編譯緩存。對於編譯所需資源開銷較小的代碼,可不進行編譯緩存。

 

由於 Docker 的安裝與使用、私有化倉庫搭建等內容不在本章討論的範疇,感興趣的同學可以自行研究。

 

| 總結與展望

本文從自身項目依賴關係出發進行分析,詳細介紹了組件間與組件內部的編譯優化方法,並提供了構建穩定高效的代碼持續集成系統的思路與最佳實踐方案。解決了依賴關係複雜所帶來的項目迭代緩慢問題,統一在容器內部操作以保證環境的一致性,通過對編譯產物的回放以及編譯緩存工具的緩存來提升編譯效率。

目前該實踐方案已在 Milvus 等產品的持續集成中提供相應的技術支持。採用了本文所描述的工作進行編譯優化後,項目工程的編譯時間平均減少了 60%,極大地提升了項目構建效率。後續我們會對於組件間與組件內部的編譯並行化進行探究,持續爲數據科學領域的發展進行賦能。

 

| 歡迎加入 Milvus 社區

github.com/milvus-io/milvus | 源碼

milvus.io | 官網

milvusio.slack.com | Slack 社區

zhihu.com/org/zilliz-11/columns | 知乎

zilliz.blog.csdn.net | CSDN 博客

space.bilibili.com/478166626 | Bilibili

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章