devops [持续交付实践] Jenkins Pipeline 高可用设计方法

蒋刚毅 · 2018年12月12日 · 最后由 小杨 回复于 2022年01月06日 · 10803 次阅读
本帖已被设为精华帖!

前言

这篇写好一段时间了,一直也没发布上来,今天稍微整理下了交下作业,部分科普内容偷懒摘用了其他文章的内容。
使用 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

主帖直达:https://testerhome.com/topics/9977

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
共收到 10 条回复 时间 点赞
simple 将本帖设为了精华贴 12月12日 20:26

加精理由:蒋老师的连载,不会错

最开始用 Jenkins 是做自动化测试,一开始就是 slave 资源池(预先算好容量)分组模式,master 上不允许跑任何 job:
group-label1:QTP
group-label2:selenium
group-label3:UT & sonarqube & Fortify
……分组插件是测开的小哥哥小姐姐们写的~当时害我查了好多文档没找到这个功能~
那时候容器还不火,现在真心是技术进步太多了,骚操作一波接一波的~

槽神 回复

确实容器技术赋予了很多以前不敢想象的能力。

赞一个

持续交付方面的资深专家啊

ivy520 [该话题已被删除] 中提及了此贴 12月16日 22:55
ivy520 [该话题已被删除] 中提及了此贴 12月16日 23:53
Jacc [该话题已被删除] 中提及了此贴 12月17日 09:11

老蒋专业啊

liumomo [该话题已被删除] 中提及了此贴 12月18日 20:27
[该话题已被删除] 中提及了此贴 12月20日 02:11
安涛 [该话题已被删除] 中提及了此贴 12月21日 16:29
蒋刚毅 关闭了讨论 01月03日 17:24
蒋刚毅 重新开启了讨论 01月03日 17:24


大佬,Jenkins 上的 slave 一个节点离线,怎么启动呢

仅楼主可见

jenkins Configure global security 你们的版本有没这选项 master security -master control security

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册