如何用 CI (持续集成) 保证研发质量

1. 背景javascript

分布式技术的发展深入地改变了咱们编程的模式和思考软件的模式。分布式很好的解决性能扩展,可靠性,组件可用性等问题,可是单机转变成分布式却加大了系统的复杂性,对于组件的开发,测试,部署,发布都提出更高的要求。那么,针对复杂的分布式系统怎么保证软件质量和系统的稳定性?首先看下,传统软件产品活动的大体流程,简化流程大概是 3 大块:html

开发 -> QA -> 灰度上线

通常以下图:
java

流程有个很大的问题,质量全靠 QA 测,对接全靠人力,沟通成本大,遗漏问题多,通常有几个常见的问题:
node

  1. QA 很难每次都测试全面,毕竟QA毕竟是人,人的主观因素太大,有时候人为判断以为简单,不用测的地方极可能有漏了。或者以为修改点太简单,以为不至于出问题,就再也不全面的测试。以致于可能会有基本功能问题;python

  2. 测试速度慢,效率过低,QA 资源浪费,若是每次 QA 都须要全量的测试,那么重复工做太多,效率过低,效果也很差,对于这些重复工做,本能够更好,更快的解决,不至于 QA 就为了测试这么点东西,而没有精力去作更多的事情;nginx

  3. 甚至说编译不过的代码都有可能遗漏到 QA;git

  4. 闭环太慢,开发功能若是有问题,等到 QA 测出来,让后再反馈到开发这个闭环就太慢了。更没必要说,问题漏到线上,再反馈到开发人员,那么戴江就更大了;github

  5. 多个开发人员并行开发的时候,工做可能相互影响,小问题越积越多,功能集成的时候可能很是耗时;golang

咱们提出的改进点:web

  1. 核心点之一:问题发现要早,发现越早,代价越小;

  2. 核心点之一:问题闭环要快,闭环越快,效率越高;

  3. 重复工做自动化,减小人的无效劳做;

  4. 多开发人员的时候,功能持续集成,问题拆小,提前发现;

最核心的一点就是:”自动化闭环问题“。

当前的复杂的软件系统对质量和效率提出了更高的要求,因此响应的软件活动必需要高度自动化才能达到要求。自动化触发、自动化测试、自动化闭环、自动化发布、自动化卡点等一系列的保证,一切可以事先预知且可固化的行为都应该自动化,把效率和质量提高,而让人去作更聪明的事情。

咱们的思考:近些年来,对于自动化有 Continuous Integration,Continuous Delivery,Continuous Deployment 的一些理论和实践。这三者来讲,突出“持续”二字,“持续”是为了达到“快”的目的,“快速迭代”,“快速响应”,“快速闭环”,“快”是核心竞争力。通常你们的共识流程分类以下:

对于开发者来讲,接触到更多的是 Continuous Integration(持续集成),CI 把经过自动化,把流程固化下来,保证代码集成的有序、可靠,确保版本可控,问题可追溯,代码的活动中经过自动化,下降了人为主观的出错率,提升速度,提升版本质量和效率。

2. CI 是什么?

CI 便是持续集成(Continuous Integration),是当今软件活动中相当重要的一环。CI 通常由开发人员递交代码改动所触发,CI 在中间环境作自动化验证,CI 验证事后,即通过了基本质量保证,那么就能够容许下一步的软件活动。

持续集成说白了就是一种软件开发实践,即团队开发成员尽量的快的集成,每次集成经过自动化的构建(包括编译,发布,自动化测试)来验证,从而尽早地发现集成错误。由于问题发现的越早,那么问题解决的成本就越少。

通常来讲,持续集成须要打通几个环节:

  1. 代码提交(git)

  2. 任务构建(jenkins)

  3. 部署测试(ansible,shell,puppet)

划重点:CI 过程由代码活动触发

代码活动关注两个时间点:

  • Pre-Merge :代码改动合入主干分支前夕触发。集成的对象是代码改动与主干最新代码 Merge 以后的代码,目的是验证代码改动是否可以合入主干;

  • Post-Merge :代码改动合入主干分支以后触发。集成对象就是最新的主干分支代码,目的是验证合入改动代码以后主干是否可以正常工做;

Pre-Merge 和 Post-Merge 关注点不一样,缺一不可。差别在哪里?若是只有一个开发者,那么 Pre-Merge 和 Post-Merge 的测试对象是相同的。在多个开发者递交代码的时候,Pre-Merge 和 Post-Merge 就会呈现差别,他们的 CI 测试对象不一样。

换句话说,Pre-Merge 是并行的,每一个开发分支想要合入主干都会触发Pre-Merge CI,CI 的测试对象是 <开发分支+主干分支>, Post-Merge 是串行的,测试对象永远都是最新的主干分支代码。

3. CI 的四个思考

3.1 CI 怎么触发?

代码活动触发:

通常有两个触发点,Pre-Merge,Post-Merge ,分别是代码合入主干以前,主干代码合入以后。

定时触发。

3.2 CI 触发以后作什么?

CI 触发了以后作什么?说白了就是构建任务作了啥,通常有几个流程:

  1. Checkout,Pre-Merge 代码——校验 MR 合入是否合法;

  2. 代码编译 —— 校验代码编译是否合法;

  3. 静态检查 —— 校验静态语法是否合法;

  4. 单元测试 —— 回归测试函数单元合法;

  5. 冒烟测试 —— 简单测试系统是否正常;

  6. 接口测试 —— 测试用户接口是否正常;

  7. 性能基准测试 —— 测试性能是否符合预期;

3.3 怎么闭环问题?

先思考可能会有什么问题:

  1. Pre-Merge 代码冲突;

  2. 代码编译失败;

  3. 静态检查失败;

  4. 单元测试回归测试不经过;

  5. 冒烟测试步经过,接口测试失败。。。;

递交一个代码 MR 递交可能遇到以上问题,那么怎么才能快速闭环这个问题呢?

首先,得有手段通知到开发者

解决:

  1. MR 的 comment,CI 活动失败以后,直接以评论的方式自动添加到 MR;

  2. 邮件,触发的一次 MR ,失败了以邮件的方式发送到相关人;

再者,得有手段让开发人员知道问题

解决:

  1. 开发者知道本身的 MR 触发 CI 失败以后,得知道怎么去排查问题 —— 测试报告,

    • 好比单元测试失败,要有单元测试报告,接口测试失败要有接口测试报告;

  2. 每次构建任务保留归档线索,以便排查;

3.4 CI 构建活动输出什么?

  1. 单元测试报告;

  2. 接口测试报告;

  3. 代码覆盖率报告;

  4. 接口覆盖率报告;

  5. 构建版本包(持续化部署须要);

4. CI 平台选型

通常对多数的公司来讲,不须要本身研发一个 CI 平台,有不少优秀的开源 CI 平台工具,工具之间并无绝对的差别优点,这里就不进行选型了,咱们以一个 Jenkins 完整示例来讲明 CI 的使用方法和技巧。代码仓库咱们使用 Gitlab,CI 平台咱们使用开源的 Jenkins 做为演示。一步步完成咱们须要得几大模块功能。

平台选型:代码平台 Gitlab,CI 平台 Jenkins;

5. CI 的流程实践

CI 主要把关的是代码活动,通常有两个触发点:

  1. 代码合入主干前,触发 CI 测试,目的是校验本次合入是否符合质量预期,若是不符合,那么不许代码合入主干;

  2. 代码合入主干后,触发 CI 测试,目的是校验最新的主干分支是否符合质量预期;

Pre-Merge 触发过程

  1. 开发代码递交 Merge Request ( github 上习惯叫作 PR,gitlab 上习惯叫作 MR )

  2. MR 自动触发 CI 构建事件

  3. 运行 静态检查,Merge 检查,单元测试,冒烟测试,集成测试,所有经过以后,代码才容许 Merge 合入主干分支;

  4. 进行下一步软件活动

Post-Merge 触发过程

  1. 管理员审核 Merge Request 经过,代码合入主干,触发 Post-Merge 事件;

  2. CI 平台收到事件,自动进行 CI 构建;

  3. 构建完成,进行下一步软件活动;

6. Jenkins 平台构建

6.1 Jenkins 平台搭建

jenkins 是 java 程序开发的,安装是很是方便的,去官网上下载一个 war 包,而后后台拉起运行便可。运行命令以下:

启动

nohup /usr/bin/java -jar jenkins.war --httpPort=8888 >> jenkins.log 2>&1 &

这样 jenkins 平台就拉起来了,超简单。

6.1.1 初始化平台

初始密钥

平台第一次搭建须要作一些配置,在日志里找到一个“初始密钥”注意一下提示:

***************************************************************************************************************************************************************************************
Jenkins initial setup is required. An admin user has been created and a password generated.Please use the following password to proceed to installation:
5ddce31f4a0d48b4b7d6d71ff41d94a8
This may also be found at: /root/software/jenkins/workhome/secrets/initialAdminPassword
***************************************************************************************************************************************************************************************

这个是最初始的超级用户密码,待会配置的 jenkins 时候会用上,因此赶忙拿个小本本记下。

登陆网页

如今打开浏览器,登陆 jenkins 的网页,咱们下面作 jenkins 的初始化:

点击 “Continue”,进行下一步,下一步就是定制一些插件安装了。

第一次定制插件安装

这个是可选的,这个根据本身需求选择插件,为了省事,选第一种就好,以后也很方便在平台上下载插件。

安装插件过程(jenkins 几乎彻底由插件组装起功能):

更新成功的就会显示“绿色”。插件安装完以后,下一步就是配置第一个超级用户了。

配置完成以后,点击 "Save and Continue" ,最后一步,配置 url ,点击 finish 便可:

如今基本配置已经完成,能够开始愉快的使用 jenkins 了。

6.1.2 使用小技巧

中文化配置

jenkins 平台搭建好以后,默认的是英文的,在国内的话可能不必,咱们能够安装中文化插件来更友好的展现咱们的 jenkins。分两个步骤:

步骤一:安装插件:”Locale“:

"Manage Jenkins" -> "Manage Plugins" -> "Available"

步骤二:安装完以后,配置 Configure

6.2 Jenkins 插件安装

6.2.1 插件更新地址

这里推荐国内的插件源地址,由于官网的网络访问不是很稳定。好比如下是清华的镜像源。

https://mirrors.tuna.tsinghua.edu.cn/jenkins/updates/update-center.json

6.2.2 必备插件安装

jenkins 功能都是由插件提供,有些插件是必须配备的,才能提供完备的 CI 功能,好比流水线 Pipeline 。这里列举几大关键插件的使用方法。

pipeline

jenkins 必备插件,流水线插件,可否很是方便的让你定义流程,调度节点,分配资源,处理结果等。

blue ocean

pipeline 的可视化插件,pipeline 仍是声明式代码编写,若是要能让人更方便的使用,那么须要一个可视化的工具,blue ocean 就是为此而生。

junit

测试报告的一个解析插件,这也是一种较为通用的测试报告格式。

Cobertura Plugin

覆盖率展现的一个插件。单测跑完,须要有手段知道覆盖率的状况,而且须要能方便的闭环处理。

  1. 显示覆盖率的状况;

  2. 代码的覆盖详情,方便开发人员闭环处理(细化到每一行代码);

GitLab

咱们的演示以 Gitlab 做为例子,须要和 GitLab 进行交互,因此须要安装插件用来接受 GitLab 事件,并反馈 CI 结果。

6.3 Jenkins 任务建立

6.3.1 建立任务(item)

item 就是 CI 的项目,item 由管理员静态建立配置好,触发起来就是 job 了。每触发一次,job 编号都是递增的。点击 New Item 建立一个”流水线“ 的项目。

6.3.2 建立视图(view)

View 是什么概念?View 能够把一些有业务意义的任务概括起来,在一个列表中显示。能够点击 ”New View“ 进行建立。

视图会展现 item ,你能够选择性的勾选。

6.4 Jenkins 流水线

流水线框架是经过 Pipeline 脚原本描述流程,Pipeline 有两种建立方式,两种语法:

  1. 声明式流水线语法

  2. 脚本化流水线语法

如今官方推荐的是声明式流水线语法。那么,声明式的语法是什么样子的?声明式语法特色之一:顶层必须以 pipeline {} 开始。

7. Jenkins 和 Gitlab 交互

这一步就是最关键的东西,Jenkins 搭建好了以后,若是只是一个孤岛平台,那么没有任何意义,它必须参与到软件开发流程中去才能发挥效果。交互示意图以下:

咱们看到,Gitlab 的代码活动须要以事件的形式触发 Jenkins,Jenkins 执行完一系列活动以后,须要把结果反馈到 Gitlab,而且可以影响到 Gitlab 的下一步活动,因此 Gitlab 和 Jenkins 须要相互配置关联。

7.1 Gitlab 配置

为何须要配置 gitlab ?由于须要打通 gitlab 到 jenkins 的路。gitlab 做为代码仓库,主要产生项目代码相关的事件,好比 Merge Request,Push Commit 等,当 gitlab 产生这些事件的时候,须要自动把这个事件推送给 jenkins ,这样就打通了触发交互。

7.1.1 配置 Web Hook 事件

操做步骤:

  1. 打开代码仓库

  2. 点击 setting -> integrations

    1. 填入 URL

    2. 填入 Secret Token

    3. 勾选 Trigger 事件

URL 和 Secret Token 怎么来的?这个是对应到 item 。

在 Jenkins 平台上,打开对应的 item,打开 Configure ,勾选 Build Triggers ,找到 ”Build when a change is pushed to GitLab“ ,就是这个了。

再往下有一个 Secret token ,点击 Generate 。

把这两个正确填写好,那么就能打通第一个环节了:Gitlab 到 Jenkins 的触发。填写完以后,能够由 GitLab 发个测试事件测试下。

返回 200 便是成功了。

7.1.2 配置 Pre-Merge 卡点

代码 Pre-Merge CI 没过不让合入主干 这个功能怎么实现?关键是 GitLab 要支持代码 Merge 前夕的 Hook 行为。

  1. 首先,咱们约定一个行为规范:全部合入主干的代码必须递交 MR,MR CI 测试经过才能够合入主干;

  2. 其次,勾选 Settings -> General -> Merge requests ,把 ”Only allow merge requests to be merged if the pipeline succeeds“ 勾选上;

7.2 Jenkins 配置

Jenkins 主要看 Pipeline 的配置,Pipeline 配置打开 Configure 以下:

看一个完整的 pipeline 架子定义各个阶段(能够直接把这个拷贝,运行看下效果):

pipeline { agent any stages { stage('代码checkout') { steps { echo "------------" } } stage ("静态检查") { steps { echo "------------" } } stage ("代码编译") { steps {  echo "------------" } } stage ("单元测试") { steps { echo "------------" } } stage ("打包") { steps { echo "------------" } } stage ("冒烟测试") { steps { echo "------------" } } stage ("集成测试") { steps { echo "------------" } } stage ("基准性能测试") { steps { echo "------------" } } } post { always { echo "------------" } success { echo "------------" } failure { echo "------------" } unstable { echo "------------" } }}

跑出来的效果:

Blue Ocean 的效果:

接下来,咱们拆解几个关键的阶段来分析。

7.2.1 代码 checkout

Checkout 的代码也就是咱们的测试对象,这个对于 Pre-merge 和 Post-merge 是不一样的,Pre-merge 卡点由 Merge Request 事件触发,咱们须要 Checkout 出 ”代码修改“ + ”最新主干分支“ 的代码。Post-merge 相对简单,咱们只须要 Checkout 出最新的主干分支便可。怎么作?

pipeline 直接支持这个使用。

stage('代码checkout') { steps { dir(path: "${你想要放置的代码路径}") { checkout changelog: true, poll: true, scm: [ $class: 'GitSCM',  branches: [[name: "*/${env.gitlabSourceBranch}"]], doGenerateSubmoduleConfigurations: false,  extensions: [ [$class: 'PreBuildMerge', options: [fastForwardMode: 'FF', mergeRemote: 'origin', mergeStrategy: 'DEFAULT', mergeTarget: "${env.gitlabTargetBranch}" ]], [ $class: 'UserIdentity', email: "${env.gitlabUserEmail}", name: "${env.gitlabUserName}" ] ], submoduleCfg: [], userRemoteConfigs: [[ credentialsId: "${env.OCS_GITLAB_CredentialsId}",  url: "${env.gitlabSourceRepoHttpUrl}" ]] ] }  } }

这里指明:

  • 代码 checkout 下来放置的路径(dir path 配置);

  • 指明 checkout 的分支,MR 事件触发 CI 的时候 env.gitlabSourceBranch 是会自动设置上的;

  • 指明 checkout 的行为,PreBuildMerge 行为(其实 Post-Merge 触发 PUSH 事件的时候,也适用于上面的写法);

  • 指明 gitlab 认证凭证(credentialsId);

上面的语法同时适用于 Pre-merge 和 Post-merge 。

7.2.2 静态检查

静态检查主要是对代码语法作一些静态检查,好比 golang ,可使用自带的 go vet ,或者 go fmt 等检查,通过这一轮检查就能保证你们的代码消除了最基本的语法和格式错误。

7.2.3 单元测试

对最小的函数作单元测试是必要的,通过单元测试能够拿到项目的一个覆盖率状况。这个阶段咱们得到两个东西:

  • 单元测试案例报告

  • 覆盖率报告

测试报告怎么拿?以 golang 为例,跑单测的时候,把覆盖率的开关打开,标准输出到一个文件:

go test -cover -coverprofile=cover.output xxx | tee ut.output

这里会产生两个文件:

  • ut.output :用来生成单元测试报告的文件;

  • cover.output :用来生成覆盖率报告的文件;

单测报告生成

首先解析这个文件成 xml 格式的文件,而后用 junit 上报给 jenkins 展现。

sh "go-junit-report < ut.output > ut.xml"junit 'ut.xml'

go-junit-report 哪里来的?这是个开源的工具,就是专门用来作单元测试解析的。

在 jenkins 上展现的效果以下:

覆盖率报告生成

解析覆盖率输出文件,生成一个xml 文件:

gocov convert cover.output | gocov-xml > cover.xml

上报这个 xml 文件,用于 jenkins 平台展现:

step([ $class: 'CoberturaPublisher',  autoUpdateHealth: false,  autoUpdateStability: false,  coberturaReportFile: '**/cover.xml',  failUnhealthy: false,  failUnstable: false,  maxNumberOfBuilds: 0,  onlyStable: false,  sourceEncoding: 'ASCII',  zoomCoverageChart: false ])



点击进文件,能够看到代码覆盖的详情:

7.2.4 接口测试

对完整的系统作一些接口级别的测试,好比模拟用户行为,测试用户调用的接口,这样能保证最基本的功能。报告输出也能够用junit 格式,能够上报给jenkins,解析如图:

7.2.5 邮件发送

测试经过或者失败,须要发送有结果邮件。

configFileProvider([configFile(fileId: '5f1e288d-71ee-4d29-855f-f3b22eee376c', targetLocation: 'email.html', variable: 'content')]) { script { template = readFile encoding: 'UTF-8', file: "${content}" emailext( subject: "CI构建结果: ${currentBuild.result?: 'Unknow'}", to: "test@test.com", from: "push@test.com",  body: """${template}""" ) }}

7.2.6 Gitlab 状态交互

// 定义 Gitlab 流程options { gitLabConnection('test-gitlab') gitlabBuilds(builds: ['jenkinsCI'])}
// 触发gitlab pipeline updateGitlabCommitStatus name: 'jenkinsCI', state: 'success' addGitLabMRComment comment: """**CI Jenkins自动构建详情**\n| 条目 | 值 || ------ | ------ || 结果 | ${currentBuild.result?: 'Unknow'} || MR LastCommit | ${env.gitlabMergeRequestLastCommit} | | MR id | ${env.gitlabMergeRequestIid} || Message Title | ${env.gitlabMergeRequestTitle} || 构建任务ID | ${env.BUILD_NUMBER} || 构建详情连接 | [${env.RUN_DISPLAY_URL}](${env.RUN_DISPLAY_URL})"""

CI 成功或失败,都须要把这个状态给到 gitlab,咱们以一个 Comment 展现结果,而且附上 jenkins 任务的跳转连接,这样能够最快的帮助开发人员闭环。

成功才容许合入 :

Gitlab CI


7.2.7 构建归档

打包日志:

// 先打一个 tar 包sh "tar -czvf log.tar.gz ${SERVICEDIR}/run/*.log"// jenkins 进行归档archiveArtifacts allowEmptyArchive: true, artifacts: "log.tar.gz", followSymlinks: false

8. Jenkins 高级技巧

8.1 资源互斥

有时候多个任务跑的时候,可能会并发使用到某个资源,而若是这个资源有限,那么可能须要用到一些互斥手段来保证。好比,两个任务可能都用到了 mongodb,而 mongodb 若是只有一套,那么就必须让多个任务串行执行才行,否则就会跑错了逻辑。怎么作?

这个能够在 “Configure System”->“Lockable Resources Manager” 定义好锁资源:

而后再 Pipeline 脚本里使用这个锁资源:

stage ("单元测试") { steps { lock(resource: "UT_TEST", quantity:1) { echo "====== 单元测试 ============" echo "====== 单元测试完成 ============"      } }}

而且还能够在界面上( Dashboard -> Lockable Resources )看到哪些资源被哪些任务占用:

经过合理定义锁资源,咱们就能作到任务能够并发,可是关键的竞态资源作互斥,这样 CI 构建任务更灵活,更有效率(这个能够类比成代码里面锁粒度的一个影响,若是你不用 Lock Resource 这种方式,那么极可能只能配置成 node 并发度为1才能保护到竞态资源)。

8.2 节点调度

jenkins 容许你调度指定的任务到合适的节点。当有多个节点的时候,可能会想要任务 A 固定到 node1 上执行,那么可使用 agent 命令指定。

定义节点的时候每一个节点都会赋有一个 label 名称,而后运行的时候,就能够指定节点了:

agent { label "slave_node_1" }

8.3 节点间文件传输

咱们使用 stash, unstash 来实现, 下面的例子就是把 build/ 目录在 node1 和 node2 直接作无损传输:

stage ("打包") { agent { label "slave_node_1" } steps { // 节点1上,把 Build 目录下的都打包; stash (name: "buildPkg", includes: "build/**/*") }}
stage ("冒烟部署") { agent { label "slave_node_2" } steps { // 节点2上,解包 unstash ("buildPkg") }}

8.4 节点的后置清理

在流水线多节点切换的时候,须要注意下你所在的节点是哪一个,千万别晕头了。

pipeline { agent { label "master" } stages { stage ("测试") { agent { label "slave_node_1" } steps { } // 阶段后置 post { always { // 清理 slave_node_1 的构建空间 cleanWs() } } }
} // 流水线总后置 post { always { // 只清理 master 节点的构建空间 cleanWs() } }}

多节点的时候,必定要记得分别清理节点。

9. 总结

经过使用合理的技术平台,把人与事合理的关联,现代软件开发活动中,CI 是必不可少的流程,开发人员身在其中,CI 以代码活动为起点,构建结果能能快速响应到对应人,并提供手段让对应人快速解决,最后提供直观的报告。咱们经过 Jenkins(CI平台) + Gitlab(仓库)来演示完整搭建流程,展现一个可实践的过程。一切都是为了软件开发效率和版本质量。


☆ END ☆


招聘信息

OPPO联网云平台团队招聘一大波岗位,涵盖Java、容器、Linux内核开发、产品经理、项目经理等多向,请在公众号后台回复关键词“云招聘”查看查详细信息。


你可能还喜欢

OPPO自研ESA DataFlow架构与实践

数据同步一致性保障:OPPO自研JinS数据同步框架实践

微服务全链路异步化实践

Dubbo协议解析与ESA RPC实践

如何设计并实现存储QoS?



更多技术干货

扫码关注

OPPO互联网技术

 

我就知道你“在看”

本文分享自微信公众号 - OPPO互联网技术(OPPO_tech)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。

相关文章
相关标签/搜索