1. 程式人生 > >基於 Kubernetes 的 Jenkins 構建叢集實踐

基於 Kubernetes 的 Jenkins 構建叢集實踐

本文由 DevOps時代 整理自 Jenkins 北京線下沙龍演講

作者:李華強-樂視致新

在大型團隊的 CI 構建裡具有豐富最佳實踐的經驗。今天我給大家分享的更多是聚焦在 Jenkins 本身,結合我在 Jenkins 實際使用過程中和整個 Jenkins Slave 管理演化的過程的案例,這樣能給大家帶來更好的借鑑和參考體驗。

下面是主要要分享的四大內容:

  1. Jenkins分散式構建架構
  2. 基於Lable的Slave叢集管理
  3. 基於Docker外掛的容器化實踐
  4. 基於Kubernetes的容器化實踐

一. Jenkins分散式構建架構

1.1 架構圖

Jenkins 分散式架構一個 Master 和多個 Slave Node 分散式的架構。

在 Jenkins Master 上管理你的專案,可以把你的一些構建任務分擔到不同的 Slave Node 上執行,Master 的效能就提高了。

如果單純的使用 Master 去構建,除了要承擔專案上的編譯、測試等開銷外,還會大大的影響 Jenkins 應用本身佔用 memory 和 CPU 資源。

1.2 Jenkins Slave 連線方式

Jenkins Slave 連線方式常使用下面兩種:

  1. 通過 SSH 啟動 Slave 代理
    • 在 Jenkins 上直接配置,相當於從 Master 往 Slave 上連線,從 Master 上主動發起的請求。
  2. 通過 JNLP 啟動代理
  • 基於 Java Web Start 的 JNLP 的連線,從 Slave 往 Master 主動的方式。

這兩種主要的連線方式,在後面的叢集 slave 的管理方案中都會涉及到。

