前言
這篇寫好一段時間了,一直也沒發布上來,今天稍微整理下了交下作業,部分內容偷懶引用了一些別人的內容。
使用Jenkins做持續集成/持續交付,當業務達到一定規模的時候,Jenkins本身就很容易成為整條流水線的瓶頸,各個業務端都依靠Jenkins,部署Jenkins服務時如何保障服務的高可用變得尤為重要。
以微醫為例,目前Jenkins的業務承載量:>1,000 Build Jobs,>5,000 Buils/Day,光依靠單master已經無法承載高並發的性能壓力,瓶頸來自多方面,不僅僅是Jenkins 應用本身占用 memory 和 CPU 資源,也包括各個job編譯、測試、部署等的資源開銷,隨着job數量的增加,大量的workspace也會耗盡服務器的存儲空間,嚴重影響整個技術團隊的工作效率和部署節奏。
一、Jenkins分布式集群架構
Jenkins 分布式架構是由一個 Master 和多個 Slave Node組成的 分布式架構。在 Jenkins Master 上管理你的項目,可以把你的一些構建任務分擔到不同的 Slave Node 上運行,Master 的性能就提高了。
Master/Slave相當於Server和agent的概念。Master提供web接口讓用戶來管理job和slave,job可以運行在master本機或者被分配到slave上運行構建。
一個master(jenkins服務所在機器)可以關聯多個slave用來為不同的job或相同的job的不同配置來服務。
二、傳統的Jenkins Slave方式存在的問題
傳統的 Jenkins Slave 一主多從式會存在一些痛點。比如:
- 主 Master 發生單點故障時,整個流程都不可用了;
- 每個 Slave 的配置環境不一樣,來完成不同語言的編譯打包等操作,但是這些差異化的配置導致管理起來非常不方便,維護起來也是比較費勁;
- 資源分配不均衡,有的 Slave 要運行的 job 出現排隊等待,而有的 Slave 處於空閑狀態;
- 資源有浪費,每台 Slave 可能是實體機或者 VM,當 Slave 處於空閑狀態時,也不會完全釋放掉資源。
三、基於 Kubernetes 搭建容器化Jenkins集群實踐
3.1 基於 Kubernetes 的Jenkins 集群架構
由於以上種種痛點,我們渴望一種更高效更可靠的方式來完成這個 CI/CD 流程,而虛擬化容器技術能很好的解決這個痛點,下圖是基於 Kubernetes 搭建 Jenkins 集群的簡單示意圖。
Jenkins Master 和 Jenkins Slave 以 Docker Container 形式運行在 Kubernetes 集群的 Node 上,Master 運行在其中一個節點,並且將其配置數據存儲到一個 Volume 上去,Slave 運行在各個節點上,並且它不是一直處於運行狀態,它會按照需求動態的創建並自動刪除。
這種方式的工作流程大致為:當 Jenkins Master 接受到 Build 請求時,會根據配置的 Label 動態創建一個運行在 Docker Container 中的 Jenkins Slave 並注冊到 Master 上,當運行完 Job 后,這個 Slave 會被注銷並且 Docker Container 也會自動刪除,恢復到最初狀態。
這種方式帶來的好處有很多:
- 服務高可用,當 Jenkins Master 出現故障時,Kubernetes 會自動創建一個新的 Jenkins Master 容器,並且將 Volume 分配給新創建的容器,保證數據不丟失,從而達到集群服務高可用。
- 動態伸縮,合理使用資源,每次運行 Job 時,會自動創建一個 Jenkins Slave,Job 完成后,Slave 自動注銷並刪除容器,資源自動釋放,而且 Kubernetes 會根據每個資源的使用情況,動態分配 Slave 到空閑的節點上創建,降低出現因某節點資源利用率高,還排隊等待在該節點的情況。
- 擴展性好,當 Kubernetes 集群的資源嚴重不足而導致 Job 排隊等待時,可以很容易的添加一個 Kubernetes Node 到集群中,從而實現擴展。
3.2 部署 Jenkins Master
在保證Jenkins Master高可用的前提下,可以按傳統方式war包方式部署,可以使用docker方式部署,也可以在Kubernetes Node中部署,這部相對簡單,不再展開詳述。
3.3 Jenkins 配置 Kubernetes Plugin
管理員賬戶登錄 Jenkins Master 頁面,點擊 “系統管理” —> “管理插件” —> “可選插件” —> “Kubernetes plugin” 勾選安裝即可。
安裝完畢后,點擊 “系統管理” —> “系統設置” —> “新增一個雲” —> 選擇 “Kubernetes”,然后填寫 Kubernetes 和 Jenkins 配置信息。

