依赖编译_开源项目的编译优化实践
Zilliz 公司以 “重新定義數(shù)據(jù)科學(xué)” (Reinvent?Data Science)為愿景,專注于研發(fā)利用新一代異構(gòu)計(jì)算的開(kāi)源數(shù)據(jù)科學(xué)軟件。隨著各項(xiàng)目的蓬勃發(fā)展,我們對(duì)于持續(xù)集成、持續(xù)交付、持續(xù)部署(CI/CD)都提出了更高的要求。本文是 CI/CD 系列的開(kāi)篇,重點(diǎn)介紹持續(xù)集成的編譯優(yōu)化實(shí)踐。?
| 問(wèn)題與挑戰(zhàn)
在編譯構(gòu)建過(guò)程中我們遇到以下幾個(gè)問(wèn)題:
1) 編譯時(shí)間較長(zhǎng)
項(xiàng)目每天都要完成上百次的代碼集成,面對(duì)幾十萬(wàn)行的代碼量,開(kāi)發(fā)人員進(jìn)行小的 feature 改動(dòng)都有可能會(huì)導(dǎo)致工程的全量編譯,需要花費(fèi)超過(guò)一個(gè)小時(shí)或者更長(zhǎng)時(shí)間,顯然讓人難以接受。
2) 編譯環(huán)境復(fù)雜
項(xiàng)目代碼在不同的操作系統(tǒng)(CentOS、Ubuntu 等)、底層依賴庫(kù)(GCC、LLVM、CUDA 等)、硬件架構(gòu)等環(huán)境下進(jìn)行編譯,并且各編譯環(huán)境下生成的編譯產(chǎn)物都很有可能無(wú)法在其他平臺(tái)下使用。
3) 項(xiàng)目依賴關(guān)系復(fù)雜
當(dāng)前項(xiàng)目編譯所涉及的各功能組件依賴以及第三方依賴不下三四十個(gè),項(xiàng)目發(fā)展時(shí)常帶來(lái)依賴關(guān)系的變動(dòng),難免會(huì)遇到依賴沖突問(wèn)題。依賴之間的版本控制過(guò)于復(fù)雜,更新依賴版本容易導(dǎo)致影響其他組件業(yè)務(wù)。
4) 第三方依賴下載緩慢或無(wú)法下載
網(wǎng)絡(luò)延遲或者第三方依賴倉(cāng)庫(kù)不穩(wěn)定等問(wèn)題所導(dǎo)致資源下載緩慢或訪問(wèn)失敗,嚴(yán)重影響代碼集成構(gòu)建。
| 主要思路
對(duì)項(xiàng)目的依賴關(guān)系進(jìn)行解耦。將依賴關(guān)系復(fù)雜的組件進(jìn)行拆分,通過(guò)不同的倉(cāng)庫(kù)進(jìn)行版本管理,以配置文件的形式來(lái)組織各組件的版本信息、編譯選項(xiàng)、依賴關(guān)系等信息。配置文件加入到組件倉(cāng)庫(kù)進(jìn)行版本管理,隨著項(xiàng)目迭代進(jìn)行更新。
實(shí)現(xiàn)組件間的編譯優(yōu)化。根據(jù)配置文件所記錄的依賴關(guān)系、編譯選項(xiàng)等信息去拉取相關(guān)組件代碼進(jìn)行編譯,編譯后生成的二進(jìn)制產(chǎn)物以及對(duì)應(yīng)編譯產(chǎn)物的歸檔清單進(jìn)行統(tǒng)一標(biāo)記打包,上傳到私有倉(cāng)庫(kù)進(jìn)行集中存儲(chǔ)。在組件以及該組件所依賴的其他組件未發(fā)生改動(dòng)時(shí),通過(guò)歸檔清單對(duì)編譯產(chǎn)物進(jìn)行回放,起到了編譯緩存效果。網(wǎng)絡(luò)延遲或者第三方依賴倉(cāng)庫(kù)不穩(wěn)定等問(wèn)題,可通過(guò)在內(nèi)部搭建私有化倉(cāng)庫(kù)或者使用多鏡像倉(cāng)庫(kù)去解決。
實(shí)現(xiàn)組件內(nèi)部的編譯優(yōu)化。選擇針對(duì)于特定語(yǔ)言的編譯緩存工具,將編譯過(guò)程中的編譯產(chǎn)物進(jìn)行緩存并打包上傳到私有倉(cāng)庫(kù)進(jìn)行集中存儲(chǔ)。舉個(gè)例子,就 C/C++ 編譯而言,可以選擇 CCache 這類編譯緩存工具來(lái)緩存 C/C++ 編譯的中間產(chǎn)物,編譯完成后對(duì) CCache 本地緩存進(jìn)行歸檔上傳。諸如此類的編譯緩存工具只是對(duì)發(fā)生改動(dòng)的代碼文件進(jìn)行編譯后逐一緩存,對(duì)未發(fā)生變動(dòng)的代碼文件命中對(duì)應(yīng)的編譯產(chǎn)物進(jìn)行拷貝,使得它可以直接參與到最終編譯。
保證編譯環(huán)境一致性。由于編譯產(chǎn)物的生成對(duì)于系統(tǒng)環(huán)境變化較為敏感,在不同的操作系統(tǒng)、底層依賴庫(kù)等環(huán)境下都可能會(huì)出現(xiàn)未知的錯(cuò)誤,因此我們需要根據(jù)系統(tǒng)環(huán)境變化對(duì)編譯產(chǎn)物緩存進(jìn)行標(biāo)記歸檔。我們所接觸的系統(tǒng)環(huán)境千差萬(wàn)別,很難通過(guò)某幾個(gè)維度對(duì)其進(jìn)行歸類,因此我們引入了容器化技術(shù),統(tǒng)一編譯環(huán)境,從而解決這類問(wèn)題。
| 實(shí)施關(guān)鍵點(diǎn)
- 項(xiàng)目依賴關(guān)系解耦
對(duì)于項(xiàng)目依賴關(guān)系的解耦,并沒(méi)有一個(gè)統(tǒng)一的定義。項(xiàng)目?jī)?nèi)部依賴關(guān)系通常是根據(jù)業(yè)務(wù)需求、技術(shù)棧選型、部署方式等方面去考慮的。項(xiàng)目外部依賴關(guān)系通常是根據(jù)第三方依賴庫(kù)與內(nèi)部組件的依賴性來(lái)確定的。在組件間存在編譯方式、編譯選項(xiàng)等方面有強(qiáng)依賴關(guān)系的第三方依賴庫(kù),選擇連同組件業(yè)務(wù)代碼一起編譯。對(duì)項(xiàng)目中能夠共用的第三方依賴庫(kù),形成統(tǒng)一的獨(dú)立倉(cāng)庫(kù)進(jìn)行集中編譯。
- 組件間編譯優(yōu)化
對(duì)于組件間的工程編譯優(yōu)化分為以下工作:
1. 開(kāi)發(fā)人員提交修改的組件業(yè)務(wù)代碼觸發(fā)項(xiàng)目的代碼集成,獲取該組件倉(cāng)庫(kù)中的配置文件,根據(jù)依賴關(guān)系獲取上下游依賴組件的版本信息(Git Branch or Tag、Git Commit ID)和編譯選項(xiàng)等信息,構(gòu)建依賴關(guān)系圖。
2. 依賴關(guān)系檢查。針對(duì)組件之間出現(xiàn)的循環(huán)依賴、版本沖突等問(wèn)題進(jìn)行報(bào)警。
3. 依賴關(guān)系扁平化處理。依賴關(guān)系圖進(jìn)行深度優(yōu)先遍歷(DFS)排序,重復(fù)依賴的組件實(shí)現(xiàn)前置合并。
4. 對(duì)每個(gè)組件的版本信息、編譯選項(xiàng)等信息生成一個(gè)哈希值,再通過(guò) MerkleTree 算法生成包含有該組件依賴關(guān)系的加密哈希值(Root Hash),該加密哈希值與組件名稱等信息組合成為該組件的唯一標(biāo)簽信息。
5. 根據(jù)組件的唯一標(biāo)簽信息去檢查私有倉(cāng)庫(kù)是否存在該組件的編譯產(chǎn)物歸檔文件。如果命中已存在的編譯產(chǎn)物歸檔文件,則解壓編譯產(chǎn)物歸檔文件,獲取歸檔清單文件進(jìn)行編譯產(chǎn)物的回放;如未命中,則對(duì)組件進(jìn)行編譯,生成的編譯產(chǎn)物和清單文件進(jìn)行標(biāo)記歸檔并上傳至私有倉(cāng)庫(kù)。
- 組件內(nèi)部編譯優(yōu)化
對(duì)于組件內(nèi)部的工程編譯優(yōu)化分為以下工作:
1. 將編譯組件代碼所需系統(tǒng)環(huán)境依賴加入到 Dockerfile。通過(guò) Hadolint 工具對(duì) Dockerfile 進(jìn)行合規(guī)檢查,確保鏡像符合 Docker 的最佳實(shí)踐。
2. 根據(jù)項(xiàng)目迭代版本號(hào)(項(xiàng)目版本號(hào) + 構(gòu)建版本號(hào))、操作系統(tǒng)等版本信息進(jìn)行編譯環(huán)境鏡像的構(gòu)建。
3. 通過(guò)鏡像啟動(dòng)用于構(gòu)建編譯環(huán)境的容器,并將鏡像 ID 通過(guò)環(huán)境變量的形式傳入到容器中。獲取鏡像 ID 命令如 “ docker inspect '--type=image' --format '{{.ID}}' repository/build-env:v0.1-centos7 ”。
4. 針對(duì)技術(shù)棧選擇合適的編譯緩存工具對(duì)代碼進(jìn)行編譯緩存。進(jìn)入到容器內(nèi)部進(jìn)行代碼集成與編譯,根據(jù)鏡像 ID 去檢查私有倉(cāng)庫(kù)是否存在針對(duì)于編譯緩存工具的編譯緩存。如果命中已存在的編譯緩存,則直接下載并解壓到指定目錄。編譯環(huán)境下的所有組件都編譯完成后,再將編譯緩存工具生成的編譯緩存通過(guò)項(xiàng)目迭代版本號(hào)、鏡像 ID 等信息統(tǒng)一標(biāo)記打包并更新上傳至私有倉(cāng)庫(kù)。
- 構(gòu)建方案再優(yōu)化
最初我們構(gòu)建的鏡像體積過(guò)于臃腫,增加了磁盤和網(wǎng)絡(luò)資源開(kāi)銷,還使得部署時(shí)間越來(lái)越長(zhǎng)。對(duì)此我們有以下幾點(diǎn)建議:
1. 選擇最精簡(jiǎn)的基礎(chǔ)鏡像來(lái)降低鏡像體積,如:alpine、busybox 等。
2. 減少鏡像層數(shù)。所需的環(huán)境依賴盡量做到能夠復(fù)用。合并指令,可以用 "&&" 將多條命令連接起來(lái)。
3. 清理鏡像構(gòu)建的中間產(chǎn)物。
4. 充分利用鏡像緩存構(gòu)建。
方案實(shí)施一段時(shí)間后,隨著編譯緩存增加導(dǎo)致私有化倉(cāng)庫(kù)的磁盤和網(wǎng)絡(luò)資源開(kāi)銷加大,并且部分編譯緩存利用率不高。對(duì)此我們有以下幾點(diǎn)建議:
1. 定期清理緩存文件。通過(guò)腳本等形式對(duì)私有化倉(cāng)庫(kù)進(jìn)行定期檢查,對(duì)于一段時(shí)間未發(fā)生變動(dòng)且下載量不高的緩存文件進(jìn)行清理。
2. 有選擇的進(jìn)行編譯緩存。對(duì)于編譯所需資源開(kāi)銷較小的代碼,可不進(jìn)行編譯緩存。由于 Docker 的安裝與使用、私有化倉(cāng)庫(kù)搭建等內(nèi)容不在本章討論的范疇,感興趣的同學(xué)可以自行研究。
| 總結(jié)與展望
本文從自身項(xiàng)目依賴關(guān)系出發(fā)進(jìn)行分析,詳細(xì)介紹了組件間與組件內(nèi)部的編譯優(yōu)化方法,并提供了構(gòu)建穩(wěn)定高效的代碼持續(xù)集成系統(tǒng)的思路與最佳實(shí)踐方案。解決了依賴關(guān)系復(fù)雜所帶來(lái)的項(xiàng)目迭代緩慢問(wèn)題,統(tǒng)一在容器內(nèi)部操作以保證環(huán)境的一致性,通過(guò)對(duì)編譯產(chǎn)物的回放以及編譯緩存工具的緩存來(lái)提升編譯效率。
目前該實(shí)踐方案已在Milvus 等產(chǎn)品的持續(xù)集成中提供相應(yīng)的技術(shù)支持。采用了本文所描述的工作進(jìn)行編譯優(yōu)化后,項(xiàng)目工程的編譯時(shí)間平均減少了 60%,極大地提升了項(xiàng)目構(gòu)建效率。后續(xù)我們會(huì)對(duì)于組件間與組件內(nèi)部的編譯并行化進(jìn)行探究,持續(xù)為數(shù)據(jù)科學(xué)領(lǐng)域的發(fā)展進(jìn)行賦能。
|歡迎加入 Milvus 社區(qū)
http://github.com/milvus-io/milvus | 源碼
http://milvus.io | 官網(wǎng)
http://milvusio.slack.com | Slack 社區(qū)
http://zhihu.com/org/zilliz-11/columns | 知乎
http://zilliz.blog.csdn.net | CSDN 博客
http://space.bilibili.com/478166626 | Bilibili
總結(jié)
以上是生活随笔為你收集整理的依赖编译_开源项目的编译优化实践的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 富士康:2 月收入同比下降 11.65%
- 下一篇: 阶梯式存款法