DevOps最佳实践(200711)

  • 其一:业务层面,重点是中台规划和建设
  • 其二:技术层面,重点是云原生解决方案,包括了微服务,DevOps和容器云

当然,如果你是传统的软件开发框架技术,或者传统的基于虚拟机的PaaS平台也可以上DevOps实践,但是我们更加推荐的还是基于微服务和容器云技术来实践DevOps。

对于DevOps基本概念的理解

首先看下DevOps的定义:

DevOps(英文Development和Operations的组合)是一组过程、方法与系统的统称,用于促进开发(应用程序/软件工程)、技术运营和质量保障(QA)部门之间的沟通、协作与整合。它的出现是由于软件行业日益清晰地认识到,为了按时交付软件产品和服务,开发和运营工作必须紧密合作。

对于DevOps的提出已经很多年,其主要的推动来自两个方面:

  • 业务和需求驱动下,推动敏捷方法论,更快的发布和交付。
  • 技术和运维部门需要衔接,在PaaS和容器技术发展下进一步推动自动化。

虽然是研发,测试QA,运维交付三方交融的地方为DevOps的内容,但是可以看到DevOps本身更多的是要解决两大类协同和自动化的问题

其一是开发部门和QA的协同

实际上该问题基于多年前已有的每日构建,持续集成方法论就可以解决。在DevOps里面我们也看到这部分协同更多的是敏捷研发最佳实践进行融合。

其二是开发部门和运维的协同

该问题的解决当前由云平台,微服务架构,容器技术发展推动的自动化发布和监控运维。注意在持续集成的时候我们只解决了自动化部署问题,但是没有解决持续交付的问题。而在DevOps里面需要同时解决持续集成和面向用户的持续交付能力。

问题目标和解决关键点技术的对应

把上面思路理清楚后,对于DevOps要做或需要解决的事情也就更加明确了:

  • 实现自动化部署和资源动态扩展-Docker容器云+K8s集群管理
  • 实现不同环境迁移和持续交付 – Docker容器云+持续集成流水线
  • 能够满足更加高频率的发布节奏 – 敏捷开发,持续集成,自动化流水线
  • 能将QA和QC部分检验和审查工作自动衔接到发布和交付过程 – 自动化测试,自动化监控

以上实际上就是我们经常谈到的DevOps关键内容。但是如果一个企业要完全实践DevOps的话就不不仅仅是简单的工具技术使用,还涉及到研发过程改进,组织文化等一系列的问题。而国内本身也给出了一个DevOps成熟度模型。

DevOps成熟度模型

整个DevOps成熟度模型可以算作是企业实践DevOps过程的一个参考基准,该文档本身由阿里,腾讯,华为,中兴,平安等大的企业共同参与完成,其总体架构包括了三大部分,即过程,应用设计,风险管理和组织结构。而里面的过程管理本身涵盖的子过程和活动要求又最多。整个过程本身按持续生命周期阶段分为了敏捷开发管理,持续交付,技术运营三个方面的内容。

敏捷开发管理

在这里完全可以参考Scrum敏捷开发方法论最佳实践进行。里面的核心对象是产品,需求,项目,迭代版本,任务,缺陷。而这些都将和持续集成和交付发生关联。

如果做过CMMI或敏捷项目管理的可以看到实际上在当前DevOps成熟度模型中的敏捷开发管理还是相当粗的,而且是将软件工程域内容和过程管理内容融合在了一起,同时也可以看到其核心还是基于scrum敏捷项目管理方法论而展开,只是在这个过程中更加强调了业务场景驱动和价值交付的重要性。

DevOps持续集成和交付本身就是为了更加敏捷响应需求,快速短周期的迭代交付,因此和敏捷方法论配合是自然的事情。同时我们也可以看到要实现敏捷, 需求必须细粒度化,需求本身需要体现业务价值 ,核心就是基于业务场景分析出来的用户故事和用户故事地图。

而对于敏捷开发或敏捷项目管理,如果要用一句话来总结的话就是

基于业务场景和用户故事驱动的短周期迭代和项目团队高效协同,以实现快速的业务价值交付。 而这个目标下我们经常谈到的可视化看板,站立会议,燃尽图等也仅仅是实现上述目标的工具。

也正是这个原因我们看到,要进入到敏捷开发过程管理,形成基于业务场景形成具备优先级划分的细粒度用户故事至关重要,这将直接是我们后面迭代计划安排,测试用例编写,交付交付的基础。

持续交付

注意不是我们传统的持续集成,而变成了持续交付。对于持续交付即包含了产品从需求开发开始,一直到最终客户的发布交付过程,是一个完整的端到端流程。对于整个过程的支撑我们看到核心仍然是持续集成和容器云的集成来完成,而整个集成过程又通过流水线进一步自动化。

技术运营

注意是技术运营而不是运维,虽然在技术运营里面也包括了运维监控类的内容。但是当升级为技术运营后进一步体现了通过对发布产品各自运营手段来持续的促进产品优化改进工作。这个也是我们在原来各类方法论中很少提及到的点。

注:对于DevOps成熟度模型,后面专门写一篇文章来谈,在此不再展开。

持续集成过程回顾

在谈DevOps持续交付这个过程域之前,准备把原来敏捷开发或软件工程里面的持续集成最佳实践再做下回顾,要知道早在10多年前我们就已经在实践持续集成,每日构建,自动化测试和冒烟测试,因此对于持续集成并不是新鲜事物。唯一我们看到的是在 DevOps最佳实践里面将持续集成过程变得更加灵活,自动化和可编排了。

对于持续集成的过程,我们以每天进行一次持续集成为例来进行说明

  1. 所有开发人员,每天在下班前Check in自己开发完成并单元测试通过的代码
  2. 统一的配置库环境服务器(CI服务器),Update到最新的源代码文件
  3. 基于自动化编译配置文件,执行自动化的代码编译,并输出编译完成二进制包
  4. 将相关部署文件根据自动化执行脚本安装和部署到我们的测试服务器
  5. 安装部署成功后,执行自动化测试脚本,输出自动化测试报告
  6. 测试人员介入进行功能点的黑盒测试,并反馈测试缺陷和跟踪缺陷在下次集成版本中解决

以上是我们最早使用的持续集成过,也没有和Docker容器相结合,对于自动部署过程也是通过自定义脚本来完成部署。而现在我们可以看到在DevOps里面的持续集成更加强调了 容器化技术的结合和整个持续集成过程的可配置性。

简单理解,就是上面提到的6个步骤,你是可以自己灵活组合和可视化编排的。

在传统的单体应用架构下,我们最终是打包成一个大的部署包,而实际在编译构建中存在诸多的依赖关系和构建顺序,而这些内容都是我在构建脚本中通过配置文件或代码进行控制的。

而在新的微服务架构模式下,各个微服务模块相互独立开来,都可以独立构建打包,但是模块之间的关联关系依然是存在的。也就是说从一个包含了多个微服务模块的大的应用来说,我们在大构建过程中仍然有依赖和先后关系。

即: 流水线编排本身应该分层,顶层为产品或达应用,下层为各个独立微服务模块

在整个持续集成的过程中一定会涉及到环境的迁移问题,比如SIT测试完成后需要迁移到UAT环境,而UAT环境测试完成后需要迁移到生产环境。

不论是最早的持续集成最佳实践,还是当前的结合容器镜像的DevOps过程实践,我们始终在强调 一次构建多地部署,即最终的环境迁移是基于二进制部署包或容器镜像进行的 。在后续的多个测试环境和生产环境,不能再执行编译构建和打包动作。

一般考虑在配置库update到最新代码后,就可以进行代码静态检查,检查完成后再进行编译和打包,代码静态检查问题不用影响到后续的直接构建过程。在部署完成后再进行自动化的单元测试操作,按道理自动化单元测试或冒烟测试不通过应该影响到后续的人工介入测试。

持续集成实际上全新的产品开发和交付只是其一,更加重要的是用于后续的变更管理和变更版本,补丁版本的开发和交付上线。这个过程本身的效率和自动化程度真正体现了开发,质量和运维人员的协同度。