1.3 Jenkins 排程策略

  • 使用者視角
    • 直接使用 slave name,構建使用指定 slave
    • 使用 label,構建多數使用同一個的 slave,偶爾使用其他 slave
  • 系統實現
    • consistent hashing algorithm (node,available executors,job name)
    • 每一個 Job 都有一個對應所有 Node 的優先順序列表
  • 其它外掛
    • Least Load (https://plugins.jenkins.io/leastload)
    • Commercial plugin (Even Scheduler Plugin)

在使用的過程中,是不是都會經常感覺到下面兩種情形:

  1. 如果一個專案的構建指定了一個 Slave,那麼這個專案所有的每次構建都只用這個Slave構建。
  2. 如果使用了label 去管理多個 Slave,給一個專案的構建指定了這個 label,會發現這個專案的多次構建,大多數都是使用同一個 Slave,並沒有使用 label 裡的其它 Slave。

從 Jenkins 本身的實現角度來說,Jenkins 分配它的排程策略的時候,有一個一致性的雜湊演算法,會將你新增的 Slave,也可以稱為 node 節點,以及 node 上面可用的 executors,包括 job name,最後算出來一個相當於你的 job 和所有 Slave 對應的優先順序列表,會選擇優先順序最高的Slave去構建,當不滿足條件或者沒有可用的 executors 時,才會選用下一個節點。這個是 Jenkins 預設的排程策略,可通過其它外掛來改變這個策略,如 Least Load 外掛,選擇一個數目最少的最空閒的節點。

二. 基於 Lable 的 Slave 叢集管理

2.1 Android 產品複雜環境

這是樂視安卓產品的環境。

  • 業務產品差異性,要支援多個產品,如手機、電視、車、盒子、VR等,我所在的持續整合部門,全面負責所有生態業務的 CI 環境。
  • 多種構建型別,我們常見的業務場景裡面,有 Daily Build,Test Build,Continiuous build,APK 編譯等,構建的型別不同。
  • 程式碼量大,這是安卓產品的一個特點。像每一個TV的程式碼,原始碼整個下下來就是50多GB,和一個普通的網際網路應用,真的是差別太大了。
  • 編譯空間大,這麼大的程式碼體量去做編譯,所需的空間可想而知,一定要比你的程式碼量還大,編譯完整個編譯空間能達到100-200GB
  • 編譯時間長,程式碼量大,編譯需要的空間多,相應的它的編譯時間就很長。我們現在並行的物理機24執行緒去編,一般2-3個小時完成整個過程。
  • 業務間相對獨立。我們有多個產品,業務之間還相對獨立,這個獨立體現在兩個方面,一個是我們不同的產品,比如手機和電視,有自己的專屬編譯伺服器。另一個是伺服器上有獨立的構建環境,沒有統一,這是業務之間的複雜性決定的。
  • 伺服器差異,版本、CPU、memory、Disk等配置不一樣。

整體來講,我們所面對的安卓產品的 CI 環境比較複雜,下面的內容也將圍繞這個痛點來講的。

2.2 普遍問題

表象問題:

  • Slave 很多,空閒的 Slave 也很多
  • 佇列中等待構建任務很多
  • 一些構建失敗,暴露 workspace 空間不足問題

原因分析:

  • 業務間,編譯環境不統一,不能跨業務共享
  • 業務內,特定 jobs 直接繫結特定 slave,併發量受限於 executor 數目
  • Slave 上構建 workspace 的遺留,佔用大量空間
  • 業務量增大,新建 Jobs 增多,瓶頸出現

我們在使用 Jenkins 過程中暴露出一些問題,我們掛載的 Slave 很多,空閒的也很多,沒有被充分利用起來。另外,掛載那麼多的 Slave,我們佇列中等待構建的還很多,相當於那麼多的 Slave 沒有解決併發的問題,任務還在等待。還有一些構建失敗了,是因為 workspace 空間不足。

為什麼會產生上述問題呢?

一個是業務間,編譯環境不統一,不能跨業務共享。一臺 Jenkins 上掛了那麼多 Slave 給不同業務用,相對來說不同的業務只能使用這麼多 Slave 中的一些子集。

第二是業務內,有些業務的同學去配置 job 的時候,直接繫結特定的 Slave,相當於直接繫結那個 Slave 的 name,而不是 Lable,這些 job 所有的構建只依賴於這個 Slave,這個併發就受到被綁的 Slave 的 executor 數目限制。

第三,Slave 上構建 workspace 有遺留,佔用大量空間。一個 job 構建結束後,編譯的空間遺留在 Slave 上,除非在你的 job 裡配置了清理規則。

此外,隨著業務量增大,新建的 job 也多,慢慢瓶頸就出現了,問題也就都暴露出來了。

在座的同學隨著你們公司業務的發展,這些問題可能都會遇到的。

2.3 優化改進

面對上述的問題我們改如何進行優化改進呢?我們做了四個方面的優化:

  1. 同質 slaves 新增相同 Label,同類型 jobs 使用 Label 進行構建
  2. 適量增大 slave 的 executor 數目
  3. 定時掃描清理 slaves 上的廢棄 jobs 的遺留 workspace
  4. 業務 jobs 配置 workspace 清理規則

下面細說下。

我們把我們同質 Slaves 新增相同 Lable,用 Lable 來管理我的 Slaves,前面有說到我們的業務不同的產品線,相當於我們的編譯環境不一樣,同一套產品,把同質 Slaves 通過 Lable 使用,同類型 jobs 可以使用 Lable 進行構建。

適量增大 executor 數目,一定會增大併發量。為什麼是適量?編譯相當於我們用物理機24執行緒併發去跑,根據業務的場景不同,我們是適量的。比如原來一個物理機只配一個,根據有的產品線的 job 沒有消耗那麼多的 CPU、memory,編譯時間要求不高,有一些 Slave 從一個 executor 增加到兩個,就能解決 job 併發量的問題。

第三個要解決 Slaves 上遺留的編譯 workspace 的問題,定時掃描 Slave 上的 workspace。有兩點要注意,一點是在你使用過程中,可能會頻繁的重新命名 job,如果把 job 重新命名了,原來的編譯空間就留下來了,就沒有意義了。另外,從業務job配置上,一定要配置相應的 workspace 清理規則。比如說編譯結束了,傳到版本伺服器或者製品倉庫,workspace 實際上沒有意義,可以在構建結束後就配置相應的清理規則,把 workspace 清理掉,這樣你的 Slave 上的空間就被及時釋放了,而不會等到下一次構建的時候由於空間不足導致的失敗。

在 Jenkins job 裡面 Post build Action,有delate workspace 配置,可以針對勾選的構建的狀態(成功、失敗等),把這個 workspace 清理掉。預設清理的時候會把整個 workspace 都清理掉,一點都不留。

考慮到我們特殊的業務場景和編譯的時間特別長,我們中間有一些編譯快取,如 ccache,保留的話對下次構建的編譯速度上是有很大提高的。在配置清理規則的時候,我們沒有完全清理,而是保留了需要的,其它的都被清理掉。從200GB清理到最後可能只有50GB,這就已經大大釋放了空間。

2.4 優化效果

通過上述的優化改進,我們取得了下面三個效果:

  • 併發能力增強,佇列中構建任務緩解
  • Slave 空間有效利率提高,空間不足造成的構建失敗大大減少
  • Slave 叢集綜合資源利用率提高(Disk,CPU),趨於一致性

併發能力增強,來自於用 Lable 去管理了,本身 Lable 關聯了多個Slave,這個就相當於Slave構成了一個資源池,併發能力就提高了。帶來的直接效果,併發能力強,佇列中構建任務等待的數目就下降了。

Slave 整體的空間利用率有效提高,這方面做了一些清理,空間不足造成的構建失敗大大減少。

整個叢集,通過 Lable 管理,綜合利用率提高了,反應到 Disk 和 CPU 上。上面兩個圖,左邊是編譯伺服器的 disk,前面每一個 Slave 上它的 disk 使用率是不均衡的,通過使用 Lable 以後,相對來說 job 分到 Slave 上更加均衡,disk 慢慢趨於一致,而且是保持到相對合理的水平。右邊這塊是從CPU的角度,從前半部分開始看,不同的 Slave 節點上,上半部分這幾臺編譯的時候 CPU 利用率很高,但下半部分這幾臺非常空。隨著加了 Lable 以後,它們的趨勢趨於一致。

2.5 Lable 方案反思

這個是對使用 Lable 管理 Slave 的一些總結和反思:

益處:

  • Slave 資源池化,整體資源利用率提高
  • 併發量增大(受所有 slave 的 executor 數量限制)
  • Slave 的管理對 Job 配置透明化(Job 配置 Label 使用)

侷限性:

  • Jenkins 內建的排程策略,資源利用不均衡
    • 同一個 job 的多次構建傾向於使用同一個 slave
    • 同一個 job 的併發構建,往往使用同一個 slave,資源競爭造成構建時間增加等
    • 某幾個 jobs 構建,往往使用同一個 slave,資源競爭造成構建時間增加等
  • 環境隔離問題
    • 不同型別的 job 資源需求不同,環境複用後資源排程是問題,加大管理成本
    • 環境不統一,業務調整,資源再分配要回爐重構(安裝對應的工具系統等)
  • 基礎設施一致性問題
    • 多個任務跑在同一臺資源上的潛在風險和衝突,不可變基礎設施
    • 環境問題導致的CI信任問題,程式碼沒錯,是環境問題

這個就不展開來說了,有利有弊,應以實際需求情況來進行合理的趨避。

三. 基於 Docker 外掛的容器化實踐

3.1 APK 編譯需求

APK 編譯需求所面臨的現狀主要有以下幾點:

  • 單點編譯伺服器支援所有 APK 編譯構建
  • 伺服器效能比較差
  • 環境依賴複雜,工具維護成本高
  • 構建任務併發比較困難

該如何去改進這些問題呢?我們決定使用容器化,基於容器化去構建 Jenkins Slave,直接使用 Docker 外掛進行容器化。

業務的需求依然是第一驅動力。

3.2 Docker image 固化編譯環境

固化 Docker image 內容,最基本的要有賬號和許可權,裡面要有相應的構建賬號,包括許可權。因為 Docke image 有可能去下程式碼,版本倉庫等等其他的整合,都需要相應的許可權。

使用 Docker 外掛去構建,這個就類似於普通的 Jenkins Slave。與用 SSHD方式一樣,Docker image 必須用到 JDK 和 SSHD。

另外在業務上,APK 編譯的依賴工具,比如不同安卓 SDK/NDK、libs 和工具等。

在固化 Docker image 過程中還有一些要考慮的問題,比如 Docker image 和業務產品以及不同版本工具集的關係。我們是多套產品,怎麼去管理呢?是做一個大而全的 Docker image 來涵蓋所有的還是每一個產品線做一個 Docker image,每一個產品線裡工具有不同的版本。我們現在基本上是按產品線走的,比如手機、電視的,我們給它一套 Docker image。

另外是 Docker image 自動化配套設施,如自動化構建、更新、上傳、部署等,這些配套的措施需要額外去維護的,這和普通的 Slave 不一樣。

3.3 Jenkins 整合 Docker 外掛

這裡是我們使用 Docker 外掛的資訊,使用的版本是0.15.0,它所支援的連線方式只能是 SSH 的連線方式。從0.16.0版本開始支援JNLP的連線方式。(最新版本已經不是0.16.2了,詳細資訊請檢視官方網站。編者注)

外掛的配置有兩點值得注意的:

  1. 可以新增1個或者多個 Docker host 的資訊,Docker 也提供一個 Cloud 叢集,這個 Docker host 可以理解成我們原來普通的 Slave。
  2. 每一個 Docker host 下面可以新增關聯1個或者多個 Docker Template。

3.4 配置示例

看一下配置示例。

Name 要指定一個 Docker 配置的名字。

Docker URL 是你 Docker 的URL,通過什麼樣的方式去連線 Docker host。

Container Cap 設定併發數量。

Docker Template 可以新增多個 Docker 模板,這裡只是列了一個,每個 Docker 模板實際上是從哪裡可以獲得你的 Docker image。

Launch method 有一個連線方式,當時這個外掛只支援SSH,我們只能通過SSH的方式。

3.5 Docker外掛反思

這個是對使用 Docker 外掛的一些總結和反思:

益處:

  • 容器化(環境標準化,隔離性,版本,可移植性 等等)
  • 容器 Slave 按需彈性收縮,自動建立,使用,銷燬
  • 資源共享
    • 多個 jobs 使用不同容器 slave 可以執行在同個 Docker host
    • 同一 Job 通過使用 label,也可以讓構建執行在不同 Docker host 上的容器 slave 上
  • 併發量(外掛設定 cloud/template Capacity)

侷限性:

  • 外掛配置中 Docker host 與 Docker image 的強耦合性,配置不方便
    • Docker host 數量很多的時候
    • Docker image 需要配置到多個 Docker host上的時候
  • 使用Jenkins內建的排程策略,資源利用不均衡
    • 相同 Docker image 配置到多個 Docker host上 使用相同 label 的時候

使用這個方案給我們帶來了什麼樣的好處,有沒有什麼樣的侷限性呢?

帶來的好處有:

第一,容器化。環境標準化、隔離性、本身 Docker image 有版本,相當於對構建有一個版本控制,還有可移植性,構建一次,host都可以去配都可以去使用。

第二,通過 Docker 外掛配置了 Cloud Slave 架構,容器的 Slave 就可以彈性進行收縮。一點有了構建需求,它就會去動態的生成一個 Docker 容器掛載為 Jenkins Slave 進行構建。構建結束以後,這個容器就會被自動銷燬。所以整個過程不再是原來那種普通的 Slave 長連線的方式掛載,這種往往使用過了,就看不到了,動態銷燬了。

第三,資源能得到共享,因為多個 job 我們使用不同的容器 slave,可以執行到同一個 Docker host,也就是說 Docker host 上我們可以配置多個 Docker image,提供不同的模板,不同的 job 都可以使用,很多 job 的構建可以扔到同一各Docker host 上去用。另外同一個 job,通過配置 Lable可以讓你的構建執行在不同的 Docker host。每一個容器,你新增 Docker image作為模板的時候,都有一個 Lable,這個和普通的 Slave 一樣,你可以新增一個或多個 Lable。

第四,併發量,在外掛裡設定。整個 Cloud,你可以設定能支援多少個例項。template Capacity 決定了你最大的併發量。每一個模板,每一個 image 也可以控制的。

有好處就有不好的地方,外掛中的 Docker host 和 Docker image 是強耦合性,必須在每一個 Docker host 下面配置你需要的應用。在 Docker host 數量很多,Docker image 需要配置到多個 Docker host 上共享的時候,這種配置就很不方便了。

另外,排程還是使用內建的策略,也存在不均衡性。比如有兩臺 Docker host,上面配置了同一個 Docker image,往往是先仍到第一個 Docker host上構建。

四. 基於 Kubernetes 的容器化實踐

前面通過使用 Lable 到 Docker 外掛,我們一步步去改進。同時也發現基於 Docker 的容器化,還是存在上面的侷限性,並不能滿足我們的需求,因此還是需要進一步優化看能不能有更高的提高。

4.1 What is Kubernetes?

kubernetes,使用過這個容器工具的可能比較熟悉。開源的,可以自動化,容器化一些應用的部署。

這個是它的一個架構圖,這裡面有一個 pod 的概念,相當於一個 pod 可以有多個 Container。後面我們講到的 kubernetes 外掛就是通過建立一個 pod 來掛載一個容器 slave 的方式。

4.2 業務需求

這是我們的一個業務需求:

  • 資源收縮嚴重,業務需要正常運轉
  • 容器化/標準化,構建環境隔離
  • K8s資源限制,排程策略,伺服器資源共享

其他考慮因素有:

  • 容器化構建效能對比
  • 版本的選擇
  • Docker registry
  • Docker image的構建更新自動化
  • 構建環境工具依賴
  • 構建優化(程式碼 mirror,編譯快取等)

4.3 資料驗證可行性

這是我們做的一些資料驗證,之前我們是對 APK 編譯,就是一個簡單的應用。但是我們要對整個安卓產品,比如手機電視,這個量就不一樣了。我們必須要做好充分的測試,使用容器去編譯效能到底靠不靠譜。

通過使用VM去編譯和使用 Docker 容器編譯,不同的場景做了一個對比測試,編譯效能基本上是一致的。所以才下決心把我們的業務都往這上面去遷移。

4.4 選擇版本

這個是給大家一個參考,我們所使用的一些主要工具或者元件的版本。

4.5 Docker image的構建

通過 Dockerfile 自動化構建我們的 Docker image。另外,我們使用 Ansible 來批量部署管理環境。我們的CI構建環境做得非常不錯的,這個地方沿用了我們的優勢,基於 Ansible playbook 的基礎上來構建我們的容器。上面是 Dockerfile 的一個示例。

4.6 構建環境依賴

  • 外部工具
    • 單一可信資料來源,單獨伺服器管理, Ceph儲存
    • 所有k8s Node 通過NFS掛載
    • 容器通過 volume 使用
  • 程式碼 mirror
    • 所有 k8s Node 本地目錄
    • 容器通過 volume 使用
    • Jenkins job 自動化更新部署
  • 編譯 cache
    • 所有 k8s Node 本地目錄
    • 容器通過 volume 使用
    • 命中率統計和多基線複用

4.7 Jenkins 整合 k8s 全景

這裡是一個全景圖。Jenkins 裡有k8s的外掛,中間這塊是k8s叢集,多臺物理機做k8s的節點,我們所有的編譯構建實際上都是通過外掛在 Node 上建了一個容器,這個容器掛載為 Jenkins 的一個 Slave。右邊和上面這塊是外部的依賴,比如 cache、Tools、Mirror、HARBOR、Grafana等等。

4.8 實現效果

遷移到容器化以後,我們整個資源利用率有50%以上的提升,資源成本也下降了35%,另外是新業務新環境上線,這個帶來的好處沒法統計,相對來說是很大的。

4.9 Kubernetes外掛

  • 配置 Kubernetes API URL
  • 新增1個或多個kubernetes pod template (image)
  • kubernetes pod template 的啟動命令間接使用JNLP
  • kubernetes pod template 可以配置資源限制, 主要是CPU和Memory資源

4.10 配置示例


這是一個配置示例,注意劃線部門。

4.11 資源限制配置示例

這是一個資源限制配置示例。對CPU、memory做一些資源限制。

4.12 容器啟動指令碼示例

容器啟動指令碼示例,這上面兩個引數 SECERT、SLAVE_NAME,這是k8s外掛預設傳遞過來的啟動引數。JENKINS_URL預設 設定為POD的環境變數。

4.13 Kubernetes外掛反思

每一個方案我們最後都要去做一個反思和總結。

k8s外掛的益處:

  • 容器化(環境標準化,隔離性,版本,可移植性 等等)
  • 容器Slave 按需彈性收縮,自動建立,使用,銷燬
  • 資源共享(所有的容器slave可以共享 Kubernetes cluster)
  • 併發量(外掛設定Cloud/Template Capacity)

k8s外掛的侷限性:

  • Kubernetes Cluster 的高可用性
    • 單master,這個Master要掛了所有構建叢集都掛了
    • 需要使用者實現HA方案

4.14 持續改進

持續改進方面:

  • Jenkins master容器化
  • Jenkins Job指令碼化
  • 流水線驅動持續交付

4.15 總結

方案 效果 注意點
Lable 1.Slave資源池化,整體資源利用率提高
2.構建併發量
3.Slave的變動對Job配置透明化
1.環境一致性,相同label下slave要求同質
2.Slave上jobs構建遺留workspace問題
3.Jenkins預設排程策略的不完美性
Docker 1.容器化帶來的好處(環境標準化,隔離 性,版本,可移植性 等等)
2.容器Slave按需彈性收縮,自動建立, 使用,銷燬
3.伺服器資源共享
4.構建併發量設定
1.Docker host與Docker image的強耦合性,外掛配置不方便性
2.使用Jenkins預設排程策略
3.Docker registry、image管理等配套工作
Kubernetes 1.Docker外掛相同的優點
2.資源申請限制等配置,滿足不同構建需求
3.使用Kubernetes排程策略
1.Kubernetes Cluster的高可用性問題
2.監控的重要性
3.Kubernetes、Docker registry、image管理等配套工作

這個是上面三種方案的一個總結。每一個方案都有好的效果和注意點,不同的方案都有各自不同的優缺點,誰優誰劣取決於具體實施場景。應根據業務實際情況來選取適合的方案。