基于Jenkins和Kubernetes的持续集成测试实践

使用虚拟机做为Jenkins Slave的方式,存在诸多弊端,好比资源分配不均、资源浪费和扩容不便。而使用Kubernetes容器云做为Jenkins Slave则能够完美解决这些问题。html

在这里插入图片描述

目前公司为了下降机器使用成本,对全部的AWS虚拟机进行了盘点,发现利用率低的机器中,有一部分是测试团队用做Jenkins Slave的机器。这不出咱们所料,使用虚拟机做为Jenkins Slave,必定会存在很大浪费,由于测试Job运行完成后,Slave 处于空闲状态时,虚拟机资源并无被释放掉。java

除了资源利用率不高外,虚拟机做为Jenkins Slave还有其余方面的弊端,好比资源分配不均衡,有的 Slave 要运行的 job 出现排队等待,而有的 Slave 可能正处于空闲状态。另外,扩容不方便,使用虚拟机做为Slave,想要增长Jenkins Slave,须要手动挂载虚拟机到Jenkins Master上,并给Slave配置环境,致使管理起来很是不方便,维护起来也是比较耗时。node

在2019年,运维团队搭建了Kubernetes容器云平台。为了实现公司下降机器使用成本的目标,我所在的车联网测试团队考虑将Jenkins Slave全面迁移到Kubernetes容器云平台。主要是想提升Jenkins Slave资源利用率,而且提供比较灵活的弹性扩容能力知足愈来愈多的测试Job对Slave的需求。python

本文就是咱们的实践总结。linux

01 — 总体架构

咱们知道Jenkins是采用的Master-Slave架构,Master负责管理Job,Slave负责运行Job。在咱们公司Master搭建在一台虚拟机上,Slave则来自Kubernetes平台,每个Slave都是Kubernetes平台中的一个Pod,Pod是Kubernetes的原子调度单位,更多Kubernetes的基础知识不作过多介绍,在这篇文章中,你们只要记住Pod就是Jenkins Slave就好了。git

基于 Kubernetes 搭建的 Jenkins Slave 集群示意图以下。github

在这里插入图片描述

在这个架构中,Jenkins Master 负责管理测试Job,为了可以利用Kubernetes平台上的资源,须要在Master上安装Kubernetes-plugin。web

Kubernetes平台负责产生Pod,用做Jenkins Slave执行Job任务。当Jenkins Master上有Job被调度时,Jenkins Master经过Kubernetes-plugin向Kubernetes平台发起请求,请Kubernetes根据Pod模板产生对应的Pod对象,Pod对象会向Jenkins Master发起JNLP请求,以便链接上Jenkins Master,一旦链接成功,就能够在Pod上面执行Job了。算法

Pod中所用的容器镜像则来自Harbor,在这里,一个Pod中用到了三个镜像,分别是Java镜像、Python镜像、JNLP镜像。Java镜像提供Java环境,可用来进行编译、执行Java编写的测试代码,Python镜像提供Python环境,用来执行Python编写的测试代码,JNLP镜像是Jenkins官方提供的Slave镜像。docker

使用Kubernetes做为Jenkins Slave,如何解决前面提到的使用虚拟机时的资源利用率低、资源分配不均的问题,而且实现Slave动态弹性扩容的呢?

首先,只有在Jenkins Master有Job被调度时,才会向Kubernetes申请Pod建立Jenkins Slave,测试Job执行完成后,所用的Slave会被Kubernetes回收。不会像虚拟机做为Slave时,有Slave闲置的状况出现,从而提升了计算资源的利用率。

其次,资源分配不均衡的主要问题在于不一样测试小组之间,由于测试环境和依赖不一样而不能共享Jenkins Slave。而Kubernetes平台打破了共享的障碍,只要Kubernetes集群中有计算资源,那么就能够从中申请到适合本身项目的Jenkins Slave,从而再也不会发生Job排队的现象。

借助Kubernetes实现Slave动态弹性扩容就更加简单了。由于Kubernetes天生就支持弹性扩容。当监控到Kubernetes资源不够时,只须要经过运维平台向其中增长Node节点便可。对于测试工做来说,这一步彻底是透明的。

02 — 配置Jenkins Master

要想利用Kubernetes做为Jenkins Slave,第一步是在Jenkins Master上安装Kubernetes插件。安装方法很简单,用Jenkisn管理员帐号登陆Jenkins,在Manage Plugin页面,搜索Kubernetes,勾选并安装便可。

接下来就是在Jenkins Master上配置Kubernetes链接信息。Jenkins Master链接Kubernetes云须要配置三个关键信息:名称、地址和证书。所有配置信息以下图所示。

在这里插入图片描述

名称将会在Jenkins Pipeline中用到,配置多个Kubernetes云时,须要为每个云都指定一个不一样的名称。

Kubernetes地址指的是Kubernetes API server的地址,Jenkins Master正是经过Kubernetes plugin向这个地址发起调度Pod的请求。

Kubernetes服务证书key是用来与Kubernetes API server创建链接的,生成方法是,从Kubernetes API server的/root/.kube/config文件中,获取/root/.kube/config中certificate-authority-data的内容,并转化成base64 编码的文件便可。

# echo certificate-authority-data的内容 | base64 -D > ~/ca.crt

ca.crt的内容就是Kubernetes服务证书key。

上图中的凭据,是使用客户端的证书和key生成的pxf文件。先将/root/.kube/config中client-certificate-data和client-key-data的内容分别转化成base64 编码的文件。

# echo client-certificate-data的内容 | base64 -D > ~/client.crt
# echo client-key-data的内容 | base64 -D > ~/client.crt

根据这两个文件制做pxf文件:

# openssl pkcs12 -export -out ~/cert.pfx -inkey ~/client.key -in ~/client.crt -certfile ~/ca.crt
# Enter Export Password:
# Verifying - Enter Export Password:

自定义一个password并牢记。

点击Add,选择类型是Cetificate,点击Upload certificate,选取前面生成cert.pfx文件,输入生成cert.pfx文件时的密码,就完成了凭据的添加。

接着再配置一下Jenkins URL和同时能够被调度的Pod数量。

配置完毕,能够点击 “Test Connection” 按钮测试是否可以链接到 Kubernetes,若是显示 Connection test successful 则表示链接成功,配置没有问题。

配置完Kubernetes插件后,在Jenkins Master上根据须要配置一些公共工具,好比我这了配置了allure,用来生成报告。这样在Jenkins Slave中用到这些工具时,就会自动安装到Jenkins Slave中了。

在这里插入图片描述

03 — 定制Jenkins Pipeline

配置完成Kubernetes链接信息后,就能够在测试Job的Pipeline中使用kubernetes做为agent了。与使用虚拟机做为Jenkins Slave的区别主要在于pipeline.agent部分。下面代码是完整的Jenkinsfile内容。

pipeline {
    agent {
      kubernetes{
          cloud 'kubernetes-bj' //Jenkins Master上配置的Kubernetes名称
          label 'SEQ-AUTOTEST-PYTHON36' //Jenkins slave的前缀
          defaultContainer 'python36' // stages和post步骤中默认用到的container。如需指定其余container,可用语法 container("jnlp"){...}
          idleMinutes 10 //所建立的pod在job结束后的空闲生存时间
          yamlFile "jenkins/jenkins_pod_template.yaml" // pod的yaml文件
      }
    }
    environment {
        git_url = 'git@github.com:liuchunming033/seq_jenkins_template.git'
        git_key = 'c8615bc3-c995-40ed-92ba-d5b66'
        git_branch = 'master'
        email_list = 'liuchunming@163.com'
    }
    options {
        buildDiscarder(logRotator(numToKeepStr: '30'))  //保存的job构建记录总数
        timeout(time: 30, unit: 'MINUTES')  //job超时时间
        disableConcurrentBuilds() //不容许同时执行流水线
    }
    stages {
        stage('拉取测试代码') {
            steps {
                git branch: "${git_branch}", credentialsId: "${git_key}", url: "${git_url}"
            }
        }
        stage('安装测试依赖') {
            steps {
                sh "pipenv install"
            }
        }
        stage('执行测试用例') {
            steps {
                sh "pipenv run py.test"
            }
        }
    }
    post {
        always{
            container("jnlp"){ //在jnlp container中生成测试报告
                allure includeProperties: false, jdk: '', report: 'jenkins-allure-report', results: [[path: 'allure-results']]
            }   
        }
    }
}

上面的Pipeline中,与本文相关的核心部分是agent.kubernetes一段,这一段描述了如何在kubernetes 平台生成Jenkins Slave。

cloud,是Jenkins Master上配置的Kubernetes名称,用来标识当前的Pipeline使用的是哪个Kubernetes cloud。

label,是Jenkins Slave名称的前缀,用来区分不一样的Jenkins Slave,当出现异常时,能够根据这个名称到Kubernetes cloud中进行debug。

defaultContainer,在Jenkins Slave中我定义了是三个container,在前面有介绍。defaultContainer表示在Pipeline中的stages和post阶段,代码运行的默认container。也就是说,若是在stages和post阶段不指定container,那么代码都是默认运行在defaultContainer里面的。若是要用其余的container运行代码,则须要经过相似container(“jnlp”){…}方式来指定。

idleMinutes,指定了Jenkins Slave上运行的测试job结束后,Jenkins Slave能够保留的时长。在这段时间内,Jenkins Slave不会被Kubernetes回收,这段时间内若是有相同label的测试Job被调度,那么能够继续使用这个空闲的Jenkins Slave。这样作的目的是,提升Jenkins Slave的利用率,避免Kubernetes进行频繁调度,由于成功产生一个Jenkins Slave仍是比较耗时的。

yamlFile,这个文件是标准的Kubernetes的Pod 模板文件。Kubernetes根据这个文件产生Pod对象,用来做为Jenkins Slave。这个文件中定义了三个容器(Container)以及调度的规则和外部存储。这个文件是利用Kubernetes做为Jenkins Slave集群的核心文件,下面将详细介绍这个文件的内容。

至此,测试Job的Pipeline就创建好了。

04 — 定制Jenkins Slave模板

使用虚拟机做为Jenkins Slave时,若是新加入一台虚拟机,咱们须要对虚拟机进行初始化,主要是安装工具软件、依赖包,并链接到Jenkins Master上。使用Kubernetes cloud做为Jenkins Slave集群也是同样,要定义Jenkins Slave使用的操做系统、依赖软件和外部磁盘等信息。只不过这些信息被写在了一个Yaml文件中,这个文件是Kubernetes的Pod 对象的标准模板文件。Kubernetes会自根据这个Yaml文件,产生Pod并链接到Jenkins Master上。

这个Yaml文件内容以下:

apiVersion: v1
kind: Pod
metadata:
  # ① 指定 Pod 将产生在Kubernetes的哪一个namespace下,须要有这个namespace的权限
  namespace: sqe-test  
spec:
  containers:
    # ② 必选,负责链接Jenkins Master,注意name必定要是jnlp
    - name: jnlp
      image: swc-harbor.nioint.com/sqe/jnlp-slave:root_user
      imagePullPolicy: Always
      # 将Jenkins的WORKSPACE(/home/jenkins/agent)挂载到jenkins-slave
      volumeMounts:
        - mountPath: /home/jenkins/agent
          name: jenkins-slave
​
    # ③ 可选,python36环境,已安装pipenv,负责执行python编写的测试代码
    - name: python36
      image: swc-harbor.nioint.com/sqe/automation_python36:v1
      imagePullPolicy: Always
      # 经过cat命令,让这个container保持持续运行
      command:
        - cat
      tty: true
      env:
        # 设置pipenv的虚拟环境路径变量 WORKON_HOME
        - name: WORKON_HOME 
          value: /home/jenkins/agent/.local/share/virtualenvs/
      # 建立/home/jenkins/agent目录并挂载到jenkins-slave Volume上
      volumeMounts: 
        - mountPath: /home/jenkins/agent
          name: jenkins-slave
      # 能够对Pod使用的资源进行限定,可调。尽可能不要用太多,够用便可。
      resources: 
        limits:
          cpu: 300m
          memory: 500Mi
​
    # ④ 可选,Java8环境,已安装maven,负责执行Java编写的测试代码
    - name: java8
      image: swc-harbor.nioint.com/sqe/automation_java8:v2
      imagePullPolicy: Always
      command:
        - cat
      tty: true
      volumeMounts:
        - mountPath: /home/jenkins/agent
          name: jenkins-slave
​
  # ⑤ 声明一个名称为 jenkins-slave 的 NFS Volume,多个container共享
  volumes:
    - name: jenkins-slave
      nfs:
        path: /data/jenkins-slave-nfs/
        server: 10.125.234.64
  # ⑥ 指定在Kubernetes的哪些Node节点上产生Pod
  nodeSelector:
    node-app: normal
    node-dept: sqe

经过上面的Yaml文件,能够看到经过 spec.containers 在Pod中定义了三个容器,分别是负责链接Jenkins Master的jnlp,负责运行Python代码的python36,负责运行Java代码的java8。咱们能够把Jenkins Slave比喻成豆荚,里面的容器比喻成豆荚中的豆粒,每颗豆粒具备不一样的职责。

在这里插入图片描述

同时,还声明了一个叫做jenkins-slave 的volume,jnlp 容器将Jenkins WORKSPACE目录(/home/jenkins/agent )mount到jenkins-slave 上。同时python36和java8这两个容器也将目录/home/jenkins/agent mount到jenkins-slave 上。从而,在任何一个容器中对/home/jenkins/agent 目录的修改,在其余两个容器中都能读取到修改后的内容。挂载外部存储的主要好处是能够将测试结果、虚拟环境持久化下来,特别是将虚拟环境持久化下来以后,不用每次执行测试建立新的虚拟环境,而是复用已有的虚拟环境,加快了整个测试执行的过程。

另外,还指定了使用kubernetes的哪个Namespace命名空间以及在哪些Node节点上产生Jenkins Slave。关于这个Yaml文件的其余细节说明,我都写在了文件的注释上,你们能够参考着理解。

05 — 定制容器镜像

前面介绍了Jenkins Slave中用到了三个容器,下面咱们分别来看下这三个容器的镜像。

首先,DockerHub(https://hub.docker.com/r/jenkinsci/jnlp-slave)提供了Jenkins Slave的官方镜像,咱们这里将官方镜像中的默认用户切换成root用户,不然在执行测试用例时,可能会出现权限问题。JNLP容器镜像的Dockerfile以下:

FROM jenkinsci/jnlp-slave:latest
LABEL maintainer="liuchunming@163.com"
USER root

Python镜像是在官方的Python3.6.4镜像中安装了pipenv。由于咱们团队目前的Python项目都是用pipenv管理项目依赖的。这里说一下,pipenv是pip的升级版,它既能为你项目建立独立的虚拟环境,还可以自动维护和管理项目的依赖软件包。与pip使用requirements.txt管理依赖不一样,pipenv使用Pipefile管理依赖,这里的好处不展开介绍,有兴趣的朋友能够查看一下pipenv的官方文档https://github.com/pypa/pipenv。Python镜像的Dockerfile以下:

FROM python:3.6.4
LABEL maintainer="xxx@163.com"
USER root
RUN pip install --upgrade pip
RUN pip3 install pipenv

Java镜像是根据DockerHub上的maven镜像扩展来的。主要改动则是将公司内部使用的maven配置文件settings.xml放到镜像里面。完整的Dockerfile以下:

FROM maven:3.6.3-jdk-8
LABEL maintainer="xxx@163.com"
USER root

# 设置系统时区为北京时间
RUN mv /etc/localtime /etc/localtime.bak && \
    ln -s /usr/share/zoneinfo/Asia/Shanghai  /etc/localtime && \
    echo "Asia/Shanghai" > /etc/timezone # 解决JVM与linux系统时间不一致问题
# 支持中文
RUN apt-get update && \
    apt-get install locales -y && \
    echo "zh_CN.UTF-8 UTF-8" > /etc/locale.gen && \
    locale-gen
# 更新资源地址
ADD settings.xml /root/.m2/
​
# 安装jacococli
COPY jacoco-plugin/jacococli.jar  /usr/bin
RUN  chmod +x /usr/bin/jacococli.jar

制做完容器镜像以后,咱们会将其push到公司内部的harbor上,以便kubernetes可以快速的拉取镜像。你们能够根据本身实际状况,按照项目需求制做本身的容器镜像。

06 — 执行自动化测试

经过前面的步骤,咱们使用Kubernetes做为Jenkins Slave的准备工做就所有完成了。接下来就是执行测试Job了。与使用虚拟机执行测试Job相比,这一步其实彻底相同。

建立一个Pipeline风格的Job,并进行以下配置:
在这里插入图片描述

配置完成后,点击Build就能够开始测试了。

07 — 性能优化

跟虚拟机做为Jenkins Salve不一样,Kubernetes生成Jenkins Slave是个动态建立的过程,由于是动态建立,就涉及到效率问题。解决效率问题能够从两方面入手,一方面是尽可能利用已有的Jenkins Slave来运行测试Job,另外一方面是加快产生Jenkins Slave的效率。下面咱们分别从这两方面看看具体的优化措施。

7.1 充分利用已有的Jenkins Slave

充分利用已有的Jenkins Slave,能够从两方面入手。

一方面,设置idleMinutes让Jenkins Slave在执行完测试Job后,不要被当即消毁,而是能够空闲一段时间,在这段时间内若是有测试Job启动,则能够分配到上面来执行,既提升了已有的Jenkins Slave的利用率,也避免建立Jenkins Slave耗费时间。

另外一方面,在更多的测试Job流水线中,使用相同的label,这样当前面的测试Job结束后,所使用的Jenkins Slave也能被即将启动的使用相同lable的测试Job所使用。好比,测试job1使用的jenkins Slave 的lable是

DD-SEQ-AUTOTEST-PYTHON,那么当测试job1结束后,使用相同lable的测试job2启动后,既能够直接使用测试job1使用过的Jenkins Slave了。

7.2 加快Jenkins Slave的调度效率

Kubernetes上产生Jenkins Slave并加入到Jenkins Master的完整流程是:

  1. Jenkins Master计算如今的负载状况;
  2. Jenkins Master根据负载状况,按需经过Kubernetes Plugin向Kubernetes API server发起请求;
  3. Kubernetes API server向Kubernetes集群调度Pod;
  4. Pod产生后经过JNLP协议自动链接到Jenkins Master。

后三个步骤都是很快的,主要受网络影响。而第一个步骤,Jenkins Master会通过一系列算法计算以后,发现没有可用的Jenkins Slave才决定向Kubernetes API server发起请求。这个过程在Jenkins Master的默认启动配置下是不高效的。常常会致使一个新的测试Job启动后须要等一段时间,才开始在Kubernetes上产生Pod。

所以,需求对Jenkins Master的启动项进行修改,主要涉及如下几个参数:

-Dhudson.model.LoadStatistics.clock=2000 
-Dhudson.slaves.NodeProvisioner.recurrencePeriod=5000 
-Dhudson.slaves.NodeProvisioner.initialDelay=0 
-Dhudson.model.LoadStatistics.decay=0.5 
-Dhudson.slaves.NodeProvisioner.MARGIN=50 
-Dhudson.slaves.NodeProvisioner.MARGIN0=0.85

Jenkins Master每隔一段时间会计算集群负载,时间间隔由hudson.model.LoadStatistics.clock决定,默认是10秒,咱们将其调整到2秒,以加快 Master计算集群负载的频率,从而更快的知道负载的变化状况。好比原来最快须要10秒才知道目前有多少job须要被调度执行,如今只须要2秒。

当Jenkins Master计算获得集群负载后,发现没有可用的Jenkins Slave。Jenkins master会通知Kubernetes Plugin的NodeProvisioner以recurrencePeriod间隔生产Pod。所以recurrencePeriod值不能比hudson.model.LoadStatistics.clock小,不然会生成多个Jenkins slave。

initialDelay是一个延迟时间,本来用于确保让静态的Jenkins Slave和Master创建起来链接,由于咱们这里是使用Kubernetes插件动态产生Jenkins slave,没有静态Jenkins Slave,因此咱们将参数设置成0。

hudson.model.LoadStatistics.decay这个参数本来的意义是用于抑制评估master负载的抖动,对于评估获得的负载值有很大影响。默认decay是0.9。咱们把decay设成了0.5,容许负载有比较大的波动,Jenkins Master评估的负载就是在当前尽量真实的负载之上,评估的须要的Jenkins Slave的个数。

hudson.slaves.NodeProvisioner.MARGIN 和hudson.slaves.NodeProvisioner.MARGIN0,这两个参数使计算出来的负载作整数向上对齐,从而可能多产生一个Slave,以此来提升效率。

将上面的参数,加入到Jenkins Mater启动进程上,重启Jenkins Master即生效。

java -Dhudson.model.LoadStatistics.clock=2000 -Dxxx -jar jenkins.war

08 — 总结

本文介绍了使用Kubernetes做为持续集成测试环境的优点,并详细介绍了使用方法,对其性能也进行了优化。经过这个方式完美解决虚拟机做为Jenkins Slave的弊端。除了自动化测试可以从Kubernetes中收益以外,在性能测试环境搭建过程当中,借助Kubernetes动态弹性扩容的机制,对于大规模压测集群的建立,在效率、便捷性方面更具备明显优点。

参考资料:

https://github.com/jenkinsci/kubernetes-plugin

https://github.com/jenkinsci/kubernetes-plugin/tree/master/examples

https://jenkins.io/doc/book/managing/system-properties/

https://issues.jenkins-ci.org/browse/JENKINS-5780

https://www.jianshu.com/p/6b5dfae4a95b

https://www.cnblogs.com/guguli/p/7827435.html