[持续交付实践] 最后一公里,你需要一套具备质量思维的发布平台!

牧云@^-^@ 提交于 2019-11-29 10:10:46

前言

发布是持续交付的最后一公里。
传统上,软件的最终发布是个充满压力的过程,需要大量的手工配置、操作和团队配合。为了发布的可靠性,开发人员需要准备详尽的部署文档,然后再把相关信息同步给运维人员执行部署,由运维人员执行一系列个性化的发布脚本,部署完后还需要测试人员做详尽的手工验证。
每个步骤里都有很多需要人为判断和信息沟通的事情,稍有不慎就很会产生人为错误造成系统故障,发布时间和结果都不可预测,发布之后忙活到凌晨,绞尽脑汁想着怎么让刚刚部署的应用程序能够正常工作,最后常常不得不回滚,类似这样的场景都很常见。
要解决这样的痛点,除了在软件研发时采用小步迭代的方式,降低交付复杂度外,一套易用、快速、稳定、容错力强,必要时有能力快速回滚的发布系统必不可少,本文将重点建设微医在发布平台建设过程中的一些优秀实践。

核心特性

  • 覆盖主流应用:Java、NodeJS、Python、PHP、Lua、Android、IOS等。
  • 支持分批发布和灰度发布
  • 支持发布暂停和恢复
  • 支持历史版本的快速回滚
  • 支持应用重启和停止等操作
  • 支持多实例应用日志聚合查看
  • 支持分布式的发布节点
  • 发布前质量红线卡点
  • 发布中实时监控分析
  • 发布后质效度量

整体架构

 

 

 

  • 典型应用发布流程:
    • 系统立项后,在WCP-应用管理中申请创建应用,输入包括开发语言、jdk版本、构建工具、部署方式、产物路径、代码地址、应用负责人等应用基础信息(应用信息是所有研发协作的基础)。
    • 应用创建后,在WCP-资源管理中申请申请资源,在CMDB中自动建立应用和IP/域名之间的关系(包括测试环境、预发环境、生产环境等)。
    • 应用建立后,发布平台自动生成发布job,发布时获取应用和资源等基础信息,可触发灰度发布或分批发布的执行。
    • 发布操作前,自动执行质量红线检查(主要包括pipeline执行结果以及发布检查表确认等),质量红线未达标拒绝发布。
    • 发布操作中,自动暂停监控,灰度发布或首批发布后,自动触发监控。若监控失败,停止发布;若监控通过,可继续发布。
    • 发布操作后,采集存储发布数据,输出给质效看板做发布数据度量(发布成功率,发布频率,发布时长等)。
    • 发布出问题,可执行快速回滚等功能,并提供发布日志以及应用聚合日志的查询,以快速定位问题。
  • 发布平台界面:

     

发布前质量卡点

质量是持续交付的内置特性。在发布这最后一公里,如何通过发布平台自动化做好质量红线的卡点,是发布平台的一个重要特性。
在Pipeline流水线中,开发代码提交后自动触发单元测试,、静态代码扫描、安全测试、集成测试, 构成了开发和测试共同参与的一套流水线。
发布卡点是用于保障交互质量的重要手段,为了达到持续交付的目标,我们把研发pipeline执行结果作为质量红线(也可以增加人工的发布检查表结果),以此方式来保障整个持续交付的顺利进行。

 


常用的自动化质量卡点策略:

 

  • 研发流水线状态
    • 单元测试结果
    • 单元测试覆盖率
    • 代码静态检查
    • 集成测试结果
    • 安全扫描结果
  • 发布计划状态(发布计划管理系统)
    • 发布时间窗口
    • 发布评审结果等

发布中质量监控

为保障系统的稳定性,我们每个应用在监控平台都配置有对应的拨测监控点。
如何减少发布时的告警误报,或者避免发布过程中出现重大故障,这里需要发布平台和监控平台配合做一系列精密的控制策略。

  • 发布场景1:
    • 描述:发布新版本,在重启某个实例服务时,有一定的概率会触发该实例的应用报警,对应用开发人员会造成一些不必要的干扰。
    • 策略:发布平台在将应用编译打包好,执行正式发布之前,调用监控平台API停止该应用下的监控任务,在发布完后同样调用监控平台API启用该应用下的监控任务。
  • 发布场景2:

    • 描述:发布时应用实例因为各种原因(如代码部署出错,新版本存在明显BUG等),出现了系统故障。
    • 策略:采用分批发布策略,各个实例发布完后立即触发该实例的监控,如果监控发现异常,标识该批次发布操作失败,并强制中止后续批次的发布操作,以避免更多的实例出现问题。
  • 逻辑流程

     

    这里需要强调的是,拨测监控覆盖率在微医会作为团队的重要指标。因为应用只有在配置监控点后,发布平台才能在发布过程中进行有效的监测和干预。

     

发布后质效度量

质效度量是研发协作平台的一个重要组成部分,主要质效指标将按照研发质量、研发效率、研发成本三方面进行细分。

 


其中在发布过程中产生的数据,将会输送给质效度量系统进行质效分析,重点包括

 

  • 发布频率
  • 发布时长
  • 发布成功率等。

所有团队和研发成员可以结合这些发布数据指标,发现自己存在的问题和短板,并进行有效改进。

分批发布

分批发布是批次进行应用部署,每次仅对应用的一部分实例进行升级。分批发布过程中如果出现故障,则终止回退,待问题修复后重新发布。
这里我们采用了比较简洁的批次分配算法,因为公司目前使用的是双机房IDC,当应用进行分批发布时,首批发布会在两个机房中随机各选择一个实例执行,其他的实例则放到了第二批发布。

 


选择发布暂停,则可在首批发布完后暂停发布,等人工确认首批发布的实例没有问题后,再执行后续其他实例的发布,如此可有效保障发布的稳定性。
发布过程中,可在发布平台中实时查看运行日志,若发现问题,可随时执行暂停、取消或者回滚等操作。

 

 

  • 最佳实践

每个实例进行部署时,需要保证没有请求会派发到该实例,否则用户就会看到502的错误。所以需要有一个“下线”的操作,把当前机器从负载均衡中摘除,然后在部署完成之后,再把自己挂回到负载均衡中,这个过程称为“上线”。
为了实现该目的,可基于OpenResty自研Nginx网关对实例上下线进行实时调度,基于 OpenResty 的 Nginx 网关的实现过程比较复杂,这里不再详尽展开。

问题响应

在发布过程中,如果出现了一些意料之外的情况,发布平台也提供了一些常用的功能,满足开发人员定位和处理问题的需要,同时也尽量避免开发人员直接登录服务器操作。

  • 查看日志

主要对接了公司统一的日志平台系统,可实时查看应用日志,并且聚合了多实例的日志信息,减少几个实例不停切换寻找问题的痛苦。

 

 

  • 重启或停止实例

某个实例故障时,可快速重启或停用实例。

 

 

  • 快速回滚

每个发布的版本发布平台都会有备份,当发布新版本发现问题时,可快速回滚到历史版本

 

 

Jenkins Pipeline

在整套发布平台中,Jenkins Pipeline提供了核心的构建、打包、部署以及分布式调度的底层基础能力,只不过为了更灵活的调度发布操作、管理应用与发布任务之间关系等,我们摒弃了Jenkins自身的UI界面,而通过发布平台调用Jenkins API的方式将其定位为基础引擎。
其中Jenkins Pipeline的共享库特性,让我们通过groovy编程的方式,很好的实现了发布脚本的版本管理,再也不用发愁怎么管理那堆凌乱的shell脚本了。
这里只截取一部分结构代码,Jenkins共享库的具体使用可参见之前的系列文章。

import groovy.json.JsonSlurperdef call(Map map) {    pipeline {        agent any        parameters {                      //java应用参数            string(name: 'BUILD_TOOL', defaultValue: 'maven', description: '构建工具')            string(name: 'MAVEN_VERSION', defaultValue: 'maven3', description: 'maven构建工具版本')            string(name: 'GRADLE_VERSION', defaultValue: 'Gradle3.3', description: 'gradle构建工具版本')            string(name: 'WAR_RELATIVE_PATH', defaultValue: '', description: 'war包地址')            string(name: 'WAR_STD_NAME', defaultValue: '', description: 'war包地址')            string(name: 'POM_RELATIVE_PATH', defaultValue: '/pom.xml', description: 'pom文件地址')            string(name: 'HAS_TEMPLATES', defaultValue: 'false', description: '是否有模板文件')            string(name: 'TEMPLATES_RELATIVE_PATH', defaultValue: '', description: '模板文件路径')            string(name: 'JETTY_VERSION', defaultValue: '', description: 'jetty版本')            string(name: 'GRADLE_TASK', defaultValue: 'war', description: 'gradleTask打包方式')            ......        }        tools {            gradle "${params.GRADLE_VERSION}"            jdk "${params.LANGUAGE_VERSION}"            maven "${params.MAVEN_VERSION}"        }        stages {            stage('部署正式环境') {                steps {                    script {                        def pmap = [:]                        try {                            //应用参数传递                            pmap.put('BUILD_TOOL', BUILD_TOOL.trim())                            pmap.put('WAR_RELATIVE_PATH', WAR_RELATIVE_PATH.trim())                            pmap.put('WAR_STD_NAME', WAR_STD_NAME.trim())                            pmap.put('POM_RELATIVE_PATH', POM_RELATIVE_PATH.trim())                            pmap.put('HAS_TEMPLATES', HAS_TEMPLATES.trim())                            pmap.put("TEMPLATES_RELATIVE_PATH", TEMPLATES_RELATIVE_PATH.trim())                            pmap.put('JETTY_VERSION', JETTY_VERSION.trim())                            pmap.put('GRADLE_TASK', GRADLE_TASK.trim())                            ......                        } catch (MissingPropertyException ex) {                            println("Catching the MissingPropertyException " + ex.messageWithoutLocationText)                            ......                        }                        pmap = Utils_EnvConfig(pmap)                        //发布前监控调度                        Utils_Monitor(pmap.isRestartMonitor,monitorApiDomain,appIpName,monitorTimeOut,true)                        switch (ACTION) {                            case "package":                                java_package(pmap)                                break                            case "copy":                                java_copy(pmap)                                break                            case "start":                                java_start(pmap)                                break                            case "rollback":                                java_rollback(pmap)                                break                            case "restart":                                java_start(pmap)                                break                            case "stop":                                java_start(pmap)                                break                            case "kill":                                java_start(pmap)                                break                            case "backup":                                java_backup(pmap)                                break                            default:                                echo "pipeline do nothing please choose one step (package,copy,start,rollback,restart,stop,kill)"                        }                        //发布后监控调度                        Utils_Monitor(pmap.isRestartMonitor, monitorApiDomain, appIpName, monitorTimeOut, false)                    }                }            }        }    }}

结语

一套好的发布平台可以充当最后的守卫角色,对交付给线上用户的产品进行最后的检查,将未达到要求的软件版本挡于门外,一套完善的自动化发布平台也往往会比制订各类书面上的发布制度更为有效。
这套发布平台从19年年初开始重构,从原有一个单纯驱动shell脚本操作的发布工具,逐渐进化成内嵌大量质量和效率特性的发布平台,过程收获良多。这里一方面得益于持续交付的先进工程理念,另一方面也是站在了Jenkins Pipeline以及内部积累的大量成熟基础设施之上,让我们在开发时事半功倍。
当然这套平台也还有不少可以继续加强的功能,比如灰度发布的能力、基于更多质量指标对发布前后智能分析的能力等等,这些都在规划和进行之中。
中秋节将至,有闲余时间码篇文章分享给大家,祝中秋节快乐吧。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!