知乎容器化构建系统设计和实践
知乎選用 Jenkins 作為構建方案,因其強大和靈活,且有非常豐富的插件可供使用和擴展。早期,應用數量較少時,每個開發者都手動創建并維護著幾個 Job,各自編寫 Jenkins Job 的配置,以及手動觸發構建。
? ?關于? ?
知乎應用平臺團隊基于 Jenkins Pipeline 和 Docker 打造了一套持續集成系統。Jenkins Master 和 Slave 基于 Docker 部署,每次構建也是在容器中進行。目前有三千個 Jenkins Job,支撐著整個團隊每日近萬次的構建和部署量。整個系統的設計目標是具備以下的能力:
-
較低的應用接入成本,較高的定制能力:寫一個構建系統配置文件成本要盡可能簡單方便,或者可以通過模板一鍵創建,但又要能滿足應用的各種定制化的需求。
-
具備語言開放性和部署多樣性:平臺需要能支撐業務技術選型上的多語言,同時,要能滿足應用不同的部署類型,如單純的打包發布,或者進一步部署到物理機、容器、離線任務平臺等。
-
構建快和穩定,復現問題成本低:每次構建都在干凈的容器中,減少非應用本身問題帶來的構建異常。同時,如果構建出現問題,在權限控制的前提下,要能方便開發者自己調試和排查。
-
推動業界標準以及最佳實踐,同時在代碼合并之前就能更好把控住質量。整個集群高可用,可擴展,以及具備較低的運維成本。
? ?背景? ?
知乎選用 Jenkins 作為構建方案,因其強大和靈活,且有非常豐富的插件可供使用和擴展。早期,應用數量較少時,每個開發者都手動創建并維護著幾個 Job,各自編寫 Jenkins Job 的配置,以及手動觸發構建。隨著服務化以及業務類型,開發者以及 Jenkins Job 數量的增加,我們面臨了以下的問題:
-
每個開發者都需要去理解 Jenkins 的基本配置和觸發邏輯,使得配置創建和維護成本高。
-
構建在物理機上進行,每個應用可能有著不同的版本依賴,構建時會遇到版本沖突,甚至上線之后發現行為不一致導致故障等。
-
構建一旦失敗,需要開發者能登錄 Jenkins Slave 所在的物理機進行調試,權限控制成為了一個問題
于是,一個能方便應用接入構建部署的系統,成為了必須。
完整的生命周期
知乎的構建工作流主要是以下兩種場景:
-
只有 Master 分支的代碼可以用于線上部署,但支持指定任意的分支進行構建
-
所有對 Master 分支的修改必須通過 Merge Request 來進行。為了避免潛在代碼沖突導致測試結果不準的情況,對 Merge Request 上的代碼進行構建前,會模擬跟 Master 分支的代碼做一次合并。
一個 Commit 從提交到最后部署,會經歷以下的環節:
開發者提交代碼到 GitLab。
GitLab 通過 Webhook 通知到 ZAE (Zhihu App Engine, 知乎的私有云平臺)。
ZAE 將構建的上下文信息,如 GitLab 倉庫 ID,ZAE 應用信息給到構建系統 Lavie。目前只處理用戶提交 MR 以及合并到 Master 分支的事件。
構建系統 Lavie 讀取應用倉庫中的配置文件后生成配置,觸發一個構建。在構建過程中獲取動態生成的 Jenkinsfile,生成 Dockerfile 構建出應用的鏡像,并跑起容器,在容器中執行構建,測試等應用指定的步驟。
測試成功之后,分別往物理機部署平臺,容器部署平臺,離線任務平臺上傳 Artifact,注冊待發布版本的信息,并 Slack 通知用戶結果。
構建結束,用戶在 ZAE 上可以進行后續操作,如選擇一個候選版本進行部署。
每個應用的拉取代碼,準備數據庫,處理測試覆蓋率,發送消息,候選版本的注冊等通用的部分,都會由構建系統統一處理,而接入構建系統的應用,只需要在代碼倉庫中包含一個約定格式的配置文件。構建系統會根據這個配置文件去動態生成 Jenkinsfile 和 Dockerfile 以完成后續的構建部署。
達到的目標以及中間遇到的問題
較低的接入成本,較高的定制能力
構建系統去理解應用要做的事情靠的是約定格式的 yaml 配置文件,而我們希望這個配置文件能足夠簡單,聲明上必要的部分,如環境、構建、測試步驟就能開始構建。
同時,也要有能力提供更多的定制功能讓應用可以使用,如選擇系統依賴和版本,緩存的路徑,是否需要構建系統提供 MySQL 以及需要的 MySQL 版本等。以及可以根據應用的類別自動生成配置文件。
一個最簡單的應用場景
base_image: python2/jessiebuild:- buildouttest:unittest:- bin/test --cover-package=pin --with-xunit --with-coverage --cover-xml一個更多定制化的場景
version: 2.0 base_image: py_node/jessie deps:- libffi-dev build:- buildout- cd admin && npm install && gulp test:deps:- mysql:5.7unittest:- bin/test --cover-package=lived,liveweb --with-xunit --with-coveragecoverage_test:report_fpath: coverage.xmlartifacts:targets:- docker- tarball cache:directories:- admin/static/components- admin/node_modulespost_build:scripts:- /bin/bash scripts/release_sentry.sh為了盡可能滿足多樣化的業務場景,我們主要將配置文件分為三部分:聲明環境和依賴、構建相關核心環節、聲明 Artifact 類型。
聲明環境和依賴
-
image,基礎鏡像,需要指明已提前準備好的語言鏡像
-
deps,dependencies 的簡寫, 聲明使用的系統依賴以及對應的版本
構建相關核心環節
-
build,構建的步驟,如 buildout, npm install ,或者執行一個腳本
-
test,測試環節,應用需要聲明構建的步驟,也可以在這里定制使用的 MySQL 以及對應的版本。構建系統會每次為其創建新的數據庫,將關鍵信息 export 為環境變量。
-
post build,最后一個環節,如發包,發 Slack 、郵件通知,或發布一個 Sentry release 等
聲明 Artifact 類型
artifact,用于選擇部署的類型, 目前支持的有:
-
tarball:構建系統會將整個應用 Workspace 打包上傳到 HDFS 用于后續的物理機部署
-
docker:鏡像會被 push 到私有的 Docker Registry 用于容器部署
-
static:應用指定的路徑打包后會被上傳到 HDFS,用于后續的靜態資源部署
-
offline: 應用指定的文件會被上傳到離線平臺,用于離線任務的執行
語言開放性
早期所有的構建都在物理機上進行,構建之前需要提前在物理機上安裝好對應的系統依賴,而如果遇到所需要的版本不同時,調度和維護的成本就高了很多。隨著團隊業務數量和種類的增加,技術選型的演進,這樣的挑戰越來越大。于是構建系統整體的優化方向由物理機向 Docker 容器化前進,如今,所有構建都在干凈的容器中進行,基礎的語言鏡像由應用自己選擇。
目前鏡像管理的方式是:
-
我們會事先準備好系統的基礎鏡像
-
在系統鏡像的基礎上,會構建出不同的語言鏡像供應用使用,如 Python,Golang,Java,Node,Rust 的各種版本以及混合語言的鏡像。
-
在應用指定的image語言鏡像之上,會安裝上 deps 指定的系統依賴,再構建出應用的鏡像,應用會在這個環境里面進行構建測試等。
語言這一層的 Dockerfile 會被嚴格 review,通過的鏡像才能被使用,以更好了解和支持業務技術選型和使用場景。
減少不穩定構建,降低問題復現成本
緩存的設計
最開始構建的緩存是落在對應的 Jenkins Slave 上的,隨著 Slave 數量的增多,應用構建被分配到不同 Slave 帶來的代價也越來越大。
為了讓 Slave 的管理更加靈活以及構建速度和 Slave 無關,我們最后將緩存按照應用使用的鏡像和系統依賴作為緩存的標識,上傳到 HDFS。在每次構建前拉取,構建之后再上傳更新。
針對鏡像涉及到的語言,我們會對常見的依賴進行緩存,如 eggs, node_modules, .ivy2/cache, .ivy2/repository。應用如果有其他的文件想要緩存,也支持在配置文件中指定。
依賴獲取穩定性
在對整個構建時間的開銷和不穩定因素的觀察中,我們發現拉取外部依賴是個非常耗時且失敗率較高的環節。
為了讓這個過程更加穩定,我們做了以下的事情:
-
完善內部不同語言的源
-
在不同語言的基礎鏡像中放入優先使用內部源的配置
-
搭建 HTTP Proxy,提供給以上覆蓋不到的場景
更低的排查錯誤的成本
本地開發和構建環境存在明顯的差異,可能會出現本地構建成功但是在構建系統失敗的情況。
為了讓用戶能夠快速重現,我們在項目 docker-ssh (https://github.com/alash3al/dockssh) 的基礎上做了二次開發,支持直接 ssh 到容器進行調試。由于容器環境與其他人的構建相隔離,我們不必擔心 ssh 權限導致的各種安全問題。構建失敗的容器會多保留一天,之后便被回收。
規范和標準的落地抓手
我們希望能給接入到構建系統的提高效率的同時,也希望能推動一些標準或者好的實踐,比如完善測試。
圍繞著測試和測試覆蓋率,我們做了以下的事情:
-
配置文件中強制要有測試環節。
-
應用測試結束之后,取到代碼覆蓋率的報告并打點。在提交的 Merge Request 評論中會給出現在的值和主分支的值的比較,以及最近主分支代碼覆蓋率的變化趨勢。
-
在知乎有應用重要性的分級,對于重要的應用,構建系統會對其要求有測試覆蓋率報告,以及更高的測試覆蓋率。
對于團隊內或者業界的基礎庫,如果發現有更穩定版本或者發現有嚴重問題,構建系統會按照應用的重要性,從低到高提示應用去升級或者去掉對應依賴。
高可用和可擴展的集群
Job 調度策略
Jenkins Master 只進行任務的調度,而實際執行是在不同的 Jenkins Node 上。
每個 Node 會被賦予一些 label 用于任務調度,比如:mysql:5.6, mysql:5.7, common 等。構建系統會根據應用的類型分配到不同的 label,由 Jenkins Master 去進一步調度任務到對應的 Node 上。
高可用設計
集群的設計如下,一個 Node 對應的是一臺物理機,上面跑了 Jenkins Slave (分別連 Master 和 Master Standby),Docker Deamon 和 MySQL(為應用提供測試的 MySQL)。
Slave 連接 Master 等待被調度,而當 Jenkins Slave 出現故障時,只需摘掉這臺 Slave 的 label,后續將不會有任務調度調度上來。
而當 Jenkins Master 故障時,如果不能短時間啟動起來時,集群可能就處于不可用狀態了,從而影響整個構建部署。為了減少這種情況帶來的不可用,我們采用了雙 Master 模型,一臺作為 Standby,一臺出現異常時就切換到另一臺健康的 Master。
監控報警
為了更好監控集群的運行狀態,及時發現集群故障,我們加了一系列的監控報警,如:
-
兩個 Jenkins Master 是否可用,當前的排隊數量情況。
-
集群里面所有 Jenkins Node 的在線狀態,Node 被命中的情況。
-
Jenkins Job 的執行時間,是否有不合理的過長構建或者卡住。
-
以及集群機器的 CPU,內存,磁盤使用情況。
后續的計劃
在未來我們還希望完善以下的方面:
-
Jenkins Slave 能更根據集群的負載情況進行動態擴容。
-
一個節點故障時能自動下掉并重新分配已經在上面執行的任務。一個 Master down 掉能被主動探測到并發生切換。
-
在 Merge Request 的構建環節推動更多的質量保證標準實施,如更多的接口自動化測試,減少有問題的代碼被合并到主分支。
參考資料:
Jenkinsfile 相關文檔 https://jenkins.io/doc/book/pipeline/jenkinsfile/
Jenkins Logo: https://jenkins.io/
Docker Logo: Brand Guidelines
創作挑戰賽新人創作獎勵來咯,堅持創作打卡瓜分現金大獎總結
以上是生活随笔為你收集整理的知乎容器化构建系统设计和实践的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 基于Wide Deep Learnin
- 下一篇: CMS之promotion failed