一、L4构建集成

1.1 构建的提速

1.1.1 升级硬件资源

需要注意的是,这里的硬件资源包括 CPU、内存、磁盘、网络等等,具体升级哪一部分,需要具体情况具体分析。

比如,你要构建一个 C 语言程序,那么 CPU 就是关键点。你可以增加 CPU 的个数或者提升 CPU 主频以实现更快的编译速度

再比如,你要用 Maven 构建一个 Java 应用,除了 CPU 之外,Maven 还会从中央仓库下载依赖写在本地磁盘

这时,网络和磁盘的 I/O 就可能成为瓶颈,你可以通过增加网络带宽提升网络吞吐,使用 SSD 代替机械硬盘增加磁盘 I/O ,从而到达提升整个构建过程速度的目的。

总之,当你使用成熟的构建工具进行构建时,如果无法通过一些软件技术手段提升软件本身的构建速度,那么根据构建特点,有针对性地升级硬件资源,是最简单粗暴的方法

1.1.2 搭建私有仓库

构建很多时候是需要下载外部依赖的,而网络 I/O 通常会成为整个构建的瓶颈。尤其在当前网络环境下,从外网下载一些代码或者依赖的速度往往是瓶颈,所以在内网搭建各种各样的私有仓库就非常重要了。

目前,我们需要的依赖基本上都可以搭建一套私有仓库,比如:

  • 使用 createrepo 搭建 CentOS 的 yum 仓库;
  • 使用 Nexus 搭建 Java 的 Maven 仓库;
  • 使用 cnpm 搭建 NodeJS 的 npm 仓库;
  • 使用 pypiserver 搭建 Python 的 pip 仓库;
  • 使用 GitLab 搭建代码仓库;
  • 使用 Harbor 搭建 Docker 镜像仓库

所以,如果你的团队暂时没有条件自己搭建私有仓库的话,可以使用国内已有的一些私有仓库,来提升下载速度。当然,在选择私有仓库时,你要尽量挑选那些被广泛使用的仓库,避免安全隐患。

1.1.3 使用本地缓存

虽然搭建私有仓库可以解决代码或者依赖下载的问题,但是私有仓库不能滥用,还是要结合构建机器本地的磁盘缓存才能达到利益最大化。

所以,妥善地用好本地缓存十分重要。这里说的“妥善”,主要包括以下两个方面:

  • 对于变化的内容,增量下载;
  • 对于不变的内容,不重复下载。

对于第一点,项目的源码是经常变化的内容,下载源码时,如果你使用 Git 进行增量下载,那么就不需要在每次构建时都重复拉取所有的代码。Jenkins 的 Git 插件,也默认使用这种方式。

对于第二点,Maven 每次下载依赖后都会在本地磁盘创建一份依赖的拷贝,在构建下载之前会先检查本地是否已经有依赖的拷贝,从而达到复用效果。并且,这个依赖的拷贝是公共的,也就是说每个项目都可以使用这个缓存,极大地提升了构建效率。

如果你使用 Docker,那么你可以在宿主机上 mount 同一个依赖拷贝目录到多个 Slave 容器上,这样多个容器就可以共享同一个依赖拷贝目录。你可以最大程度地利用这一优势,但要注意不要让宿主机的磁盘 I/O 达到瓶颈。

1.1.4 规范构建流程

Less is More,Simple is Better

程序的追求是简约而不简单,但随着业务越来越复杂,构建过程中各种各样的需求也随之出现,虽然工具已经封装了很多实用的功能,但是很多情况下,你都需要加入一些自定义的个性化功能,才能满足业务需求。

Java 构建过程中就有大量的额外逻辑,比如 Enforcer 检查、框架依赖检查、Sonar 检查、单元测试、集成测试等等,可以说是无所不用其极地去保证构建产物的质量

以 Java 构建为例,Enforcer 检查、框架依赖检查、Sonar 检查、单元测试、集成测试这些步骤,并没有放在同一个构建过程中同步执行,而是通过异步的方式穿插在 CI/CD 当中,甚至可以在构建过程之外执行。

比如, Sonar 扫描在代码集成阶段执行,用户在 GitLab 上发起一个合并请求(Merge Request),这时只对变更的代码进行对比 Sonar 扫描,只要变更代码检查没有问题,那么就可以保证合并之后主干分支的代码也是没问题的。

所以,用户发布时就无需再重复检查了,只要发布后更新远端 Sonar Qube 的数据即可,同时,这个过程完全不会影响用户的构建体验。

1.1.5 善用构建工具

以 Maven 为例,我来带你看看有哪些提速方式,当然其他的构建工具,如 Gradle 等也都可以采用类似的方法:

1、设置合适的堆内存参数。 过小的堆内存参数,会使 Maven 增加 GC 次数,影响构建性能;过大的堆内存参数,不但浪费资源,而且同样会影响性能。因此,构建时,你需要反复试验,得到最优的参数。 2、使用 -Dmaven.test.skip = true 跳过单元测试。 Maven 默认的编译命令是 mvn package,这个命令会自动执行单元测试,但是通常我们的构建机器无法为用户提供一套完整的单元测试环境,特别是在分布式架构下。因此如果单元测试需要服务依赖,则可以去掉它 3、在发布阶段,不使用 Snapshot 版本的依赖。 这就可以在 Maven 构建时不填写 -U 参数来强制更新依赖的检查,省下因为每次检查版本是否更新而浪费的时间。 4、使用 -T 2C 命令进行并行构建。 在该模式下 ,Maven 能够智能分析项目模块之间的依赖关系,然后并行地构建那些相互间没有依赖关系的模块,从而充分利用计算机的多核 CPU 资源。 5、局部构建。 如果你的项目里面有多个没有依赖关系的模块,那么你可以使用 -pl 命令指定某一个或几个模块去编译,而无需构建整个项目,加快构建速度。 6、正确使用 clean 参数。 通常情况下,我们建议用户在构建时使用 clean 参数保证构建的正确性。clean 可以删除旧的构建产物,但其实我们大多数时间可能不需要这个参数,只有在某些情况下(比如,更改了类名,或者删除了一些类)才必须使用这个参数,所以,如果某次变更只是修改了一些方法,或者增加了一些类,那么就不需要强制执行 clean 了。

五种常见的构建提速的方式,分别是

1、升级硬件资源,最直接和粗暴的提速方式; 2、搭建私有仓库,避免从外网下载依赖; 3、使用本地缓存,减少每次构建时依赖下载的消耗; 4、规范构建流程,通过异步方式解决旁支流程的执行; 5、善用构建工具,根据实际情况合理发挥的工具特性。

1.2 构建检测

1.2.1 什么是Maven Enforcer插件?

Maven Enforcer 插件提供了非常多的通用检查规则,比如检查 JDK 版本、检查 Maven 版本、检查依赖版本,等等。下图所示就是一个简单的使用示例。

Alt Image Text

上述的配置会在构建时(准确的说是在 validate 时)完成三项检查:

  • requireMavenVersion 检查 Maven 版本必须大于 3.3.9;
  • requireJavaVersion 检查 JDK 版本必须大于等于 1.9;
  • requireOS 检查 OS 必须是 Windows 系统。

如果你使用 Java 1.8, Maven 3.3.3, 在 Linux 上构建, 便会出现如下的错误:

  • Rule 0: org.apache.maven.plugins.enforcer.RequireMavenVersion failed with message: Detected Maven Version: 3.3.3 is not in the allowed range 3.3.9.
  • Rule 1: org.apache.maven.plugins.enforcer.RequireJavaVersion failed with message: Detected JDK Version: 1.8.0-77 is not in the allowed range 1.9.
  • Rule 2: org.apache.maven.plugins.enforcer.RequireOS failed with message: OS Arch: amd64 Family: unix Name: linux Version: 3.16.0-43-generic is not allowed by Family=windows

从而导致构建失败。

构建系统在构建之前会先检查项目的继承树,继承树中必须包含 super-pom, 否则构建失败。并且,构建系统虽然允许用户自定义 Maven 的构建命令,但是会将 Enforcer 相关的参数过滤掉,用户填写的任何关于 Enforcer 的参数都被视为无效。

Enforcer 会被强制按照统一标准执行,这样就保证了所有应用编译时都要经过检查。

1.2.2 丰富的内置的Enforcer规则

Maven Enforcer 提供了非常丰富的内置检查规则,在这里,我给你重点介绍一下 bannedDependencies 规则、dependencyConvergence 规则,和 banDuplicateClasses 规则。

第一,bannedDependencies 规则

该规则表示禁止使用某些依赖,或者某些依赖的版本,使用示例:

Alt Image Text

该代码检查的逻辑是,只允许使用版本大于等于 1.8.0 的 org.slf4j:slf4j-api 依赖,否则将会出现如下错误:

Alt Image Text

bannedDependencies 规则的常见应用场景包括:

  • 当我们知道某个 jar 包的某个版本有严重漏洞时,可以用这种方法禁止用户使用,从而避免被攻击;
  • 某个公共组件的依赖必须要大于某个版本时,你也可以使用这个方法禁止用户直接引用不兼容的依赖版本,避免公共组件运行错误。

第二,dependencyConvergence 规则

但是,Maven 基于这两个原则处理依赖的方式过于简单粗暴。毕竟在一个成熟的系统中,依赖的关系错综复杂,用户很难一个一个地排查所有依赖的关系和冲突,稍不留神便会掉进依赖的陷阱里,这时 dependencyConvergence 就可以粉墨登场了。

dependencyConvergence 规则的作用是: 当项目中的 A 和 B 分别引用了不同版本的 C 时, Enforce 检查失败。 下面这个实例,可以帮你理解这个规则的作用。

Alt Image Text

org.slf4j:slf4j-jdk14:1.6.1 依赖了 org.slf4j:slf4j-api:1.6.1, 而 org.slf4j:slf4j-nop:1.6.0 依赖了 org.slf4j:slf4j-api:1.6.0,当我们在构建项目时, 便会有如下错误:

Alt Image Text

这时就需要开发人员介入了,使用 dependecy 的 exclusions 元素排除掉一个不合适的版本。 虽然这会给编程带来一些麻烦, 但是非常必要。因为,我始终认为你应该清楚地知道系统依赖了哪些组件, 尤其是在某些组价发生冲突时,这就更加重要了。

第三,banDuplicateClasses 规则

该规则是 Extra Enforcer Rules 提供的,主要目的是检查多个 jar 包中是否存在同样命名的 class,如果存在编译便会报错。

同名 class 若内容不一致,可能会导致 java.lang.NoSuchFieldErrorjava.lang.NoSuchMethodException 等异常,而且排查起来非常困难,因为人的直觉思维很难定位到重复类这个非显性错误上,例如下面这种情况:

org.jboss.netty 包与 io.netty 包中都包含一个名为 NettyBundleActivator 的类,另外还有 2 个重复类:spring/NettyLoggerConfigurator 和 microcontainer/NettyLoggerConfigurator

Alt Image Text

当激活了 banDuplicateClasses 规则之后,Enforcer 检查,便会有如下的报错:

Alt Image Text

通常情况下,用户需要排除一个多余的 jar 包来解决这个问题,但有些情况下两个 jar 包都不能被排除,如果只是个别类名冲突了,那么可以通过 ignoreClasses 去忽略冲突的类,类名可以使用通配符(),如: org.jboss.netty.container.

但是,用户不能随意更改这个配置,因为它必须得到一定的授权,否则随意忽略会产生其他不确定的问题。因此我们将这个插件做了一些改动,通过 API 来获取 ignoreClasses 的内容。当用户有类似的需求时,可以提交 ignoreClasses ,但必须申请,经过 Java 专家审批之后才可忽略掉。

1.2.3 自定义的Enforcer检查规则

除了上述的官方规则,实际上还做了若干个扩展的规则,如:

  • CheckVersion,用于检查模块的版本号必须是数字三段式,或者带有 SNAPSHOT 的数字三段式;
  • CheckGroupId,用于检查 GroupId 是否符合规范,我们为每个部门都分别指定了 GroupId
  • CheckDistributionManagementRepository,用于检查项目的 distributionManagement 中的 repository 节点,并为每个部门都指定了他们在 Nexus 上面的 repositroy;
  • CheckSubModuleSaveVersion,用于检查子模块版本号是否与父模块版本号一致。

1.3 构建资源的弹性伸缩

1.3.1 持续集成工具

这些持续集成工具,最流行的应属 Travis CI、Circle CI、Jenkins CI 这三种。

第一,Travis CI

Travis CI 是基于 GitHub 的 CI 托管解决方案之一,由于和 GitHub 的紧密集成,在开源项目中被广泛使用。

Travis CI 的构建,主要通过 .travis.yml 文件进行配置。这个 .travis.yml 文件描述了构建时所要执行的所有步骤。

另外,Travis CI 可以支持市面上绝大多数的编程语言。但是,因为 Travis 只支持 GitHub,而不支持其他代码托管服务,所以官方建议在使用前需要先具备以下几个条件:

  • 能登录到 GitHub;
  • 对托管在 GitHub 上的项目有管理员权限;
  • 项目中有可运行的代码;
  • 有可以工作的编译和测试脚本。

Travis CI 的收费策略是,对公共仓库免费,对私有仓库收费

第二,CircleCI

CircleCI 是一款很有特色,也是比较流行的,云端持续集成管理工具。CircleCI 目前也仅支持 GitHub 和 Bitbucket 管理。

CircleCI 与其他持续集成工具的区别在于,它们提供服务的方式不同。CircleCI 需要付费的资源主要是它的容器。

你可以免费使用一个容器,但是当你发现资源不够需要使用更多的容器时,你必须为此付费。你也可以选择你所需要的并行化级别来加速你的持续集成,它有 5 个并行化级别(1x、4x、8x,、12x,和 16x)可供选择,分别代表利用几个容器同时进行一个项目的构建,如何选择就取决于你了。

第三,Jenkins CI

Jenkins 是一款自包含、开源的用于自动化驱动编译、测试、交付或部署等一系列任务的自动化服务,它的核心是 Jenkins Pipline 。Jenkins Pipline 可以实现对持续交付插件的灵活组合,以流水线的方式接入到 Jenkins 服务。

Jenkins 还提供了一整套可扩展的工具集,程序员可以通过代码的方式,定义任何流水线的行为。另外,经过多年的发展,Jenkins 已经包含了很多实用的第三方插件,覆盖了持续交付的整个生命周期。

目前,绝大多数组织都选择了 Jenkins 作为内部的持续集成工具,主要原因是:

  • 代码开源, 插件完善,系统稳定;
  • 社区活跃,成功实践与网上资源比较丰富;
  • Jenkins Pipeline 非常灵活好用。

1.3.2 Jenkins Master高可用架构的

目前普遍的 Jenkins 搭建方案是:一个 Jenkins Master 搭配多个 Jenkins Slave。大多数情况下,这种方案可以很好地工作,并且随着构建任务的增加,无脑扩容 Jenkins Slave 也不是一件难事。另外,不管是 Linux Slave 还是 Windows Slave ,Jenkins 都可以很好地支持,并且非常稳定。

但是,随着业务的增长,微服务架构的流行,持续交付理念的深入人心,构建会变得越来越多,越来越频繁,单个 Jenkins Master 终究会成为系统中的瓶颈。

遗憾的是,开源的 Jenkins 并没有给我们提供一个很好的 Master 高可用方案,CloudBees 公司倒是提供了一个高可用的插件,但是价格不菲。

Alt Image Text

思路是在 Jenkins 上面再封装两层: Build Service 暴露构建的 HTTP 接口,接收请求后将任务丢给异步队列 Build Worker,Build Worker 根据不同的策略将任务分发给符合条件的 Jenkins Master

这里的分发条件,可以是编译任务的平台或语言,比如可以将基于 Windows 和 Linux 的任务分别放在不同的 Jenkins Master 上,也可以将 Java 构建和 NodeJS 构建任务放在不同的 Jenkins Master 上。

总而言之,构建任务分发的策略可以是非常灵活的:构建 Worker 和 Jenkins Master 之间有“心跳监测”,可以时刻检查 Jenkins Master 是否还健康,如果有问题就将任务分发到其他等价的 Jenkins Master 上,并给相关人员发送告警通知。

这种拆解 Jenkins Master 主要有以下几个好处:

  • 每个 Job 都可运行在至少两个 Jenkins Master 之上, 保证高可用;
  • 根据不同的策略将 Job 做 Sharding, 避免积压在同一个 Master 上;
  • Jenkins Master 按需配置,按需安装不同的插件,便于管理。

1.3.3 Jenkins Slave弹性伸缩方案

解决了 Jenkins Master 的高可用问题,接着就要去思考如何才能解决 Slave 资源管理和利用率的问题了。

因为,你会发现一个组织的集成和构建往往是周期性的,高峰和低谷都比较明显,而且随着组织扩大,幅度也有所扩大。所以,如果按照高峰的要求来配备 Slave 实例数,那么在低谷时,就很浪费资源了。反之,又会影响速度,造成排队。

因此,我们需要整个 Slave 集群具有更优的弹性:既要好管理,又要好扩展。

容器化的甜头

  • 使用 Dockerfile 描述环境信息相对于之前的文档更加直观,并且可以很自然地跟 Git 结合做到版本化控制,先更新 Dockerfile 再更新镜像是很自然的事。
  • 镜像更容易继承,你可以配置一个 Base 镜像,然后根据不同的需求叠加软件。比如,你的所有构建都需要安装 Git 等软件,那么就可以把它写到 Base 镜像里面。
  • Docker 镜像可以自由控制,开发人员可以自己推送镜像,快速迭代。重建容器的代价比重建虚拟机小得多,容器更加轻量,更容易在本地做测试。

1.4 容器镜像构建

1.4.1 什么是容器镜像?

容器镜像可以是一个完整的 Ubuntu 系统,也可以是一个仅仅能运行一个 sleep 进程的独立环境,大到几 G 小到几 M。而且 Docker 的镜像是分层的,它由一层一层的文件系统组成,这种层级的文件系统被称为 UnionFS。下图就是一个 Ubuntu 15.04 的镜像结构。

Alt Image Text

图中的镜像部分画了一个锁的标记,它表示镜像中的每一层都是只读的,只有创建容器时才会在最上层添加一个叫作 Container layer 的可写层。容器运行后的所有修改都是在这个可写层进行,而不会影响容器镜像本身。

因为这一特性,创建容器非常节省空间,因为一台宿主机上基于同一镜像创建的容器只有这一份镜像文件系统,每次创建多出来的只是每个容器与镜像 diff 的磁盘空间。而虚拟机每增加一个实例,都会在宿主机上占用一个完整的镜像磁盘空间。

1.4.2 什么是Dockerfile

简单来说,Dockerfile 第一个好处就是,可以通过文本格式的配置文件描述镜像,这个配置文件里面可以运行功能丰富的指令,你可以通过运行 docker build 将这些指令转化为镜像。

FROM ubuntu 
RUN apt-get install vim -y

其中,FROM 指令说明我们这个镜像需要继承 Ubuntu 镜像,RUN 指令是需要在镜像内运行的命令。

因为 Ubuntu 镜像内包含了 apt-get 包管理器,所以相当于启动了一个 Ubuntu 镜像的容器,然后在这个容器内部安装 Vim。这期间会产生一个新的 layer,这个新的 layer 包含安装 Vim 所需的所有文件。

Dockerfile 的另外一个好处就是可以描述镜像的变化,通过一行命令就可以直观描述出环境变更的过程,如果再通过 git 进行版本控制,就可以让环境的管理更加可靠与简单。

了解了 Dockerfile 之后,你就可以利用它进行代码更新了,最主要的步骤就以下三步:

  • 将代码包下载到构建服务器;
  • 通过 Dockerfile 的 ADD 命令将代码包加载到容器里;
  • Docker build 完成新的镜像。

1.4.3 镜像构建优化

原则上,我们总是希望能够让镜像保持小巧、精致,这样可以让镜像环境更加清晰,不用占用过多空间,下载也会更快。

那么,如何做好镜像的优化呢?你可以从 3 个方面入手:

  • 选择合适的 Base 镜像;
  • 减少不必要的镜像层的产生;
  • 充分利用指令的缓存。

减少不必要的镜像层,是因为使用 Dockerfile 时,每一条指令都会创建一个镜像层,继而会增加整体镜像的大小。

比如,下面这个 Dockerfile:

FROM ubuntu 
RUN apt-get install vim -y 
RUN apt-get remove vim -y

虽然这个操作创建的镜像中没有安装 Vim,但是镜像的大小和有 Vim 是一样的。原因就是,每条指令都会新加一个镜像层,执行 install vim 后添加了一层,执行 remove vim 后也会添加一层,而这一删除命令并不会减少整个镜像的大小。

Dockerfile 构建的另外一个重要特性是指令可以缓存,可以极大地缩短构建时间。 因为之前也说了,每一个 RUN 都会产生一个镜像,而 Docker 在默认构建时,会优先选择这些缓存的镜像,而非重新构建一层镜像。

1.4.4 镜像构建环境

当我们学会了使用 Dockerfile 构建镜像之后,下一步就是如何搭建构建环境了。搭建构建环境最简单的方式就是在虚拟机上安装 Docker Daemon,然后根据你所使用的语言提供的 Docker 客户端与 Docker Daemon 进行交互,完成构建。

但是,我们推崇构建环境容器化,因为我们的构建环境可能除了 Docker 外,还会有一些其他的依赖,比如编程语言、Git 等等。

接下来,我们就看看构建环境如何实现容器化。一般情况下,用容器来构建容器镜像有两种方式:

  • Docker Out Of Docker(DooD)
  • Docker In Docker(DinD)

第一,Docker Out Of Docker(DooD)

这种方式比较简单,首先在虚拟机上安装 Docker Daemon,然后将你的构建环境镜像下载下来启动一个容器。

在默认情况下,Docker 客户端都是通过 /var/run/docker.sockDocker Daemon 进行通信。我们在创建 Docker 实例时,把外部的 /var/run/docker.sock mount 到容器内部,这样容器内的 Docker 客户端就可以与外部的 Docker Daemon 进行通信了。

另外,你还需要注意权限问题,容器内部的构建进程必须拥有读取 /var/run/docker.sock 的权限,才可以完成通信过程。

第二,Docker In Docker(DinD)

Docker In Docker ,就是在容器内部启动一个完整的 Docker Daemon 进程,然后构建工具只需要和该进程交互,而不影响外部的 Docker 进程。

默认情况下,容器内部不允许开启 Docker Daemon 进程,必须在运行容器的时候加上 --privileged 参数,这个参数的作用是真正取得 root 的权限。另外,Docker 社区官方提供了一个 docker:dind 镜像可以直接拿来使用。

这样一来,容器内部 Docker Daemon 就和容器外部的 Docker Daemon 彻底分开了,容器内部就是一个完整的镜像构建环境,是不是很神奇。

然而 DinD 也不是百分之百的完美和健壮,它也有一些关于安全和文件系统的问题。此外,因为每个容器都有独立的 /var/lib/docker 用来保存镜像文件,一旦容器被重启了,这些镜像缓存就消失了,这可能会影响我们构建镜像的性能。

1.4.5 总结

首先,容器镜像是一个独立的文件系统,它包含了容器运行初始化时所需要的数据或软件。Docker 容器的文件系统是分层的、只读的,每次创建容器时只要在最上层添加一个叫作 Container layer 的可写层就可以了。这种创建方式不同于虚拟机,可以极大的减少对磁盘空间的占用。

其次,Docker 提供了 Dockerfile 这个可以描述镜像的文本格式的配置文件。你可以在 Dockerfile 中运行功能丰富的指令,并可以通过 docker build 将这些指令转化为镜像。

再次,基于 Dockerfile 的特性,我分享了 Dockerfile 镜像构建优化的三个建议,包括:选择合适的 Base 镜像、减少不必要的镜像层产生,以及善用构建缓存。

1.5 如何做好容器镜像的个性化及合规检查

1.5.1 自定义镜像发布

Docker Clair 是一种静态检查,但对于动态的情况就显得无能为力了。所以,对于镜像的安全规则我还总结了如下的一些基本建议:

  • 基础镜像来自于 Docker 官方认证的,并做好签名检查;
  • 不使用 root 启动应用进程;
  • 不在镜像保存密码,Token 之类的敏感信息;
  • 不使用 --privileged 参数标记使用特权容器;
  • 安全的 Linux 内核、内核补丁。如 SELinux,AppArmor,GRSEC 等。

  • 用户自定义环境脚本,通过 build-env.sh 和 image-env.sh 两个文件可以在构建的两个阶段改变镜像的内容;

  • 平台环境选项与服务集市,利用这两个自建系统,可以将个性化的内容进行抽象,以达到快速复用,和高度封装的作用;

  • 自定义镜像,是彻底解决镜像个性化的方法,但也要注意符合安全和合规的基本原则。

除了 Clair 进行 CVE 扫描之外,还有其他一些关于镜像安全的工具也可以从其他方面进行检查,

二、L5发布及监控

2.1 发布

部署和发布是不是一回事儿?

有一些观点认为,部署和发布是有区别的,前者是一个技术范畴,而后者则是一种业务决策。这样的理解应该说是正确的。应用被部署,并不代表就是发布了,比如旁路运行(dark launch)方式,对于客户端产品更是如此的。

但对互联网端的产品来说,这个概念就比较模糊了,所以从英文上来看,我们通常既不用 deploy 这个词,也不用 release 这个词,而是使用 rollout 这个词。所以,从用词的选择上,我们就可以知道,发布是一个慢慢滚动向前、逐步生效的过程。

2.1.1 发布的需求

因此,我们想要的应该是:一个易用、快速、稳定、容错力强,必要时有能力迅速回滚的发布系统

2.1.2 什么是好的发布流程?

这套程序是否能满足发布的需求:快速、易用、稳定、容错、回滚顺滑。

  • 易用:执行脚本就好,填入参数,一键执行。
  • 快速:自动化肯定比手工快,并且有提升空间。比如,因为有版本的概念,我们可以跳过相同版本的部署,或是某些步骤。
  • 稳定:因为这个程序逻辑比较简单,而且执行步骤并不多,没有交叉和并行,所以稳定性也没什么大的挑战。
  • 容错性强:表现一般,脚本碰到异常状况只能停下来,但因为版本间是隔离的,不至于弄坏老的服务,通过人工介入仍能恢复。
  • 回滚顺滑:因为每个版本都是完整的可执行产物,所以回滚可以视作使用旧版本重新做一次发布。甚至我们可以在目标机器上缓存旧版本产物,实现超快速回滚。

2.1.3 扩展到集群

当发布的目标是一组机器而不是一台机器时,主要问题就变成了如何协调整个过程。

比如,追踪、同步一组机器目前发布进行到了哪一步,编排集群的发布命令就成为了更核心功能。好消息是,集群提供了新的、更易行的方法提高系统的发布时稳定性,其中最有用的一项被称为灰度发布。

灰度发布是指,渐进式地更新每台机器运行的版本,一段时期内集群内运行着多个不同的版本,同一个 API 在不同机器上返回的结果很可能不同。 虽然灰度发布涉及到复杂的异步控制流,但这种模式相比简单粗暴的“一波流”显然要安全得多。

2.1.4 几种常见的灰度方式

灰度发布中最头疼的是如何保持服务的向后兼容性,发现苗头不对后如何快速切回老的服务。这在微服务场景中,大量服务相互依赖,A 回滚需要 B 也回滚,或是 A 的新 API 测试需要 B 的新 API 时十分头疼。为了解决这些问题,业界基于不同的服务治理状况,提出了不同的灰度理念。