3.4 使用Jenkins Pipeline測試驗證
接下來,我們可以配置 Job 測試一下是否會根據配置的 Label 動態創建一個運行在 Docker Container 中的 Jenkins Slave 並注冊到 Master 上,並且在運行完 Job 后,Slave 會被注銷並且自動刪除 Docker Container。
創建一個 Pipeline 類型 Job 並命名為"pipeline_kubernetes_demo1,然后在 Pipeline 腳本處填寫一個簡單的測試腳本如下:
pipeline {
agent {
kubernetes {
//cloud 'kubernetes'
label 'k8s-jenkins-jnlp'
containerTemplate {
name 'jnlp'
image 'harbor.guahao-inc.com/base/jenkins/jnlp-slave:latest'
}
}
}
stages {
stage('Run shell') {
steps {
script {
git 'https://github.com/nbbull/demoProject.git'
sh 'sleep 5'
}
}
}
}
}
執行構建,此時去構建隊列里面,可以看到有一個構建任務,第一次構建的時候會稍慢,因為k8s的node需要去下載jnlp-slave的鏡像。
稍等一會就會看到k8s-jenkins-jnlp-8gqtp-j9948的容器正在創建,然后開始運行,Job 執行完畢后,jenkins-slave 會自動注銷並刪除容器,我們通過 kubectl 命令行,可以看到整個自動創建和刪除過程,整個過程自動完成。
[root@kubernetes-master1 ~]# kubectl get pods|grep jenkins
k8s-jenkins-jnlp-8gqtp-j9948 0/1 ContainerCreating 0 4s
[root@kubernetes-master1 ~]# kubectl get pods|grep jenkins
k8s-jenkins-jnlp-8gqtp-j9948 1/1 Running 0 18s
具體的構建日志參考如下:
3.5 自定義 jenkins-slave 鏡像
通過 kubernetest plugin 默認提供的鏡像 jenkinsci/jnlp-slave 可以完成一些基本的操作,它是基於 openjdk:8-jdk 鏡像來擴展的,但是對於我們來說這個鏡像功能過於簡單,比如我們想執行 Maven 編譯或者其他命令時,就有問題了,那么可以通過制作自己的鏡像來預安裝一些軟件,既能實現 jenkins-slave 功能,又可以完成自己個性化需求,dockfile如下:
FROM harbor.guahao-inc.com/base/jenkins/jnlp-slave:latest
USER root
//下載安裝必要組件
RUN apt-get update && apt-get install -y sudo && rm -rf /var/lib/apt/lists/*
RUN apt-get update && apt-get install -y vim && apt-get install -y sshpass
//下載和配置maven
COPY apache-maven-3.2.6-GH /usr/greenline/install/apache-maven-3.2.6-GH
RUN ln -s /usr/greenline/install/apache-maven-3.2.6-GH /usr/greenline/maven3
//下載和配置jdk
COPY jdk1.8.0_91 /usr/greenline/install/jdk1.8.0_91
RUN ln -s /usr/greenline/install/jdk1.8.0_91 /usr/greenline/jdk_1.8
ENV JAVA_HOME=/usr/greenline/jdk_1.8
ENV CLASSPATH=.:/usr/greenline/jdk_1.8/lib/dt.jar:/usr/greenline/jdk_1.8/lib/tools.jar:/usr/greenline/jdk_1.8/lib/rt.jar
ENV PATH=/usr/greenline/jdk_1.8/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/sbin: