前面已经讲解了 CI/CD、DevOps 的操作过程和工具链,这一讲我重点介绍自动化部署,也会讲解关于 BVT 的具体内容。因为自动化部署内容比较多,所以这一讲会拆分上、下两部分,先介绍持续集成中的部署和 BVT,后介绍 DevOps、Cloud 中的部署和发布。

持续集成环境中的部署活动,就是在完成单元测试、代码静态分析和打包等之后,需要把生成的软件包部署到测试环境中。有人说,持续集成的过程就是不断地尝试各种“在一起”:提交代码是让代码在一起,集成是让业务逻辑在一起。那么,部署就是让系统运行在一起。

软件部署的场景有很多,构建成功后生成的软件包有不同的格式,需要不同的运行环境;软件部署的目标实体可以是一台服务器、一台嵌入式系统的终端,也可能是一个手机。这里我以微服务架构体系的企业级应用系统为例进行讲解。

微服务的容器化部署

早期的软件系统都是采用单体架构,就是软件所有功能都放在一个工程里进行开发,各个模块紧密耦合,相互依赖,只能将一个软件包整体部署到服务器上。部署简单,但系统任何一个小的变更和升级都需要重新构建和部署整个系统,风险大、耗时长。

当前,微服务架构是最流行的软件架构风格,强调业务系统彻底的组件化和服务化,一个微服务完成一个特定的业务功能,比如设备管理、订单管理等。每个微服务可以独立开发、独立部署,微服务可以部署在物理机上,也可以部署在虚拟机上,但更适合部署在 Docker 容器(Container)上。微服务架构强调单个微服务在独立的进程中运行,Docker 容器刚好能做到进程级别的隔离;而且容器占用资源很少,启动速度又很快,这些特点刚好满足微服务架构的持续集成和持续交付的需要。

使用 Docker 模式需要将每个微服务打包成 Docker 镜像(Image),而一个 Docker 镜像就是一个包含软件应用和依赖资源的文件系统。容器是 Docker 镜像的运行实例,Docker 是容器引擎,相当于系统平台。

Dockerfile 是一个文本格式的配置文件,定义 docker 在创建镜像时需要执行的命令。下面是一个通过 Dockerfile 部署 Spring Boot 应用的简单示例,脚本逻辑是:指定一个提供 JDK 的基础镜像(FROM)及创建镜像时用到的变量(ARG),把应用的 jar 包复制到镜像中根目录下(COPY);ENTRYPOINT 指定容器启动程序及参数。

 

创建 docker 镜像,并启动容器实例的命令示例如下:

在第一条命令中,Dockerfile 位于 /tmp/dockerfiles 目录下,逐条执行 Dockerfile 中的指令创建了一个名称和版本号为 my-image:1.0.0 的镜像文件;

第二条命令是用生成的镜像启动一个容器,容器命名为 my-image。

CI 配置管理工具

在 CI 环境里,有时需要用到一些配置管理工具来完成系统部署,比如数据库、Web 服务器等。在第 15 讲中,我在介绍 Molecule 时简单介绍过 Ansible,它是一个基于 Python 开发的自动化运维工具,提供远程系统安装、启动 / 停止、配置管理等服务,并且可以对服务器集群进行批量系统配置、批量部署和批量运行命令。Ansible 使用 SSH 协议与目标机器进行通信。Ansible 还可以实现对 Docker 集群的自动化管理工作,比如安装、部署、管理 Docker 容器和 Docker 镜像。

图1 Ansible 架构示意图

Ansible 项目的目录结构如图 2 所示:

图2 Ansible 目录结构图

Ansible执行的脚本文件 playbook.yaml 做为入口文件,指定在哪些服务器集群为哪些 role 执行配置任务,示例如下:

Inventories 目录下的 hosts 文件存放所有目标服务器的地址。你可以为需要管理的应用创建一个对应的 role,比如 Nginx、Redis 等,把配置信息、需要执行的 shell 脚本存放在每个 role 的目录下。

Chef Solo 是另一个配置管理工具 Chef 提供的一个工具,它的特点是去中心化,不需要 Chef Server 就可以本地运行各种 Chef 方案,从而自动执行软件包安装、语言或框架编程和软件配置等多种任务。由于篇幅有限,这里就不展开讨论了。

微服务在 CI 环境中的自动化部署

有了 Docker 和自动化配置管理工具,接下来就可以在 CI 环境中轻松的完成微服务的自动化部署。目前主流的实践基本都是通过 SSH 协议和远程目标服务器建立连接,然后在目标服务器上执行 shell 命令进行部署,比如 Jenkins、Ansible、GitLab CI 等。

图3 Jenkins + Docker 实现持续集成

以 Jenkins 为例,一个微服务的构建部署过程如下。

开发 push 代码到代码仓库,触发 Jenkins 拉取代码,通过构建服务器编译、测试、打包,然后执行 shell 脚本使 docker 构建镜像并 push 到镜像仓库。此操作完成后 Jenkins 服务器再执行 SSH 命令登录到部署服务器,执行 shell 脚本使 docker 从镜像仓库拉取镜像,启动容器。

Jenkins 运行机器上的 Shell 命令如下:

 

目标服务器上的 Shell 命令如下:

以 Jenkins Pipeline + Ansible 为例,Jenkins 内需要安装 Ansible 插件。微服务 Docker 镜像生成后,由 Ansible 上传至目标服务器,执行容器管理对应的 role 所定义的任务进行部署。

Jenkins 流水线脚本 jenkinsfile 如下所示:

GitLab CI 是 GitLab 自带的持续集成服务,用 GitLab CI 也可以实现微服务的自动化部署,如图 4 所示。

             

图4  GitLab CI 部署流水线呈现部署状态的界面

BVT 究竟要验证哪些点

BVT(Build Verification Test)是持续集成的最后一步,也被称为冒烟测试(Smoke Testing)。“冒烟测试”这个术语来源于电子行业,先对电路板进行通电测试,如果冒烟,电路板存在致命的故障,就没有必要做进一步的测试了,否则需要做进一步的测试。微软把冒烟测试这一概念引入到了软件领域中,即在 2000 年出版的《微软项目求生法则》一书,其中第 14 章“构建过程”里提到过它。

BVT 就是用来验证软件的基本功能是否能正常工作,也是用于检验持续集成是否成功。如果 BVT 不通过,则相当于电路板冒烟,意味着没必要或无法进行更深入或更细粒度的测试。BVT  的测试点应该包括最基本的功能或业务上最常用的功能,一般也包括软件所依赖的数据库,以及外部服务的验证。而且,BVT 的测试内容要根据软件版本的演化进行持续更新,增加新的基本特性的覆盖,去掉那些风险较低的测试项。

持续集成之前已做了单元测试,如果单元测试的代码覆盖率很高,还需要做 BVT 吗?当然需要。因为单元测试只是验证单元自身的功能特性,并没有验证众多单元组合在一起的业务逻辑,我们经常会遇到单元测试全部通过,但是软件部署后系统不能正常运行的情况。

这里用一个手机端视频会议 App 来作为示例,BVT 应该包括哪些测试,如表 1 所示。

表1 视频会议 App 的 BVT 测试项

可以说,这几个测试项是视频会议最基本的功能,组合起来可以形成不同的应用场景,任何步骤的失败都会导致后续测试无法正常开展,比如,以此为基础的三方到多方会议、预定选项更多的重复性会议、网络环境不稳定性时开会或加会等场景的测试。

在持续集成里的 BVT 要求是全部自动化的,从自动化测试的角度,除了上面说的“测什么”问题”,BVT 还需要考虑“怎么测”的问题。关于怎么测的问题,一种方式是借助 Selenium、Appium 这类工具直接从 UI 层实现自动化测试;另一种方式是绕过 UI,基于 API 接口进行测试。与 UI 层相比,API 接口改动少,脚本开发效率要高很多,测试执行又快很多,所以尽可能用 API 的方式实现 BVT。比如上面的视频会议 App 的 BVT 测试都可以通过 API 来实现,这也是上一讲说的金字塔测试策略。

持续集成要考虑速度和质量的平衡, 而 BVT 测试脚本越多,那么需求覆盖就越全,但需要的执行时间就越长。Martin Flower 在《持续集成》一文中,建议采用次级构建(Secondary Build)和并行测试等策略来解决这类问题,即能缩短时间,又有较高的覆盖率。

按照 Martin 的建议,持续集成的第一阶段是“提交构建”,由开发人员提交代码触发的编译、测试、打包、部署等活动。考虑到持续集成的速度,BVT 中只包含最关键的功能,比如风险高、执行时间短的测试场景。提交构建成功后,再立即触发“次级构建”,通常只包括测试活动,测试执行时间较长、风险不太高的基本功能。

图5   BVT 并行测试脚本示例

并行测试是指利用分布式测试环境把测试分布到多台执行机器和被测系统中,以节省执行时间。比如,BVT 测试如果在一台机器上执行需要 30 分钟,那么分布到 3 台机器上执行时间就可以下降到10分钟。如果通过 Jenkins Pipeline 管理持续集成过程,流水线脚本如图 5 所示,在部署之后执行 BVT 并行测试。

到这里,你是不是对整个持续集成的过程和实现已经有了一个完整的认识?

不稳定的情况 Flaky

最后简单介绍一下自动化测试中的 Flaky 问题——测试结果是非确定性的问题,即一个测试用例在同样的测试环境和产品中运行不同次数得到的结果不同,有时成功,有时失败,而为了分析测试用例的失败原因,挺耗费时间的。在持续集成中,Flaky 是影响自动化测试效率的一个主要因素。而造成 Flaky Test 的原因有很多,常见的有:

测试用例代码的问题,比如存在异步等待:执行异步调用,并等待对方回复。如果某次运行中得到回复的时间比设定的等待时间长,测试用例就会失败。

测试环境的稳定性问题,比如测试工具不够稳定;测试依赖的远程服务由于网络原因访问不可靠。

正因为造成 Flaky Test 的原因复杂,目前还没有简单有效的办法彻底消除。

首先还是要从发生的根本原因入手,进行失败分析,找出修复的方法,比如,在测试代码中添加轮询机制解决异步等待失败的问题。在持续集成的测试中尽可能消除对外部环境和系统的依赖,否则应该将这部分测试用例放在持续集成之后的持续测试中执行。另外,也可以考虑采用不同(执行步骤、用例等)级别的重试机制。如果不能彻底修复出现 Flaky 现象的个别测试用例,则在测试报告中标记这些测试用例,这样,研发人员就不用花时间分析其失败的原因了。

最后总结一下今天的知识点:

在微服务软件架构下,应该首先考虑结合 Docker 容器进行微服务的 CI 部署;

CI 的自动化管理工具 Ansible 可以实现 Docker 环境的自动化管理;

CI 环境中的调度工具、配置管理工具结合 Docker 容器技术可以轻松地实现微服务的自动化部署;

在 BVT 测试中首先应该考虑测试质量和速度的平衡,因为 CI 的关键是提供快速反馈。因此,BVT 测试只覆盖最基本的功能点;

Flaky Test 在自动化测试中是一个比较难解决的问题,但我们还是要尽可能消除 Flaky 的情况以保证 CI 的效率和结果的可靠性。

最后,给你出一道思考题:对于自动化测试中的 Flaky Test,你所在的团队有过哪些优秀实践?你认为还需要做什么样的改进?欢迎留言讨论。

-– ### 精选评论 ##### **林: > 用例执行失败,让其再重新执行。