蓝绿发布

是先增加一套新的集群,发布新版本到这批新机器,并进行验证,新版本服务器并不接入外部流量。此时旧版本集群保持原有状态,发布和验证过程中老版本所在的服务器仍照常服务。

验证通过后,流控处理把流量引入新服务器,待全部流量切换完成,等待一段时间没有异常的话,老版本服务器下线。

  • 这种发布方法需要额外的服务器集群支持,对于负载高的核心应用机器需求可观,实现难度巨大且成本较高。
  • 蓝绿发布的好处是所有服务都使用这种方式时,实际上创造了蓝绿两套环境,隔离性最好、最可控,回滚切换几乎没有成本。

滚动发布

  • 是不添加新机器,从同样的集群服务器中挑选一批,停止上面的服务,并更新为新版本,进行验证,验证完毕后接入流量。
  • 重复此步骤,一批一批地更新集群内的所有机器,直到遍历完所有机器。 这种滚动更新的方法比蓝绿发布节省资源,但发布过程中同时会有两个版本对外提供服务,无论是对自身或是调用者都有较高的兼容性要求,需要团队间的合作妥协。但这类问题相对容易解决,实际中往往会通过功能开关等方式来解决。

金丝雀发布

从集群中挑选特定服务器或一小批符合要求的特征用户,对其进行版本更新及验证,随后逐步更新剩余服务器。这种方式,比较符合携程对灰度发布的预期,但可能需要精细的流控和数据的支持,同样有版本兼容的需求。

2.2 发布系统的用户体验

2.2.1 张页面展示发布信息

首先应该有 1 张页面,且仅有 1 张页面,能够展示发布当时的绝大多数信息、数据和内容,这个页面既要全面,又要精准。 全面指的是内容清晰完整,精准指的是数据要实时、可靠。

除了以上的要求外,对于实际的需求,还要考虑 2 个时态,即发布中和未发布时,展示的内容应该有所区别。

  • 发布中:自然应该展示发布中的内容,包括处理的过程、结果、耗时、当前情况等等。
  • 未发布时:应该显示这个应用历史发布的一个过程,也就是整个版本演进的路线图,以及当前各集群、各服务器上具体版本的情况。

2.2.2 个操作按钮简化使用

最终,用户在页面上可能会看到的同时出现的按钮组合有以下四种情况:

  • 开始发布,1 个按钮;
  • 中断发布,1 个按钮;
  • 中断或重试发布,2 个按钮,发生在有局部错误的情况下;
  • 中断或继续发布,2 个按钮,发生在发布被刹车时。

2.2.3 种发布结果

系统不复杂了,用户体验自然也就简单了。这是一个相辅相成的过程。

从最抽象的角度来说,发布系统只需要 3 种结果,即:成功、失败和中断。

  • 成功状态:很好理解,即整个发布过程,所有的实例发布都成功;
  • 失败状态:只要发布过程中有一个步骤、一个实例失败,则认为整个发布事务失败;
  • 中断状态:发布过程中任何时间点都可以允许中断此次发布,中断后事务结束。

2.2.4 类操作选择

将发布结果高度概括为成功、失败和中断后,配合这三种状态,我们可以进一步地定义出最精简的 4 种用户操作行为,即开始发布、停止发布、发布回退和发布重试。

  • 开始发布,指的是用户操作开始发布时,需要选择版本、发布集群、发布参数,配置提交后,即可立即开始发布。
  • 停止发布,指的是发布过程中如果遇到了异常情况,用户可以随时停止发布,发布状态也将停留在操作“停止发布”的那一刻。
  • 发布回退,指的是如果需要回退版本,用户可以在任意时刻操作“发布回退”,回退到历史上最近一次发布成功的版本。
  • 发布重试,指的是在发布的过程中,因为种种原因导致一些机器发布失败后,用户可以在整个事务发布结束后,尝试重新发布失败的机器,直到发布完成。

2.2.5 个发布步骤

  • markdown:为了减少应用发布时对用户的影响,所以在一个实例发布前,都会做拉出集群的操作,这样新的流量就不会再继续进入了。
  • download:这就是根据版本号下载代码包的过程;
  • install:在这个过程中,会完成停止服务、替换代码、重启服务这些操作;
  • verify:除了必要的启动预检外,这一步还包括了预热过程;
  • markup:把实例拉回集群,重新接收流量和请求。

2.2.6 大页面主要内容

  • 第一,集群。集群是发布的标准单元
  • 第二,实例。实例是集群的成员,通常情况下,一个集群会有多个实例承载流量。在界面上,用户可以查看实例的基本信息,了解实例的 IP、部署状态、运行状态等。用户能够看到发布时的状态与进度,这些信息可以帮助用户更好地控制发布。 * 第三,发布日志。在发布中和发布完成后,用户都可以通过查询发布日志了解发布时系统运行的日志,包括带有时间戳的执行日志和各种提示与报错信息,方便后续排查问题。
  • 第四,发布历史。发布历史对发布系统来说尤为重要。用户可以通过发布历史了解集群过去所做的变更,并且可以清晰地了解集群回退时将会回退到哪一天发布的哪个版本。
  • 第五,发布批次。由于集群中有很多实例,如何有序地执行发布,就是比较重要的事情。设定发布批次,可以让集群的发布分批次进行,避免问题版本上线后一下子影响所有的流量。每个批次中的实例采用并行处理的方式,而多个批次间则采用串行处理的方式。
  • 第六,发布操作。所有的发布操作按钮都会集中在这个区域,以便用户快速定位。

所以,在考虑灰度发布系统的用户体验时,我建议你可以参考以下三个原则:

  • 信息要全面直观,并且聚合,而不要分散;
  • 操作要简单直接,不要让用户做过多思考;
  • 步骤与状态要清晰,减少模糊的描述。

2.3 发布系统的核心架构和功能设计

2.3.1 发布系统架构

2.3.2 发布系统核心模型

发布系统的核心模型主要包括 Group、DeploymentConfig、Deployment、DeploymentBatch,和 DeploymentTarget 这 5 项。

Alt Image Text

同时,Group 的属性非常重要,包括 Site 站点、Path 虚拟路径、docBase 物理路径、Port 应用端口、HealthCheckUrl 健康检测地址等,这些属性都与部署逻辑息息相关。

所以这样设计,是因为 group 这个对象直接表示一个应用的一组实例,这样既可以支持单机单应用的部署架构,也可以支持单机多应用的情况。

DeploymentConfig,即发布配置,提供给用户的可修改配置项要通俗易懂,包括:单个批次可拉出上限比、批次间等待时间、应用启动超时时间、是否忽略点火。

Deployment,即一个发布实体,主要包括 Group 集群、DeploymentConfig 发布配置、Package 发布包、发布时间、批次、状态等等信息。

DeploymentBatch,即发布批次,通常发布系统选取一台服务器作为堡垒批次,集群里的其他服务器会按照用户设置的单个批次可拉出上限比划分成多个批次,必须先完成堡垒批次的发布和验证,才能继续其他批次的发布。

DeploymentBatch,即发布批次,通常发布系统选取一台服务器作为堡垒批次,集群里的其他服务器会按照用户设置的单个批次可拉出上限比划分成多个批次,必须先完成堡垒批次的发布和验证,才能继续其他批次的发布。

这里一定要注意,发布系统的对象模型和你所采用的部署架构有很大关系

2.3.3 发布流程及状态流转

发布系统的主流程大致是:

同一发布批次中,目标服务器并行发布;不同发布批次间则串行处理。每台目标服务器在发布流程中的五个阶段包括 Markdown、Download、Install、Verify、Markup。

Alt Image Text

首先,借助于 Celery 分布式任务队列的 Chain 函数,发布系统将上述的 Markdown、Download、Install、Verify、Markup 五个阶段定义为一个完整的链式任务工作流,保证一个 Chain 函数里的子任务会依次执行。

其次,每个子任务执行成功或失败,都会将 DeploymentTarget 设置为对应的发布状态。例如,堡垒批次中的 DeploymentTarget 执行到 Verify 点火这个任务时,如果点火验证成功,那么 DeploymentTarget 会被置为 VERIFY_SUCCESS(点火成功)的状态,否则置为 VERIFY_FAILURE(点火失败)的状态。

发布过程中,如果有任意一台 DeploymentTarget 发布失败,都会被认为是发布局部失败,允许用户重试发布。因此,重试发布只针对于有失败的服务器批次进行重试,对于该批次中已经发布成功的服务器,发布系统会对比当前运行版本与发布目标版本是否一致,如果一致且点火验证通过的话,则直接跳过。

这里需要注意的是, 堡垒批次不同于其他批次:堡垒批次中 DeploymentTarget 的 Chain 的最后一个子任务是 Verify 点火,而不是 Markup。

再次,点火验证成功,DeploymentTarget 的状态流转到 VERIFY_SUCCESS 后,需要用户在发布系统页面上触发 Baking 操作,即堡垒批次中 DeploymentTarget 的 Markup,此时执行的是一个独立的任务事务,会将堡垒批次中的服务器拉入集群,接入生产流量。也就是说,这部分是由用户触发而非自动拉入。BAKE_SUCCESS 堡垒拉入成功之后,就是其他批次的 RollingOut 事务了,这也是一个独立的任务,需要由用户触发其他批次开始发布的操作。

2.4 如何利用监控保障发布质量

2.4.1 监控的分类

从一般意义上来讲,我们会把监控分为以下几类:

1、用户侧监控,关注的是用户真正感受到的访问速度和结果; 2、网络监控,即 CDN 与核心网络的监控; 3、业务监控,关注的是核心业务指标的波动; 4、应用监控,即服务调用链的监控; 5、系统监控,即基础设施、虚拟机及操作系统的监控。

因此,我们要做好一个监控系统,可以从这五个层面去考虑,将这五个层面整合就可以做成一个完整的、端到端的全链路监控系统。当然,监控系统的这 5 个层次的目标和实现方法有所不同,接下来我将分别进行介绍。

2.4.2 用户侧监控

用户侧的监控通常从以下几个维度进行,这些监控数据既可以通过打点的方式,也可以通过定期回收日志的方式收集。

  • 端到端的监控,主要包括包括一些访问量、访问成功率、响应时间、发包回包时间等等监控指标。同时,我们可以从不同维度定义这些指标,比如:地区、运营商、App 版本、返回码、网络类型等等。因此,通过这些指标,我们就可以获得用户全方位的感受。
  • 移动端的日志。我们除了关注系统运行的日志外,还会关注系统崩溃或系统异常类的日志,以求第一时间监控到系统故障。
  • 设备表现监控,主要指对 CPU、内存、温度等的监控,以及一些页面级的卡顿或白屏现象;或者是直接的堆栈分析等。
  • 唯一用户 ID 的监控。除了以上三种全局的监控维度外,用户侧的监控一定要具备针对唯一用户 ID 的监控能力,能够获取某一个独立用户的具体情况。

2.4.3 网络监控

网络是整个系统通路的保障。因为大型生产网络配置的复杂度通常比较高,以及系统网络架构的约束,所以网络监控一般比较难做。

一般情况下,从持续交付的角度来说,网络监控并不需要做到太细致和太深入,因为大多数网络问题最终也会表现为其他应用层面的故障问题。但是,如果你的诉求是要快速定位 root cause,那就需要花费比较大的精力去做好网络监控了。

网络监控,大致可以分为两大部分:

  • 公网监控。这部分监控,可以利用模拟请求的手段(比如,CDN 节点模拟、用户端模拟),获取对 CDN、DNS 等公网资源,以及网络延时等监控的数据。当然,你也可以通过采样的方式获取这部分数据。
  • 内网监控。这部分监控,主要是对机房内部核心交换机数据和路由数据的监控。如果你能打造全局的视图,形成直观的路由拓扑,可以大幅提升监控效率。

2.4.4 业务监控

如果你的业务具有连续性,业务量达到一定数量后呈现比较稳定的变化趋势,那么你就可以利用业务指标来进行监控了。一般情况下,单位时间内的订单预测线,是最好的业务监控指标。

任何的系统故障或问题,影响最大的就是业务指标,而一般企业最重要的业务指标就是订单和支付。因此,监控企业的核心业务指标,能够以最快的速度反应系统是否稳定。 反之,如果系统故障或问题并不影响核心业务指标,那么也就不太会造成特别严重的后果,监控的优先级和力度也就没有那么重要。

2.4.5 应用监控

分布式系统下,应用监控除了要解决常规的单个应用本身的监控问题外,还需要解决分布式系统,特别是微服务架构下,服务与服务之间的调用关系、速度和结果等监控问题。因此,应用监控一般也被叫作调用链监控。

调用链监控一般需要收集应用层全量的数据进行分析,要分析的内容包括:调用量、响应时长、错误量等;面向的系统包括:应用、中间件、缓存、数据库、存储等;同时也支持对 JVM 等的监控。

调用链监控系统,一般采用在框架层面统一定义的方式,以做到数据采集对业务开发透明,但同时也需要允许开发人员自定义埋点监控某些代码片段。

另外,除了调用链监控,不要忘了最传统的应用日志监控。将应用日志有效地联合,并进行分析,也可以起到同样的应用监控作用,但其粒度和精准度比中间件采集方式要弱得多。

2.4.6 系统监控

系统监控,指的是对基础设施的监控。我们通常会收集 CPU、内存、I/O、磁盘、网络连接等作为监控指标。

对于系统监控的指标,我们通常采用定期采样的方式进行采集,一般选取 1 分钟、3 分钟或 5 分钟的时间间隔,但一般不会超过 5 分钟,否则监控效果会因为间隔时间过长而大打折扣

2.4.7 发布监控的常见问题

快速发现发布带来的系统异常。

对于这样的诉求,优先观察业务监控显然是最直接、有效的方式。但是只观察业务监控并不能完全满足这样的需求,因为有两种情况是业务监控无能为力的:

  • 第一种情况是我们所谓的累积效应,即系统异常需要累积到一定量后才会表现为业务异常;
  • 另外一种情况就是业务的阴跌,这种小幅度的变化也无法在业务监控上得到体现。

第一,测试环境也要监控吗?

首先,我们需要认识到一个问题,即:部署一套完整的监控系统的代价非常昂贵。而且,监控作为底层服务,你还要保证它的稳定性和扩展性。

我来说说我建议的做法:

  • 如果你的监控系统只能做到系统监控或日志级别的系统监控,那么对于一些对系统性能压榨比较厉害、对稳定性也没太多要求的测试环境来说,无需配备完整的监控系统。
  • 如果你的监控系统已经做到了调用链甚至全链路的监控,那么监控系统无疑就是你的“鹰眼 ",除了发现异常,它还可以在定位异常等方面给你帮助(比如,对测试环境的 Bug 定位、性能测试等都有极大帮助)。在这样的情况下,你就一定要为测试环境配备监控系统

第二个问题,发布后需要监控多久?

我建议的发布后监控时间为 30 分钟。

第三个问题,如何确定异常是由我的发布引起的?

解决这个问题,你需要建立一套完整的运维事件记录体系,并将发布纳入其中,记录所有的运维事件。当有异常情况时,你可以根据时间线进行相关性分析。

2.4.8 总结

首先,我介绍了监控的几种分类,以及分别可以采用什么方式去采集数据:

  • 用户侧监控,可以通过打点收集,或者定期采集日志的方式进行数据收集;
  • 网络监控,通过模拟手段或定期采样进行收集;
  • 业务监控,需要定义正确的指标以及相匹配的采集技术,务必注意实时性;
  • 应用监控,可以通过中间件打点采集,也可以通过日志联合分析进行数据采集;
  • 系统监控,通常采用定期采样的方式收集数据。

其次,我和你分享了三个对发布来说特别重要的监控问题:

  • 测试环境的监控需要视作用而定,如果不能帮助分析和定位问题,则不需要很全面的监控;
  • 一般发布后,我建议继续坚持监控 30 分钟,把这个流程纳入发布流程中;
  • 完整的运维事件记录体系,可以帮你定位某次故障是否是由发布引起的。

三、L6测试管理

3.1 代码静态检查实践

因为这个专栏我们要解决的最主要的问题是持续交付,所以我在这个测试管理这个系列里面,不会去过多的展开测试本身的内容,而是要把重点放在与持续交付相关的三个重点上:

  • 代码静态检查;
  • 破坏性测试;
  • Mock 与回放。

3.1.1 为什么需要代码静态检查?

代码静态检查,即静态代码分析,是指不运行被测代码,仅通过分析或检查源程序的语法、结构、过程、接口等检查程序的正确性,并找出代码中隐藏的错误和缺陷(比如参数不匹配、有歧义的嵌套语句、错误的递归、非法计算、可能出现的空指针引用等等)。

在软件开发的过程中,静态代码分析往往在动态测试之前进行,同时也可以作为设计动态测试用例的参考。有统计数据证明,在整个软件开发生命周期中,有 70% 左右的代码逻辑设计和编码缺陷属于重复性错误,完全可以通过静态代码分析发现和修复。

但是,代码静态检查规则的建立往往需要大量的时间沉淀和技术积累,因此对初学者来说,挑选合适的静态代码分析工具,自动化执行代码检查和分析,可以极大地提高代码静态检查的可靠性,节省测试成本

3.1.2 静态检查工具的优势

总体来说,静态检查工具的优势,主要包括以下三个方面:

  • 帮助软件开发人员自动执行静态代码分析,快速定位代码的隐藏错误和缺陷;
  • 帮助软件设计人员更专注于分析和解决代码设计缺陷;
  • 显著减少在代码逐行检查上花费的时间,提高软件可靠性的同时可以降低软件测试成本。

目前,已经有非常多的、成熟的代码静态检查工具了。其中,SonarQube 是一款目前比较流行的工具,国内很多互联网公司都选择用它来搭建静态检查的平台。

SonarQube 采用的是 B/S 架构,通过插件形式,可以支持对 Java、C、C++、JavaScript 等二十几种编程语言的代码质量管理与检测。

Sonar 通过客户端插件的方式分析源代码,可以采用 IDE 插件、Sonar-Scanner 插件、Ant 插件和 Maven 插件等,并通过不同的分析机制完成对项目源代码的分析和扫描,然后把分析扫描的结果上传到 Sonar 的数据库,之后就可以通过 Sonar Web 界面管理分析结果。

https://chao-xi.github.io/jxjenkinsbook/chap8/2chap8_code_quality_sonarqube/

3.1.3 跳过检查的几类方式

为持续交付体系搭建好静态检查服务并设置好 Rules 后,你千万不要认为事情结束了,直接等着看检查结果就行了。因为,通常还会有以下问题发生:

  • 代码规则可能不适合程序语言的多个版本;
  • 第三方代码生成器自动产生的代码存在问题,该怎么略过静态检查;
  • 静态检查受客观情况的限制,存在误报的情况;
  • 某些规则对部分情况检查得过于苛刻;
  • 其他尚未归类的不适合做静态检查的问题。

其实,这些问题都有一个共同特点:静态检查时不该报错的地方却报错了,不该报严重问题的地方却报了严重问题。

于是,我们针对这个共性问题的处理策略,可以分为三类:

  • 把某些文件设置为完全不做静态检查;
  • 把某些文件内部的某些类或方法设置为不做某些规则的检查;
  • 调整规则的严重级别,让规则适应语言的多个版本。

3.1.4 如何提高静态检查的效率?

提高静态检查的效率的重要性,可以概括为以下两个方面:

  • 其一,能够缩短代码扫描所消耗的时间,从而提升整个持续交付过程的效率;
  • 其二,我们通常会采用异步的方式进行静态检查,如果这个过程耗时特别长的话,会让用户产生困惑,从而质疑执行静态检查的必要性。

怎么才能提升静态检查的效率呢

除了提升静态检查平台的处理能力外,在代码合入主干前采用增量形式的静态检查,也可以提升整个静态检查的效率。增量静态检查,是指只对本次合入涉及的文件做检查,而不用对整个工程做全量检查。

3.1.5 Sonar代码静态检查实例

  • 第一步:搭建 Sonar 服务,安装 CheckStyle 等插件。
  • 第二步:设置统一的 Java 检查规则。
  • 第三步:在 IDE 中安装 SonarLint 插件后,就可以使用 SonarSource 的自带规则了。
  • 第四步:如果 SonarLint 的检查规则不能满足开发环境的要求,你可以执行相关的 mvn 命令,把检查结果吐到 Sonar 服务器上再看检查结果,命令如下:
mvn org.sonarsource.scanner.maven:sonar-maven-plugin:3.2:sonar -f ./pom.xml -Dsonar.host.url=sonar 服务器地址 -Dsonar.login= 账号名称 -Dsonar.password= 账号密码 -Dsonar.profile= 检查规则的集合 -Dsonar.global.exclusions= 排除哪些文件 -Dsonar.branch= 检查的分支
  • 第五步:在 GitLab 的 Merge Request 中增加 Sonar 静态检查的环节,包括检查状态和结果等。

