[持續交付實踐] Jenkins Pipeline 高可用設計方法


前言

這篇寫好一段時間了,一直也沒發布上來,今天稍微整理下了交下作業,部分內容偷懶引用了一些別人的內容。
使用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:/usr/greenline/maven3/bin

USER jenkins

ENTRYPOINT ["jenkins-slave"]

除了在Pipeline中定義slave的image,我們也可以使用非 Pipeline 類型指定運行該自定義 slave,那么我們就需要修改 “系統管理” —> “系統設置” —> “雲” —> “Kubernetes” —> “Add Pod Template” 修改配置 “Kubernetes Pod Template” 信息如下:

 

3.6 Jenkins啟動參數調整

默認情況下,Jenkins保守地生成代理。比如,如果隊列中有2個構建,它將不會立即生成2個執行程序。它會產生一個執行器並等待一段時間讓第一個執行器被釋放,然后再決定產生第二個執行器。Jenkins確保它產生的每個執行者都得到最大限度的利用。如果要覆蓋此行為並立即為隊列中的每個構建生成執行程序,可以在Jenkins啟動時參加一下參數:

-Dhudson.slaves.NodeProvisioner.initialDelay=0
-Dhudson.slaves.NodeProvisioner.MARGIN=50
-Dhudson.slaves.NodeProvisioner.MARGIN0=0.85

 

四、使用GlusterFS共享存儲

Jenkins的日常工作包括大量的編譯構建,構建過程中會涉及到大量依賴包的下載(比如jar包,npm包等),采用原生容器的方式由於沒有持久化本地倉庫,每次構建都需要對這些依賴包重新下載,嚴重影響效率。
這里需要解決公共依賴包持久化存儲的問題,一種做法是配置宿主機目錄掛載的方式,把文件掛載到宿主機,這樣雖然方便但是不夠安全,而且Kubernetes集群一般有多個Node節點,如果容器在掛了被重新拉起的時候被調度到其他的Node節點,那映射在原先主機上的數據還是在原先主機上,新的容器還是沒有原來的數據。
所以推薦的方法一般都是把數據存儲在遠程服務器上如:NFS,GlusterFS,ceph等,目前主流的還是使用GlusterFS。事實上,Kubernetes的選擇很多,目前Kubernetes支持的存儲有下面這些:

GCEPersistentDisk
AWSElasticBlockStore
AzureFile
AzureDisk
FC (Fibre Channel)
FlexVolume
Flocker
NFS
iSCSI
RBD (Ceph Block Device)
CephFS
Cinder (OpenStack block storage)
Glusterfs
VsphereVolume
Quobyte Volumes
HostPath (就是剛才說的映射到主機的方式,多個Node節點會有問題)
VMware Photon
Portworx Volumes
ScaleIO Volumes
StorageOS

Kubernetes有這么多選擇,GlusterFS只是其中之一,但為什么可以脫穎而出呢?GlusterFS,是一個開源的分布式文件系統,具有強大的橫向擴展能力,通過擴展能夠支持數PB存儲容量和處理數千客戶端。GlusterFS借助TCP/IP或InfiniBand RDMA網絡將物理分布的存儲資源聚集在一起,使用單一全局命名空間來管理數據。GlusterFS的Volume有多種模式,復制模式可以保證數據的高可靠性,條帶模式可以提高數據的存取速度,分布模式可以提供橫向擴容支持,幾種模式可以組合使用實現優勢互補。

4.1 GlusterFS集群的部署:

安裝環境: 192.168.XX.A , 192.168.XX.B
GlusterFS集群的部署比較簡單,在各台機器分別安裝

# yum install centos-release-gluster
# yum install glusterfs-server
# /etc/init.d/glusterd start

在一台上面建立信任關系

# gluster peer probe 192.168.XX.B #后面跟另外一台的IP
# gluster peer status

創建名稱為“jenkins_public”的分布式卷:

# gluster volume create jenkins_public 192.168.XX.A:/data/exp1 1192.168.XX.B:/data/exp2 force
# gluster volume info 查看邏輯卷信息
# gluster volume start jenkins_public #啟動邏輯卷

4.2 如何在Kubernetes中使用GlusterFS

Kubernetes用PV(PersistentVolume)、PVC(PersistentVolumeClaim)來使用GlusterFS的存儲。PV與GlusterFS的Volume相連,相當於提供存儲設備;PVC消耗PV提供的存儲,由應用部署人員創建,應用直接使用PVC進而使用PV的存儲。
官方文檔對配置過程進行了介紹:https://github.com/kubernetes/examples/blob/master/staging/volumes/glusterfs/README.md

4.2.1 在Kubernetes中創建GlusterFS的端點定義(endpoints)