流水线设计实践和优化改进

对于DevOps流水线可视化设计,主要是由各类任务串联起来,而对于任务本身又分为两种类型,一种是自动化任务,一种是人工执行任务。具体如下:

  • 自动化任务:包括了代码静态检查,构建,打包,部署,单元测试,环境迁移,自定义脚本运行等。
  • 人工任务:人工任务主要包括了检查审核,打标签基线,组件包制作等类似工作。

而通常我们看到的流水线基本都由上述两类任务组合编排而成,一个流水线可以是完全自动化执行,也可以中间加入了人工干预节点,在人工干预处理后再继续朝下执行。比如流水线中到了测试部署完成后,可以到测试环境人工验证环节,只有人工验证通过再流转到迁移发布到生产环境动作任务。

DevOps流水线实际上和我们原来经常谈到的持续集成最佳实践是相当类似的,较大的一个差异点就在于引入了容器化技术来实现自动化部署和应用托管。但是在DevOps实践中,是否必须马上将项目切换到微服务架构框架模式,反而不是必须的。

在整个DevOps流水线中,我们实际上强调个一个关键点在于 一套Docker镜像文件+多套环境配置+多套构建版本标签 做法。以确保我们最终构建和测试通过的版本就是我们部署到生产环境的版本。

构建操作只有一次,而后面到测试环境,到UAT环境,到生产环境,都属于是镜像的环境迁移和部署。而不涉及到需要再次重新打包的问题。这个是持续集成,也是DevOps的基本要求。

今天谈DevOps流水线编排,主要是对流水线编排本身的灵活性进一步思考。

构建操作: 构建我们通常采用Maven进行自动化构建,构建完成输出一个或多个Jar包或War包。

注意常规方式下构建完执行进行部署操作,部署操作一般就是将构建的结果拷贝到我们的测试环境服务器,同时对初始化脚本进行启动等。而在DevOps下,该操作会变成两个操作,即一个打包,一个部署。打包是将构建完成的内容制作为镜像,部署是将镜像部署到具体的资源池和指定集群。

打包操作: 实际上即基于构建完成的部署包来生成镜像。该操作一般首先基于一个基础镜像文件基础上进行,在基础镜像文件上拷贝和写入具体的部署包文件,同时在启动相应的初始化脚本。

构建操作和打包操作的松耦合

那么首先要考虑构建操作和打包操作如何松耦合开,打包操作简单来就是就是一个镜像制作,需要的是构建操作产生的输出。我们可以对其输出和需要拷贝的内容在构建的时候进行约定。而打包任务则是一个标准化的镜像制作任务,我们需要考虑的仅仅是基于1)基于哪个基础镜像 2)中间件容器默认目录设置 3)初始化启动命令。即在实际的打包任务设计的时候,我们不会指定具体的部署包和部署文件,这个完全由编排的时候由上游输入。

难点在哪?实际上在于我们dockerfile文件中往往增加了非标准化的自定义运行脚本,导致了镜像制作过程和你最终打包生成二进制文件产生了很强的关联性。而要做到彻底解耦,就需要对这部分进一步进行标准化。

部署操作: 部署操作相当更加简单,重点就是将镜像部署到哪个资源池,哪个集群节点,初始化的节点配置等。具体部署哪个镜像不要指定,而是由上游任务节点输入。

DevOps持续集成和研发测试过程协同

流水线设计属于持续交付过程域中的一个关键内容,其核心还是为了持续集成服务。那么我们首先回顾下流水线作业解决什么问题?

简单来说就是 解决自动化的持续集成问题 ,那么持续集成本身又包括哪些关键内容?即我们常说的自动化编译构建,自动化部署,自动化测试。而在转到和容器技术结合后可以看到在编译构建完成后自动化部署进行了拆分,即部署动作变化为了首先是自动化的打包即制作容器镜像,然后才是自动化部署,部署部署基于部署包的,而是基于制品库中的容器镜像的。

说这么多,可以看到,在DevOps中的流水线设计更多的是 解决开发人员的自动化问题,即开发只需要check in自测通过的代码到配置管理库,那么后续的所有事情都自动化完成 ,最终部署到测试环境供测试人员测试。开发人员不用关心编译构建过程,不用关心测试服务器包括测试环境数据库,中间件的安装部署等一系列的问题。

同时由于一系列的自动化操作,也让代码从检入到交付测试的周期缩短了,在周期缩短后交付频度也可以进一步提升以加快迭代速度。

今天再谈流水线设计,主要原因是 原来更多是从技术层面开发人员自动化在谈,但是却少考虑研发管理开发和测试过程协同问题。

在DevOps最佳实践里面分为了研发管理,持续交付和技术运营几个关键的过程域。

但是在实践的过程中,最容易出现问题的不是单个技术点,而是跨域的协同问题,或者说 研发过程管理和持续集成交付本身就是密不可分的两个部分 ,我们只是为了容易理解和学习将其划分为了不同的过程域而已。

因此 流水线设计需要理清研发过程管理和自动化持续集成间的协同关系 。具体两者的协同关系我们用下图来进行说明。

要明白任何一次新的编译构建部署完成后都涉及到测试人员测试,测试人员测试出问题后又会提交Bug,开发人员修改Bug后Check in代码,等待下一次打包部署以形成多次迭代。整个过程最好方式就是要尽量 减少大量的人工沟通协同,而是应该通过工具链协同 来完成。

对于传统的持续集成,一般最佳实践为每天晚上进行自动化构建和冒烟测试,而对于当期的DevOps过程,在设计完流水线后,可以手工去启动流水线作业,也可以自动去执行流水线,流水线执行时间频度也可以进一步缩短。

假设我们每2个小时自动化的执行一次流水线作业,我们以此场景来做进一步梳理。因为对于大部分企业来说再高频的构建集成也不需要代码有变动就马上触发构建打包的程度。

流水线增加启动检查节点

虽然2小时执行一次流水线,但是在执行前先进行启动前检查,如果在最近2个小时内没有新代码check in,那么不执行流水线。其次,如果上一次流水线执行实例还处于待处理或未关闭状态的时候,同样也不执行流水线作业而直接跳过。

是否需要人工验证

流水线打包部署包括两个方面,一个是新功能提交或新bug解决,只有这种情况才需要人工验证。因此一次流水线执行如果没有新需求或Bug状态变化,那么应该直接跳过人工验证节点并关闭。反之,则应该跳转到待人工验证环节。

需求和缺陷的管理

要注意到新需求和新缺陷都应该提交,而且都有状态,需求细分到具体的需求功能点,同时测试人员提交的缺陷应该对应到具体的需求功能点上面。一个需求开发完成后,需求本身也到待验证状态,但是一个待验证的需求是否能够关闭则必须是该需求下面所有的bug都解决完成后才能够关闭。

需求和缺陷状态的变化

开发人员首先是将需求或缺陷完成,并在本机进行自测通过,然后将代码check in到配置管理库。同时手工将需求或缺陷状态处理到待部署状态。在流水线启动后,如果整个构建打包和部署成功,则在成功完成应用部署后,将待部署状态的需求或bug转到待验证状态。在部署完成后测试人员可以看到待验证的bug或需求,那么他进入当前的测试环境一定是最新的可以进行缺陷验证的环境。

应用部署和环境迁移

实际上我一直在思考如何理清这两者的关系,包括在流水线节点设计的时候是同种类型的一个节点还是不同的两个节点?应用来说是直接将编译打包后的镜像执行进行部署,前面有编译构建操作;而对于环境迁移来说则是直接从制品库里面使用已有镜像进行环境部署。

流水线模板和流水线实例应该是两个不同的概念,一个流水线模板每次运行都会产生一个实例,而每次实例都会形成一次构建版本号。即每次打包后形成的镜像入库也会附带上这么一个流水线实例号。那么我们的应用部署操作本身就简单的,对于应用部署活动节点编排在流水线上面,作用是进行应用部署,但是本质是从制品库拿到对应镜像去部署?如何拿到对应镜像,基于流水线实例号就可以很好的进行对接上。

环境迁移-从SIT到UAT环境

在环境迁移中,我们设置两个环境,一个SIT环境供内部测试人员测试,一个UAT环境供客户方进行UAT测试,那么在SIT测试完成后可以将SIT环境的镜像包迁移到UAT环境进行部署。那么并不是每一次流水线作业都涉及到环境迁移操作,而实际上需要测试人员去判断当前的测试版本是否需要迁移到UAT环境去供用户测试。

同时测试人员在当前测试问题全部修复并关闭后,可以对当前版本设置配置基线。其次,对于用户测试提交的问题一般并不会在我们自己的缺陷管理系统进行Bug记录,因此用户反馈问题给测试人员,测试人员帮忙录入到缺陷管理系统,同时缺陷修改完成后测试先要验证没有问题,再通知用户进行验证,只有用户验证通过后缺陷才能够最终关闭。

变更驱动的版本开发和流水线设计

对于一个变更,如果只涉及到一个微服务模块的变动,那么相当来说整个持续集成过程是很简单的,我们也很容易在DevOps上完成这个流水线设计并执行。但是如果涉及多个整个过程就复杂了很多。

我们举例来说,现在接收到一个或多个用户变更需求,经过需求分析后发现实际影响三个微服务模块都需要进行配套变更才能够完成。那么我们可以规划一个研发小版本来解决,即首先该需求就会拆分,并对应到三个微服务模块变更的任务。

在这种场景下变更驱动场景下,我们可以保留原来的大而全的顶层流水线,但是对应没有代码变化版本不再执行编译构建操作。

一个大应用涉及多个微服务,务必要执行无变更不重新编译构建原则。但是我们实际看到在很多DevOps实践中,一个变更版本开发,往往也会对没有变动的微服务重新构建打包。

即大流水线执行到子流水线的时候自动跳过一些子流水线的执行。当然我们也可以重新规划一个新的顶层流水线,只选择有变更的三个微服务模块进行编排设计,同时根据依赖关系定义好三个模块本身的编译构建顺序。

那我们整个顶层流水线执行的时候就会将三个变更模块全部编译构建并打包部署,然后驱动后面的自动化测试,人工测试和验证。整个需求的实现,缺陷的修改过程应该是完整可视的。简单来说,基于这个变更小版本,提交的几个需求变更点当前已经实现了几个,究竟还有多少缺陷在处理,我们应该一清二楚的了解到。

DevOps过程和容器云协同

对于微服务架构框架选择Spring Cloud框架来实现,不同的业务组件模块单独打包,同时将业务组件的对外接口API通过注解的方式快速的暴露为Rest API接口。同时对于单个业务组件模块我们进一步实现展现层和后端逻辑+服务层的分离,每一个业务组件独立构建为两个部署包,并且可以独立部署,通过这种分离后我们在开发上也实现前后端角色的分离。

代码放置在外网的GitHub代码仓库,通过Maven来实现代码的构建,同时在Jenkins中本身集成Maven,对于构建完成的部署包会进入到Jenkins本身的部署包仓库管理。

这个完成后进一步启用Jenkins来调用底层的Docker命令生成Docker镜像文件,这个镜像则是我们后续做自动部署和持续集成的关键镜像文件。

我们知道Docker本身仅仅是容器,是资源调度的单位,在当前开源解决方案中用的比较多的是通过Kubernetes实现容器集群管理和资源调度能力。因此在这个步骤完成后即对接到Kubernetes来实现Docker镜像文件的自动化部署和动态调度。

在持续集成中有一个重点就是环境迁移,注意每次迁移的都应该是相同的镜像文件,而对于和环境相关的配置则单独进行配置或放置到OS的环境变量中。只有这样才能够保证最终上线的部署包就是我们最终开发和测试完成的部署包而没有任何变动。

如果在动态部署的时候启用了多个节点,那么还需要提供负载均衡能力。要注意的是Kubernetes本身也提供负载均衡和虚拟IP路由能力。我们最终访问的是域名,而域名最终会解析到实际的计算节点。

在微服务架构中,还通过微服务网关对所有注册的接口服务进行完整的管理和监控,包括服务的注册发布,服务的路由,服务日志,服务安全和限流等,这些通过微服务网关实现。

DevOps过程和API网关协同

对于API网关,我在前面文章都单独谈到过。在一个大型项目的多团队协同下,如果都采用微服务架构,我们实际建议的是每个团队都是自己独立的微服务注册中心,负责团队内部多个微服务模块之间的API接口调用,这些API接口调用走注册中心即可,但是涉及到跨团队协同的API接口服务时,就需要注册到API网关进行统一管理。

简单来说就是, 对外发布API或者跨团队API接口调用都涉及将API注册接入到网关统一管理

对于一个微服务模块和API网关的协同,包括了提供API接口服务注册和接入到网关,也包括了从网关调用API接口服务消费。因此需要从API注册接入和API消费调用两个方面来谈协同。

API接口服务自动注册接入网关

对于整个DevOps过程可以看到,底层是Docker容器+K8s资源调度,在我们编排流水线的时候涉及到编译构建和打包,部署等各个动作。实际上可以看到在完成自动部署后接口服务会暴露一个k8s提供出来的动态ip访问地址。而我们需要做的是将这个ip地址提供出来的接口注册和接入到网关。

在整个过程搞清楚后,实际上可以有两种方式来处理API注册接入:

  • 在部署节点,增加自定义脚本,通过运行脚本来完成API接口注册。
  • 增加接口注册流水线编排节点,编排注册节点,在该节点完成接口注册内容。

由于整个DevOps流水线设计和执行偏开发人员使用,可以看到,采用第一种方式往往更加灵活。唯一需注意的就是在定义某一个流水线的时候,需要预先规划好需要接入和注册的接口内容。

而在DevOps支撑平台虽然不需要完整的API网关管控功能,但是最好还是能够在DevOps支撑平台查询到当前已经注册和接入了哪些接口服务,注册接入后提供的代理服务地址是什么,是哪个微服务模块注册接入的该服务等基本服务目录信息。

API接口消费调用

在采用了API网关后带来的一个好处就是,API网关本身提供出来的API访问地址的IP是固定的,不会随着每次微服务模块的自动构建和部署动态变化。对于API网关我们会提前先部署到测试环境和生产环境,在网关部署完成后再开始进行各个微服务模块的持续集成和部署操作。

因此一个微服务模块需要访问其它微服务模块哪些接口,一个方法是每次都到服务注册中心去查询具体的服务访问地址,另一个方法就是微服务模块本身要将访问地址存在在本地配置文件中。而更好的方法是:

  • 首先调用先访问服务注册中心,获取服务访问地址,并存在到本地配置文件。
  • 在发现本地配置有服务访问地址后,不再从服务注册中心调用,除非有地址变更消息通知。

在这个确定后,微服务模块本身的构建打包和部署,实际上和原来没有和API网关协同是完全一样的,只是配置文件访问地址固定为了API网关提供的地址而已。如何知道API网关提供了哪些地址,即我们谈到的可以在API网关的管控平台查询,也可以在DevOps平台提供的服务目录查询功能上进行查询。

API接口本地调式

在前面我们谈到过,在持续集成过程中的环境设置是否需要设置开发环境的问题。对于这个问题,个人的观点是,在开发人员的本地开发项目只包括自己的开发包,其它交互和协同都需要调用其它模块的API接口服务来完成的时候,必须设置开发环境,而且要确保开发环境和测试环境分离。

当开发人员在本地调试的时候,涉及到外部模块的接口则直接配置为开发环境接口地址进行本地调试,而外部模块提供的接口也需要提前开发完成且已部署到开发环境上面。

API接口环境迁移

环境迁移是另外一个在协同过程中必须解决的关键问题。

即在环境迁移后,需要对服务访问地址的配置文件中的ip地址进行自动化修改,这个过程可以在流水线编排的时候用脚本代码来完成。我们可以准备多个备用的配置文件,在环境迁移的时候用当前环境的配置文件对缺省配置文件进行覆盖即可。