Alt Image Text

  • 第六步:发布到用户验证环境(UAT)前,先查看静态检查结果。如果没有通过检查,则不允许发布。

Alt Image Text

3.2 破坏性测试

3.2.1 什么是破坏性测试?

这里需要注意的是,我们需要使用的测试手段必须是有效的。为什么这样说呢,有两点原因。

第一,破坏性测试的手段和过程,并不是无的放矢,它们是被严格设计和执行的。不要把破坏性测试和探索性测试混为一谈。也就是说,破坏性测试不应该出现,“试试这样会不会出问题”的假设,而且检验破坏性测试的结果也都应该是有预期的。

第二,破坏性测试,会产生切实的破坏作用,你需要权衡破坏的量和度。因为破坏不仅仅会破坏软件,还可能会破坏硬件。通常情况下,软件被破坏后的修复成本不会太大,而硬件部分被破坏后,修复成本就不好说了。所以,你必须要事先考虑好破坏的量和度。

3.2.2 破坏性测试的执行策略

因此,绝大部分破坏性测试都会在单元测试、功能测试阶段执行。而执行测试的环境也往往是局部的测试子环境。

3.2.3 混沌工程

混沌工程是在分布式系统上建立的实验,其目的是建立对系统承受混乱冲击能力的信心。鉴于分布式系统固有的混乱属性,也就是说即使所有的部件都可以正常工作,但把它们结合后,你还是很难预知会发生什么。

说到具体的实验方法,需要遵循以下 4 个步骤,即科学实验都必须遵循的 4 个步骤:

  • 将正常系统的一些正常行为的可测量数据定义为“稳定态”;
  • 建立一个对照组,并假设对照组和实验组都保持“稳定态”;
  • 引入真实世界的变量,如服务器崩溃、断网、磁盘损坏等等;
  • 尝试寻找对照组和实验组之间的差异,找出系统弱点。

混沌工程也有几个高级原则:

  • 使用改变现实世界的事件,就是要在真实的场景中进行实验,而不要想象和构造一些假想和假设的场景;
  • 在生产环境运行,为了发现真实场景的弱点,所以更建议在生产环境运行这些实验;
  • 自动化连续实现,人工的手工操作是劳动密集型的、不可持续的,因此要把混沌工程自动化构建到系统中;
  • 最小爆破半径,与第二条配合,要尽量减少对用户的负面影响,即使不可避免,也要尽力减少范围和程度。

3.2.4 Netflix公司的先驱实践

Netflix 为了保证其系统在 AWS 上的健壮性,创造了 Chaos Monkey,可以说是混沌工程真正的先驱者。

Chaos Monkey 会在工作日期间,随机地杀死一些服务以制造混乱,从而检验系统的稳定性。而工程师们不得不停下手头工作去解决这些问题,并且保证它们不会再现。久而久之,系统的健壮性就可以不断地被提高。

3.3 Mock与回放技术助力自动化回归

3.3.1 持续交付中的测试难点

  • 测试数据的准备和清理。
  • 分布式系统的依赖。
  • 测试用例的高度仿真。

3.3.2 两大利器之一Mock

如果某个对象在测试过程中依赖于另一个复杂对象,而这个复杂对象又很难被从测试过程中剥离出来,那么就可以利用 Mock 去模拟并代替这个复杂对象。

下面这张图就是 Mock 定义的一个具象化展示

Alt Image Text

测试过程中,被测对象的外部依赖情况展示

在测试过程中,你可能会遇到这样的情况。你要测试某个方法和对象,而这个被测方法和对象依赖了外部的一些对象或者操作,比如:读写数据库、依赖另外一个对象的实体;依赖另一个外部服务的数据返回。

而实际的测试过程很难实现这三种情况,比如:单元测试环境与数据库的网络不通;依赖的对象接口还没有升级到兼容版本;依赖的外部服务属于其他团队,你没有办法部署等等。

那么,这时,你就可以利用 Mock 技术去模拟这些外部依赖,完成自己的测试工作。

Mock 因为这样的模拟能力,为测试和持续交付带来的价值,可以总结为以下三点:

  • 使测试用例更独立、更解耦。利用 Mock 技术,无论是单体应用,还是分布式架构,都可以保证测试用例完全独立运行,而且还能保证测试用例的可迁移性和高稳定性。为什么呢? 因为足够独立,测试用例无论在哪里运行,都可以保证预期结果;而由于不再依赖于外部的任何条件,使得测试用例也不再受到外部的干扰,稳定性也必然得到提升。
  • 提升测试用例的执行速度。由于 Mock 技术只是对实际操作或对象的模拟,所以运行返回非常快。特别是对于一些数据库操作,或者复杂事务的处理,可以明显缩短整个测试用来的执行时间。 这样做最直接的好处就是,可以加快测试用例的执行,从而快速得到测试结果,提升整个持续交付流程的效率。
  • 提高测试用例准备的效率。因为 Mock 技术可以实现对外部依赖的完全可控,所以测试人员在编写测试用例时,无需再去特别考虑依赖端的情况了,只要按照既定方式设计用例就可以了。

基于对象和类的 Mock

Mockito 或者 EasyMock 这两个框架的实现原理,都是在运行时,为每一个被 Mock 的对象或类动态生成一个代理对象,由这个代理对象返回预先设计的结果。

这类框架非常适合模拟 DAO 层的数据操作和复杂逻辑,所以它们往往只能用于单元测试阶段。而到了集成测试阶段,你需要模拟一个外部依赖服务时,就需要基于微服务的 Mock 粉墨登场了。

基于微服务的 Mock

基于微服务的 Mock,我个人比较推荐的框架是 Weir Mock 和 Mock Server。这两个框架,都可以很好地模拟 API、http 形式的对象。

从编写测试代码的角度看,Weir Mock 和 Mock Server 这两种测试框架实现 Mock 的方式基本一致:

  • 标记被代理的类或对象,或声明被代理的服务;
  • 通过 Mock 框架定制代理的行为;
  • 调用代理,从而获得预期的结果。

3.3.3 “回放”技术

如何把用户的请求记录下来

  • 第一种方案是,在统一的 SLB 上做统一的拦截和复制转发处理。这个方案的好处是,管理统一,实现难度也不算太大。但问题是,SLB 毕竟是生产主路径上的处理服务,一不小心,就可能影响本身的路由服务,形成故障。所以,我们有了第二种替换方案。
  • 第二种方案是,在集群中扩容一台服务器,在该服务器上启动一个软交换,由该软交换负责复制和转发用户请求,而真正的用户请求,仍旧由该服务器进行处理。 这个方案比第一种方案稍微复杂了一些,但在云计算的支持下,却显得更经济。你可以按需扩容服务器来获取抽样结果,记录结束后释放该服务器资源。这个过程中,你也不需要进行过多的配置操作,就和正常的扩容配置一样,减少了风险。