data1-volume-pv-cluster.json:

 {
"kind": "Endpoints",
"apiVersion": "v1",
"metadata": {
"name": "data1-volume-pv-cluster"
},
"subsets": [
{
"addresses": [{ "ip": "192.168.XX.A" }],
"ports": [{ "port": 20 }]
},
{
"addresses": [{ "ip": "192.168.XX.B" }],
"ports": [{ "port": 20 }]
}
]
}

備:該subsets字段應填充GlusterFS集群中節點的地址。可以在port字段中提供任何有效值(從1到65535)。

##創建端點:
[root@k8s-master-01 ~]# kubectl create -f data1-volume-pv-cluster.json
##驗證是否已成功創建端點
[root@k8s-master-01 ~]# kubectl get ep |grep data1-volume-pv-cluster
glusterfs-cluster 192.168.XX.A:20,192.168.XX.B:20

4.2.2 配置 service

我們還需要為這些端點(endpoint)創建服務(service),以便它們能夠持久存在。我們將在沒有選擇器的情況下添加此服務,以告知Kubernetes我們想要手動添加其端點
data1-volume-sv-cluster.json:

{
"kind": "Service",
"apiVersion": "v1",
"metadata": {
"name": "data1-volume-pv-cluster",
"namespace": "default",
}
},
"spec": {
"ports": [
{
"protocol": "TCP",
"port": 20,
"targetPort": 20
}
]
}
}

創建服務

[root@k8s-master-01 ]# kubectl create -f data1-volume-sv-cluster.json
##查看service
[root@k8s-master-01 ]# kubectl get service | grep data1-volume-sv-cluster

4.2.3.配置PersistentVolume(簡稱pv)

創建jenkins-public-pv.yaml文件,指定storage容量和讀寫屬性
jenkins-public-pv.yaml:

kind: PersistentVolume
apiVersion: v1
metadata:
labels:
name: jenkins-public-pv
namespace: default
name: jenkins-public-pv
spec:
capacity:
storage: 600Gi
accessModes:
- ReadWriteMany
persistentVolumeReclaimPolicy: Retain
glusterfs:
endpoints: data1-volume-pv-cluster
path: jenkins_public
readOnly: false

然后執行:

[root@k8s-master-01 ~]# kubectl create -f jenkins-public-pv.yaml
## 查看pv
[root@k8s-master-01 ~]# kubectl get pv|grep jenkins-public-pv

4.2.4 配置PersistentVolumeClaim(簡稱pvc)

創建jenkins-public-pvc.yaml文件,指定請求資源大小

kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: jenkins-public-pvc
namespace: default
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 600Gi
selector:
matchLabels:
name: jenkins-public-pv

然后執行:

[root@k8s-master-01 ~]# kubectl create -f jenkins-public-pvc.yaml
## 查看pvc
[root@k8s-master-01 ~]# kubectl get pvc|grep jenkins-public-pvc

4.2.5 部署Jenkins Slave掛載pvc

修改pipeline,把pvc掛載到容器內的/home/maven_repository,下面是完整的Pipeline:

pipeline {
agent {
kubernetes {
cloud 'kubernetes'
label 'k8s-jenkins-jnlp'
defaultContainer 'jnlp'
yaml """
apiVersion: v1
kind: Pod
metadata:
labels:
node-label: "k8s-jenkins-jnlp-${UUID.randomUUID().toString()}"
spec:
containers:
- name: jnlp
image: harbor.guahao-inc.com/base/jenkins/jnlp-slave:1123_20
volumeMounts:
- mountPath: /home/maven_repository
name: docker-socker-file
volumes:
- name: docker-socker-file
persistentVolumeClaim:
claimName: jenkins-public-pvc
"""
}
}
stages {
stage('Run shell') {
steps {
script {
git 'https://github.com/nbbull/demoProject.git'
sh 'sleep 50'
}
}
}
}

}

可以把agent的邏輯統一抽象到共享庫,就可以實現所有的job都通過K8S進行構建,至此部署完成。

五、Jenkins性能調優

除了結合K8S搭建Jenkins集群保證高可用以外,最后再補充總結一些jenkins自身的性能調優技巧,這里不再展開,具體可在使用中去體會和補充。
• 使用Pipeline方式比配置方式運行更快。
• 讓Pipeline做中間組織的工作,而不是取代其他工具。
• 能用腳本實現的,就不要用插件。
• 根據資源情況限制並發執行的job數量。
• 使用共享庫抽象公共的代碼並持續優化。
• 不要寫太復雜的Pipeline腳本(>1000行),包括共享庫代碼在內。
• 不要對外網環境有強依賴(萬惡的牆)。
• 使用“@NonCPS”注解高耗代碼方法。
• 使用SSD硬盤等高性能硬件
• Durability/Speed Options


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM