一、L1量身定制你的持续交付体系¶
1.1 持续交付到底有什么价值?¶
1.1.1 持续集成、持续交付和持续部署的关系¶
我们通常会把软件研发工作拆解,拆分成不同模块或不同团队后进行编码,编码完成后,进行集成构建和测试。这个从编码到构建再到测试的反复持续过程,就叫作“持续集成”。
“持续集成”一旦完成,则代表产品处在一个可交付状态,但并不代表这是最优状态,还需要根据外部使用者的反馈逐步优化。当然这里的使用者并不一定是真正的用户,还可能是测试人员、产品人员、用户体验工程师、安全工程师、企业领导等等。
这个在“持续集成”之后,获取外部对软件的反馈再通过“持续集成”进行优化的过程就叫作“持续交付”,它是“持续集成”的自然延续。
而“持续部署”就是将可交付产品,快速且安全地交付用户使用的一套方法和系统,它是“持续交付”的最后“一公里”。
可见,“持续交付”是一个承上启下的过程,它使“持续集成”有了实际业务价值,形成了闭环,而又为将来达到“持续部署”的高级目标做好了铺垫。
1.1.2 持续交付的显性价值¶
持续交付也通常以“发布流水线”的方式来解释,即研发团队从开发,到测试,再到部署,最终将产品交付给最终用户使用的过程。如下图:

虽然持续交付着重打造的是发布流水线的部分,但它所要达到的目标是在“最终用户”和“研发团队”之间建立紧密的反馈环:通过持续交付新的软件版本,以验证新想法和软件改动的正确性,并衡量这些改动对软件价值的影响。
这里说的“软件价值”,说白了就是收入、日活、GMV 等 KPI 指标了。
1.2 影响持续交付的因素有哪些?¶
1.2.1 组织和文化因素¶
- 第一个层次:紧密配合,这是组织发展,部门合作的基础。
- 第二个层次:集思广益,这就需要组织内各个不同部门,或不同职能的角色,跳出自身的“舒适区”。
- 第三个层次:自我驱动,是理想中的完美组织形式。
持续集成的三个层次
- 分模块编码;
- 整体集成;
- 实现以上两个过程的自动化,并形成闭环;
一般软件企业与交付有关的研发部门包括四个:产品、开发、测试和运维。而这四个部门天然地形成了一个生产流水线,所以形成理想组织的第一层次紧密配合,基本没什么问题。
但是,要达到第二层次集思广益的难度,往往就很大。因为,每个部门有自身的利益,以及自己的工作方式和目标。
- 比如,产品人员和测试人员就是一对矛盾体:产品人员希望产品尽快上线,而测试人员则希望多留时间进行更完整的测试。
- 又比如,开发人员和运维人员也经常矛盾:开发人员希望能有完全权限,而运维人员却控制着生产的 root。
织的问题,还是需要通过组织变革来解决。通常我们会采用以下三种方案:
- 成立项目管理办公室(Project Manage Office,简称 PMO)这样的监督型组织,帮助持续交付落地;
- 独立建立工程效能部门,全面负责包括持续交付在内的研发效率提升工作;
- 使用敏捷形式,如 Scrum,打破职能部门间的“隔离墙”,以产品的形式组织团队,各团队自行推进持续交付 。
总而言之,持续交付必须有与其相适应的组织和文化,否则将很难实施。
1.2.2 流程因素¶
要说持续交付对企业和组织改变最多的是什么,那么一定是流程。
- 耗时较长的流程。比如,一个功能的研发迭代周期为 5 天,而其中有一个上线审核流程,需要花费 3 天时间,那这个流程就严重影响了持续交付,必须被打破。
- 完全人工类的流程。 完全人工操作的流程,一般效率低下,且质量难以保证,持续交付的逐步深入会通过自动化替代这些人工流程的存在。
- 信息报备类的流程。 持续交付过程中同样会产生各种信息流,这些信息有些需要广播,有些需要定点传递。实施持续交付后,这些信息报备类的流程一定会通过异步消息等方式进行改造。
其中,如何对待审批流程是重点。
在持续交付过程中,其实最让你头痛的应该是一些审批流程。这些流程既然叫做审批,那就代表着授权与责任,代表着严谨与严肃,因此也一定有其存在的价值和意义,不能轻易被去除或打破。
1.2.3 架构因素¶
影响持续交付的架构因素,主要有两大部分:系统架构和部署架构
- 第一,系统架构
- 系统架构指系统的组成结构,它决定了系统的运行模式,层次结构,调用关系等。我们通常会遇到的系统架构包括:
- 单体架构,一个部署包,包含了应用所有功能;
- SOA 架构,面向服务,通过服务间的接口和契约联系;
- 微服务架构,按业务领域划分为独立的服务单元,可独立部署,松耦合。
- 系统架构指系统的组成结构,它决定了系统的运行模式,层次结构,调用关系等。我们通常会遇到的系统架构包括:
对单体架构来说:
-
整个应用使用一个代码仓库,在系统简单的情况下,因为管理简单,可以快速简单地做到持续集成;
- 但是一旦系统复杂起来,仓库就会越变越大,开发团队也会越来越大,多团队维护一个代码仓库简直就是噩梦,会产生大量的冲突;
- 而且持续集成的编译时间也会随着仓库变大而变长,团队再也承受不起一次编译几十分钟,结果最终失败的痛苦。
- 应用变复杂后,测试需要全回归,因为不管多么小的功能变更,都会引起整个应用的重新编译和打包。即使在有高覆盖率的自动化测试的帮助下,测试所要花费的时间成本仍旧巨大,且错误成本昂贵。
-
在应用比较小的情况下,可以做到单机部署,简单直接,这有利于持续交付;
- 但是一旦应用复杂起来,每次部署的代价也变得越来越高,这和之前说的构建越来越慢是一个道理。而且部署代价高会直接影响生产稳定性。这显然不是持续交付想要的结果。
对 SOA 架构来说:
- 由于服务的拆分,使得应用的代码管理、构建、测试都变得更轻量,这有利于持续集成的实施。
- 因为分布式的部署,使得测试环境的治理,测试部署变得非常复杂,这里就需要持续交付过程中考虑服务与服务间的依赖,环境的隔离等等。
- 一些新技术和组件的引入,比如服务发现、配置中心、路由、网关等,使得持续交付过程中不得不去考虑这些中间件的适配。
对微服务架构来说:
- 其实,微服务架构是一种 SOA 架构的演化,它给持续交付带来的影响和挑战也基本与 SOA 架构一致。
- 当然,如果你采用容器技术来承载你的微服务架构,就另当别论了,这完全是一个持续交付全新的领域,这部分内容我将在后续文章中跟你分享
第二,部署架构
部署架构指的是,系统在各种环境下的部署方法,验收标准,编排次序等的集合。它将直接影响你持续交付的“最后一公里”。
- 首先,你需要考虑,是否有统一的部署标准和方式。
- 其次,需要考虑发布的编排次序。
- 再次,是 markdown 与 markup 机制。
- 最后,是预热与自检。
二、L2配置管理¶
2.1 代码分支策略的选择¶
你需要思考的几个问题如下:
- Google 和 Facebook 这两个互联网大咖都在用主干开发(Trunk Based Development,简称 TBD),我们是不是也参照它俩,采用主干开发分支策略?
- 用 Google 搜索一下,会发现有个排名很靠前的分支策略,叫“A successful Git branching model”(简称 Git Flow),它真的好用吗?团队可以直接套用吗?
- GitHub 和 GitLab 这两个当下最流行的代码管理平台,各自推出了 GitHub Flow 和 GitLab Flow,它们有什么区别?适合我使用吗?
2.1.1 谈谈主干开发(TBD)¶
主干开发是一个源代码控制的分支模型,开发者在一个称为 “trunk” 的分支(Git 称 master) 中对代码进行协作,除了发布分支外没有其他开发分支。
Google 和 Facebook 都是采用“主干开发”的方式,代码一般直接提交到主干的头部,这样可以保证所有用户看到的都是同一份代码的最新版本。
“主干开发”确实避免了合并分支时的麻烦,因此像 Google 这样的公司一般就不采用分支开发,分支只用来发布。
大多数时候,发布分支是主干某个时点的快照。
以后的改 Bug 和功能增强,都是提交到主干,必要时 cherry-pick (选择部分变更集合并到其他分支)到发布分支。与主干长期并行的特性分支极为少见。
由于不采用“特性分支开发”,所有提交的代码都被集成到了主干,为了保证主干上线后的有效性,一般会使用特性切换(feature toggle)。特性切换就像一个开关可以在运行期间隐藏、启用或禁用特定功能,项目团队可以借助这种方式加速开发过程。
2.1.2 谈谈特性分支开发¶
- 第一,Git Flow
Git 刚出来的那些年,可参考的模型不多,所以 Git Flow 模型在 2011 年左右被大家当作了推荐的分支模型,至今也还有项目团队在使用。然而,Git Flow 烦琐的流程也被许多研发团队吐槽,大家普遍认为 hotfix 和 release 分支显得多余,平时都不会去用。

- 第二,GitHub Flow
GitHub Flow 是 GitHub 所使用的一种简单流程。该流程只使用 master 和特性分支,并借助 GitHub 的 pull request 功能。

在 GitHub Flow 中,master 分支中包含稳定的代码,它已经或即将被部署到生产环境。
任何开发人员都不允许把未测试或未审查的代码直接提交到 master 分支。
对代码的任何修改,包括 Bug 修复、热修复、新功能开发等都在单独的分支中进行。不管是一行代码的小改动,还是需要几个星期开发的新功能,都采用同样的方式来管理。
当需要修改时,从 master 分支创建一个新的分支,所有相关的代码修改都在新分支中进行。开发人员可以自由地提交代码和提交到远程仓库。
当新分支中的代码全部完成之后,通过 GitHub 提交一个新的 pull request。团队中的其他人员会对代码进行审查,提出相关的修改意见。由持续集成服务器(如 Jenkins)对新分支进行自动化测试。当代码通过自动化测试和代码审查之后,该分支的代码被合并到 master 分支。再从 master 分支部署到生产环境。
GitHub Flow 的好处在于非常简单实用,开发人员需要注意的事项非常少,很容易形成习惯。当需要修改时,只要从 master 分支创建新分支,完成之后通过 pull request 和相关的代码审查,合并回 master 分支就可以了。
- 第三,GitLab Flow
上面提到的 GitHub Flow,适用于特性分支合入 master 后就能马上部署到线上的这类项目,但并不是所有团队都使用 GitHub 或使用 pull request 功能,而是使用开源平台 GitLab,特别是对于公司级别而言,代码作为资产,不会随意维护在较公开的 GitHub 上(除非采用企业版)。
GitLab Flow 针对不同的发布场景,在 GitHub Flow(特性分支加 master 分支)的基础上做了改良,额外衍生出了三个子类模型,如表 2 所示。

表 2 GitLab Flow 的三个分支

图 3 带生产分支的 GitLab Flow

图 4 带环境分支的 GitLab Flow

GitLab Flow 的特性分支合入 master 用的是“Merge Request”,功能与 GitHub Flow 的“pull request”相同,这里不再赘述。
通过 Git Flow、GitHub Flow 和 GitLab Flow(3 个衍生类别) 这几个具体模型的介绍,我给你总结一下特性分支开发的优缺点。

2.1.3 选出最适合的分支策略¶

2.2 依赖管理¶
操作系统的依赖管理工具,比如 CentOS 的 yum,Debian 的 apt,Arch 的 Packman,macOS 的 Homebrew; 编程语言的依赖管理工具,比如 Java 的 Maven, .Net 的 nuget,Node.js 的 npm,Golang 的 go get,Python 的 pip,Ruby 的 Gem 。
这些平台的解决思路都是将依赖放到共同的仓库,然后管理工具通过依赖描述文件去中央仓库获取相应的包。
一个典型的依赖管理工具通常会有以下几个特性:
- 统一的命名规则,也可以说是坐标,在仓库中是唯一的,可以被准确定位到;
- 统一的中心仓库可以存储管理依赖和元数据;
- 统一的依赖配置描述文件;
- 本地使用的客户端可以解析上述的文件以及拉取所需的依赖。
2.2.1 Maven如何管理依赖?¶
Maven 是 Java 生态系统里面一款非常强大的构建工具,其中一项非常重要的工作就是对项目依赖进行管理。
Maven 使用 XML 格式的文件进行依赖配置描述的方式,叫作 POM(Project Object Model ),以下就是一段简单的 pom.xml 文件片段:

在 POM 中,根元素 project 下的 dependencies 可以包含一个或多个 dependency 元素,以声明一个或者多个项目依赖。每个依赖可以包含的元素有:
1、groupId、artifactId、version: 依赖的基本坐标; 2、type: 依赖的类型,默认为 jar; 3、scope: 依赖的范围; 4、optional: 标记依赖是否可选; 5、exclusions: 用来排除传递性依赖;
要想用好 Maven 管理依赖,你必须理解每一项的含义,而新手通常傻傻分不清楚。举个例子,依赖范围这一项,Maven 在不同的时期会使用不同的 classpath :
- 比如,junit 只有在测试的时候有用,那么将其设为 test scope 就可以;
- 再比如 ,servlet API 这个 jar 包只需要在编译的时候提供接口,但是实际在运行时会有对应的 servlet 容器提供,所以没必要打到 war 包中去,这时候只需要指定在 provided scope 就可以了
包管理工具还解决了依赖传递的问题,比如你的项目 A 依赖了 B,而 B 依赖了 C 和 D,那么在获取依赖的时候会把 B、C、D 都一起拉下来,这样可以节省大量的时间。
- 第一原则: 最短路径优先原则。 比如,A 依赖了 B 和 C,而 B 也依赖了 C,那么 Maven 会使用 A 依赖的 C 的版本,因为它的路径是最短的。
- 第二原则: 第一声明优先原则。 比如,A 依赖了 B 和 C,B 和 C 分别依赖了 D,那么 Maven 会使用 B 依赖的 D 的版本,因为它是最先声明的。
根据这两个原则,Maven 就可以确定一个项目所有依赖的列表,但它处理依赖的方式还是有些简单粗暴。有时 Maven 的决定结果并不是你想要的,所以我们在使用 Maven 的时候还是要多加小心。
2.2.2 Maven最佳实践¶
- 生产环境尽量不使用 SNAPSHOT 或者是带有范围的依赖版本,可以减少上线后的不确定性,我们必须保证,测试环境的包和生产环境是一致的。
- 将 POM 分成多个层次的继承关系,比如携程的 POM 继承树一般是这样:
corp pom
ctrip pom/qunar pom
bu pom
product pom
project parent pom
project sub module pom
这样做的好处是每一层都可以定义这一级别的依赖。 其中 ctrip pom/qunar pom 我们叫它为公司的 super-pom,每个项目必须直接或间接的继承其所在公司的 super-pom。这样做的好处是每一层都可以定义这一级别的依赖,便于各个层次的统一管理。
为什么我本地可以编译通过,而你们编译系统编译通不过?”难道 Maven 在工作的时候还看脸? 当然不是!
遇到这样的情况不要急,处理起来通常有如下“三板斧”:
1、确认开发操作系统,Java 版本,Maven 版本。通常情况下操作系统对 Java 编译的影响是最小的,但是偶尔也会遇到一些比如分隔符(冒号与分号)之类的问题。Java 和 Maven 的版本应尽量与生产编译系统保持一致,以减少不必要的麻烦。 2、如果确认了开发操作系统没问题,那么你可以把用户的项目拉到自己的本地,并且删除本地依赖的缓存,也就是删除 .m2 目录下的子目录,减少干扰,执行编译。若编译通不过,说明用户本地就有问题,让他也删掉自己本地的缓存找问题。如果可以编译通过,说明问题出在编译系统,进入第 3 步。 3、使用 mvn dependency 命令对比生产编译系统与本地依赖树的区别,检查编译系统本地是否被缓存了错误的 jar 包,从而导致了编译失败。有时候这种错误会隐藏得比较深,非常难查,需要很大的耐心。
2.3 代码回滚¶
- 包回滚是指,线上运行的系统,从现在的版本回滚到以前稳定的老版本。
- 代码回滚是指,Git 分支的指针(游标),从指向当前有问题的版本改为指向一个该分支历史树上没问题的版本,而这个版本可以是曾经的 commit,也可以是新建的 commit。
2.3.1 你是不是也遇到了问题?¶
-
我本地的分支通过
reset --hard的方式做了代码回滚,想通过 push 的方式让远端的分支也一起回滚,执行 push 命令时却报错,该怎么办?- 答:如果不加
-f参数,执行reset --hard后,push 会被拒绝,因为你当前分支的最新提交落后于其对应的远程分支。push 时加上-f参数代表强制覆盖。 -
线上产品包已经回滚到昨天的版本了,我清清楚楚地记得昨天我把发布分支上的代码也
reset --hard到对应的commit了,怎么那几个有问题的 commit 今天又带到发布分支上了?真是要命 -
答:集成分支不能用
reset --hard做回滚,应该采用集成分支上新增 commit 的方式达到回滚的目的。 -
我刚刚在 GitLab 上接纳了一个合并请求(Merge Request),变更已经合入到 master 上了,但现在我发现这个合并出来的 commit 有较大的质量问题,我必须把 master 回滚到合并之前,我该怎么办?
-
答:可以在 GitLab 上找到那个合并请求,点击 revert 按钮。
-
刚刚线上 A 产品 V6.2 的包有问题,我已经把 A 的产品包回退到 V6.1 版本了,请问发布分支上的代码也要回滚到 V6.1 对应的 commit 吗?
-
答:你可以在下文“哪些情况下需要回滚代码?”和“哪些情况下包的回滚无需回滚代码?”中找到答案。
-
产品包的回滚可以在我们公司持续交付云平台上执行,平台能不能也提供代码一键回滚的功能?这样我们回滚代码能相对轻松一些。
-
答:针对已上线发布的版本,我认为持续交付平台提供一键回滚的方式还是有必要的。这么做可以规范集成分支上线后代码回滚的行为,也能减少人为失误。
- 答:如果不加
2.3.2 哪些情况下需要回滚代码?¶
在代码集成前和集成后,都有可能需要回滚代码。
第一种情况:开发人员独立使用的分支上,如果最近产生的 commit 都没有价值,应该废弃掉,此时就需要把代码回滚到以前的版本。 如图 1 所示。

图 1 个人分支回滚
第二种情况:代码集成到团队的集成分支且尚未发布,但在后续测试中发现这部分代码有问题,且一时半会儿解决不掉,为了不把问题传递给下次的集成,此时就需要把有问题的代码从集成分支中回滚掉。 如图 2 所示。

图 2 集成分支上线前回滚
第三种情况:代码已经发布到线上,线上包回滚后发现是新上线的代码引起的问题,且需要一段时间修复,此时又有其他功能需要上线,那么主干分支必须把代码回滚到产品包 V0529 对应的 commit。 如图 3 所示。

图 3 集成分支上线后回滚
2.3.3 哪些情况下包的回滚无需回滚代码?¶
- 线上回滚后,查出并不是因为源代码有问题。
- 下次线上发布,就是用来修复刚才线上运行的问题。
2.3.4 代码回滚必须遵循的原则¶
集成分支上的代码回滚坚决不用 reset --hard 的方式,原因如下:
- 集成分支上的 commit 都是项目阶段性的成果,即使最近的发布不需要某些 commit 的功能,但仍然需要保留这些 commit ,以备后续之需。
- 开发人员会基于集成分支上的 commit 拉取新分支,如果集成分支采用 reset 的方式清除了该 commit ,下次开发人员把新分支合并回集成分支时,又会把被清除的 commit 申请合入,很可能导致不需要的功能再次被引入到集成分支。
2.3.5 三种典型回滚场景及回滚策略¶

第一,个人分支回滚
- feature-x 分支回滚前 HEAD 指针指向 C6 。
- 在个人工作机上,执行下面的命令
$ git checkout feature-x
$ git reset --hard C3 的 HASH 值
如果 feature-x 已经 push 到远端代码平台了,则远端分支也需要回滚:
$ git push -f origin feature-x
第二,集成分支上线前回滚
- 假定走特性分支开发模式,上面的 commit 都是特性分支通过 merge request 合入 master 产生的 commit。
- 集成后,测试环境中发现 C4 和 C6 的功能有问题,不能上线,需马上回滚代码,以便 C5 的功能上线。
- 团队成员可以在 GitLab 上找到 C4 和 C6 合入 master 的合并请求,然后点击 revert 。

回滚后 master 分支变成如所示,C4’是 revert C4 产生的 commit,C6’是 revert C6 产生的 commit。通过 revert 操作,C4 和 C6 变更的内容在 master 分支上就被清除掉了,而 C5 变更的内容还保留在 master 分支上。

第三,集成分支上线后回滚
- C3 打包并上线,生成线上的版本 V0529,运行正确。之后 C6 也打包并上线,生成线上版本 V0530,运行一段时间后发现有问题。C4 和 C5 并没有单独打包上线,所以没有对应的线上版本。
- 项目组把产品包从 V0530 回滚到 V0529,经过定位,V0530 的代码有问题,但短时间不能修复,于是,项目组决定回滚代码。
- C4 和 C5 没有单独上过线,因此从线上包的角度看,不能回滚到 C4 或 C5,应该回滚到 C3。
- 考虑到线上包可以回滚到曾发布过的任意一个正确的版本。为了适应线上包的这个特点,线上包回滚触发的代码回滚我们决定不用 一个个 revert C4、C5 和 C6 的方式,而是直接创建一个新的 commit,它的内容等于 C3 的内容。
- 具体回滚步骤:
$ git fetch origin
$ git checkout master
$ git reset --hard V0529 # 把本地的 master 分支的指针回退到 V0529,此时暂存区 (index) 里就指向 V0529 里的内容了。
$ git reset --soft origin/master # --soft 使得本地的 master 分支的指针重新回到 V05javascript:;30,而暂存区 (index) 变成 V0529 的内容。
$ git commit -m "rollback to V0529" # 把暂存区里的内容提交,这样一来新生成的 commit 的内容和 V0529 相同。
$ git push origin master # 远端的 master 也被回滚。

回滚后的示意图
C3’的内容等于 C3,master 分支已清除 C4、C5 和 C6 的变更。
现在 master 又回到了正确的状态,其他功能可以继续上线。
如果要修复 C4、C5 和 C6 的问题,可以在开发分支上先 revert 掉 C3’ ,这样被清除的几个 commit 的内容又恢复了。
三、L3环境管理¶
3.1 测试环境要多少¶
3.1.1 互联网公司测试环境的结构¶
目前互联网公司常见的环境模型一般分为开发环境,功能测试环境,验收测试环境,预发布环境,生产环境这五个大套环境。
- 第一,开发环境
微服务架构下,单机已经无法完整地运行业务应用,这就需要开发环境内包含一套完整的业务应用依赖以及相关的基础设施,以保证业务开发同学能在本地完成开发测试。
- 第二,功能测试环境
在开发环境下,每个下游依赖应用都只有一个可用的 stable 版本。而在实际的开发过程中,由于项目的并行开发,往往会同时存在多个可依赖的版本。而每个项目组的同学在测试时,都希望测试过程中的关键依赖应用是可以被独占的,版本是固定的,不会被其他项目组干扰。
所以,一套独立的功能测试环境就很有必要了。通常,互联网企业会通过中间件的方式分割出一块隔离区域,在功能测试环境中创建多个子环境来解决这个问题。
- 第三,验收测试环境
验收测试环境和功能测试环境是完全隔离的。当功能测试通过后,你可以在验收测试环境进行最终的验收。
它除了可以用作测试之外,还可以用作产品展示。所以,除了测试和开发人员,产品经理也是验收测试环境的主要使用者。
- 第四,预发布环境
到了预发布阶段,应用已经进入了生产网络,和真实的生产应用共享同一套数据库等基础设施。
预发布是正式发布前的最后一次测试,在这个环境中往往可以发现线下环境中发现不了的 Bug。这个环境的运维标准等同于生产环境,一般不允许开发人员直接登录机器。
根据不同的业务需求和部署策略,不同公司对预发布环境的实现也有所不同:
一种比较常见的方式是,将金丝雀发布作为预发布,从接入真实流量的集群中挑选一台或一小组机器先进行版本更新,通过手工测试以及自动化测试和监控系统验证,降低新版本发布的风险。 另一种做法是,独立出一组始终不接入真实流量的机器,调用在预发布环境中形成闭环。 相对于第一种方式,第二种方式对生产环境的影响更小,但需要额外的资源和维护成本。
- 第五,生产环境
生产环境是用户真实使用的环境,对安全性和稳定性的要求最高。
3.1.2 什么是好的测试环境?¶
而每种角色对于产品研发流程中的需求也是不同的:
- 开发同学关注研发效率;
- 测试同学关注测试的可靠性;
- 产品经理更关注的是真实的用户体验和产品的完整性;
- 预发布环境的需求其实来自于运维同学,他们需要保证生产环境的稳定性,减少生产环境的变更,所以需要将预发布环境与线下环境完全隔离。
以下几个需求都是必须被做到的
- 可得性,即在开发一个新项目时,能快速获取构建一个环境需要的机器,基础设施。最好的情况是,能随时可得,随时归还。
- 快速部署,即在搭建新环境时,能以最快的速度构建出一整套完整的环境。测试环境的部署很频繁,在代码提交后,能在很短的时间内构建代码,在环境上更新,就能更早开始测试。
- 独立性,即一个环境在使用过程中,可以不受其他项目测试人员的干扰。
- 稳定性,即不会因为下游服务,基础设施的异常,造成测试中断、等待。
- 高仿真,主要分为两个方面:“测试数据真实”,即能在测试环境构建出真实的测试用例;“环境真实”,即基础服务的架构和行为与线上环境保持一致,避免因为环境不一致造成测试结果不一致。
3.2 各环境的重要性¶
从面向的目标来看,环境配置大体上可以分为两大部分:
- 以环境中每台服务器为对象的运行时配置;
- 以一个环境为整体目标的独立环境配置。
3.2.1 服务器运行时配置¶
以一个 Java Web 应用为例,需要哪些运行时配置呢?
- 安装 war 包运行依赖的基础环境,比如 JDK,Tomcat 等。
- 修改 Tomcat 的配置文件,关注点主要包括:应用的日志目录,日志的输出格式,war 包的存放位置。Tomcat 的 server.xml 配置包括:连接数、 端口、线程池等参数。
- 配置 Java 参数,包括 JVM 堆内存的 xmx、xmn 等参数,GC 方式、参数,JMX 监控开启等。
- 考虑操作系统参数,比较常见的一个配置是 Linux 的文件句柄数,如果应用对网络环境有一些特殊要求的话,还需要调整系统的 TCP 参数等配置。
我们不光要考虑单个实例初始化配置,还要考虑每次 JDK、Tomcat 等基础软件的版本升级引起的运行时配置的变更,而且这些变更都需要被清晰地记录下来,从而保证扩容出新的服务器时能取到正确的、最新的配置。
3.2.2 独立环境配置¶
独立环境配置的主要目的是,保证一个环境能够完整运作的同时,又保证足够的隔离性,使其成为一个内聚的整体。
所以,要让一个环境能够符合需求的正常运作,你需要考虑的内容包括:
- 这个环境所依赖的数据库该如何配置,缓存服务器又该如何配置。
- 如果是分布式系统,或者 SOA 架构的话,就需要考虑服务中心、配置中心等一系列中间件的配置问题。
3.2.3 环境一定要标准化¶
最好的简化方法,莫过于标准化了。所谓标准化,就是为了在一定范围内获得最佳秩序,对实际的或潜在的问题制定共同、可重复使用的规则
我们首先可以实现对语言栈的使用、运行时配置模板、独立环境配置的方法等的标准化:
- 规定公司的主流语言栈;
- 统一服务器安装镜像;
- 提供默认的运行时配置模板;
- 统一基础软件的版本,以及更新方式;
- 在架构层面统一解决环境路由问题;
- 自动化环境产生过程。
建议你在实施持续交付的同时,去推动形成以下几个方面的规范:
1、代码及依赖规范; 2、命名规范; 3、开发规范; 4、配置规范; 5、部署规范; 6、安全规范; 7、测试规范。
3.2.4 环境配置¶
首先,需要定义 Server Spec。
这是重中之重,在服务器生成时,写入它自己的描述文件。我们通常把这个文件命名为“Server Spec”。在这个文件里,记录了这台服务器的所有身份信息,包括:IDC,型号,归属环境,作用,所属应用,服务类型,访问路径等。
其次,解决配置中心寻址。
中间件根据 Server Spec 的描述,寻找到它所在环境对应的配置中心,从而进一步获取其他配置,如数据库连接字符串,短信服务地址等等。
最后,完成服务自发现。
其实这就是一个服务自发现的过程。根据服务类型,访问路径等,还可以自动生成对应的路由配置,负载均衡配置等。
3.3 了解各种配置方法¶
配置管理: 是通过技术或行政手段对软件产品及其开发过程和生命周期进行控制、规范的一系列措施。 它的目标是记录软件产品的演化过程,确保软件开发者在软件生命周期的各个阶段都能得到精确的产品配置信息。
配置: 是指独立于程序之外,但又对程序产生作用的可配变量。也就是说,同一份代码在不同的配置下,会产生不同的运行结果。
如果我们把这些信息都硬编码在代码里,结果就是:每次发布因为环境不同,或者业务逻辑的调整,都要修改代码。而代码一旦被修改,就需要完整的测试,那么变更的代价将是巨大的。
3.3.1 构建时配置¶
以 Maven 为例,实现多环境的构建可移植性需要使用 profile。
profile 是一组可选的配置,可以用来设置或者覆盖配置默认值。通过不同的环境激活不同的 profile,可以实现构建的可移植性。 我们可以看一个简单使用示例:

这段代码定义了 dev 和 prod 两个 profile(没有定义任何其他配置,实际使用中可按需定义任何配置),并且使用了 echo 插件验证 profile 是否生效,通过运行。
maven initialize –Pdev
maven initialize –Pprod
然后,可以看到输出:
[INFO] profiles.active = prod
其中, dev 是默认激活的,也就是说如果不填写任何 –P 参数,或者 –P 参数不为 dev 或者 prod,都会使用 dev 作为默认的 profile。
这样在代码构建时,你就可以根据具体需要选择对应的 profile 了。
这个方案看起来很简单, 但也有两个缺点:
- 它依赖于某个特定的构建工具,而且使用方法不统一。 什么意思呢?如果你不使用 Maven 作为构建工具,这个配置功能就失效了;而且对于跨平台、跨语言栈的支持也不友好。
- 每次都要重新编译,浪费计算资源。 即使你只是替换一些配置文件,并没有改动任何代码,但为了让配置生效,还是需要完成代码的整个构建过程,这就会在编译上花费大量的计算资源。
3.3.2 打包时配置¶
正因为构建时配置,需要针对多个 profile 编译多次,而持续交付有一个核心概念,即:一次构建多次部署。打包就是为了解决这个问题而被发明的。
打包时配置,需要借助发布的力量使配置生效。而实际场景中,只是修改了配置就要发布代码往往是不被接受的。特别是,如果你还不具备很成熟的持续部署能力,那将会是很头痛的事情。
3.3.3 运行时配置¶
随着程序功能的日益复杂,程序的配置日益增多,各种功能的开关,参数的配置,服务器的地址,等等不断增加到系统中。而且应用对程序配置的期望值也越来越高,需要配置系统能够:
- 修改后实时生效;
- 支持灰度发布;
- 能分环境、分集群管理配置;
- 有完善的权限、审核机制。
在这样的大环境下,传统的配置文件、数据库等方式已经越来越无法满足开发人员对配置的管理需求;另外,对于数据库连接串,各个服务之间的 API Key 等机密配置,如果放在代码里也会引起安全的问题。
3.3.4 回滚是配置永远的痛¶
先回滚配置还是先回滚代码就成了一个死循环的问题。最好的办法是保证配置与代码的兼容性,这有点类似于数据库的 schema 变更。 比如,只增加配置不删减配置、不改变配置的数据类型而是新增一个配置等方法。同时,也要做好代码版本与配置版本的对应管理
所以,对于配置回滚这个复杂问题,没有一劳永逸的办法, 只能根据实际情况选择最适合自己的方案。
我有一个推荐做法就是,每次回滚时,将可能发生变化的配置进行 diff 操作,由负责回滚的具体人根据结果去做最后的判断
三种配置方案:
- 构建时配置:会增加构建成本;
- 打包时配置:依赖发布生效;
- 运行时配置:配置中心,便于管理和维护。
3.4 如何做到分钟级搭建环境¶
3.4.1 环境构建流水线¶
- 虚拟机环境准备,根据环境的应用数、每个应用需要的硬件配置,准备好环境的硬件资源。
- 应用部署流水线,在标准化的虚拟机上进行应用部署,当出现问题时如何容错。
- 环境变更,在 SOA 或微服务的架构体系下,常常会因为测试的需求,将几套环境合并或拆分,创建环境时,你需要考虑如何高效地完成这些操作。
3.4.2 虚拟机环境准备¶
建议是,采用资源池的方案。你可以根据用户平时使用虚拟机的情况,统计每天虚拟机申请和销毁的具体数量,预先初始化一定量的虚拟机。 这样用户从上层的 PaaS 平台创建环境时,就不用等待初始化了,可以直接从资源池中获取虚拟机,这部分的时间就被节省下来了
3.4.3 应用部署流水线¶
由于不同公司的中间件和运维标准不同,部署流水线的差异也会很大,所以这里我只会从单应用部署标准化、应用部署的并行度,以及流水线的容错机制,这三个关键的角度,分享如何提速环境的搭建
- 单应用部署标准化,这是整个环境部署的基础
- 应用部署的并行度,为了提高环境的部署速度,需要尽可能得最大化应用部署的并行度
- 流水线的容错机制
- 第一种方法是,错误中断法。 创建环境过程中,各种资源申请、应用部署出现问题时,我们将工作流快照下来,然后收集所有的异常信息,返回给用户。由用户判断当前的情况,等用户确认问题已经得到解决后,可以触发一次快照重试,继续被中断的流程。
- 第二种方法是,优先完成法。 创建环境过程中发生错误时,先进行几次重试。
- 如果重试依然发生错误的话,就忽略当前错误,先走完剩余的流程,等所有的流程都走完了,再一次性将错误返回给用户。
- 从整体速度上来看,第二种优先完成的处理方式是更优的,而且也会更少地打断用户。
- 只是方式二需要保证的关键原则是:所有的部署脚本的操作都是幂等的,即两次操作达成的效果是一致的,并不会带来更多的问题。
3.4.4 环境变更¶
实现了应用部署流水线后,创建环境的主流程,即虚机准备和应用部署已经完成,环境已经可以工作了。但还是不能忽略了后续环境变更的需求和工作。一般情况下,研发人员变更环境主要有以下 4 种场景。
- 已经有一套新环境,当有新项目时,开发人员会挑选部分应用,组成一个独立的子环境。这里的重点是,要保证子环境和完整环境的调用是互相隔离的。
- 当存在多个子环境时,可能在某个时间点需要做多个项目的集成,这时开发人员需要合并多个环境。
- 和合并的情况相反,有些情况下,开发人员需要将一个子环境中的应用切分开来,分为两个或者多个环境分别进行隔离测试。
- 已经存在一个子环境,当多个并行项目时,开发人员会克隆一套完整的子环境做测试。
对于这 4 个场景,我们需要关注的是在多并行环境的情况下应用拓扑图,包括用户访问应用的入口、应用之间调用链的管理,以及应用对数据库之类的基础设施的访问。
- 用户访问应用的入口管理。 以最常用的访问入口(域名)为例,我推荐的做法是根据约定大于配置的原则,当环境管理平台识别到这是一个 Web 应用时,通过应用在生产环境中的域名、路由,环境名等参数,自动生产一个域名并在域名服务上注册。 这里需要注意的是,域名的维护尽量是在 SLB(负载均衡,Server Load Balancer)类似的软负载中间件上实现,而不要在 DNS 上实现。因为域名变更时,通过泛域名的指向,SLB 二次解析可以做到域名访问的实时切换。而如果配置在 DNS 上,域名的变更就无法做到瞬时生效了。
- 应用之间调用链的管理
- 对数据库的访问。
- 一是,数据库连接串的维护问题,与 SOA 调用链(即服务之间的调用关系)的维护类似,完全可以借鉴;
- 二是,数据库的快速创建策略。 对于数据库中的表结构和数据,我们采取的方式是根据生产中实际的数据库结构,产生一个基准库,由用户自己来维护这个基准库的数据,保证数据的有效性。并在环境创建时,提供数据库脚本变更的接口,根据之前的基准库创建一个新的实例,由此保证环境中的数据符合预期。
对于环境的创建和拆分,最主要的问题就是如何复制和重新配置环境中的各个零件。
- 环境创建,就是不断提高虚拟机准备和应用部署两个流水线的速度和稳定性;
- 环境拆分,则需要关注以上所说的三个最重要的配置内容。
而环境的合并需要注意的问题是,合并后的环境冲突。 比如,两套环境中都存在同一个服务应用,而两者的版本是不一致的;又或者,两个环境各自配置了一套数据库。此时该如何处理呢。
因为环境的描述已经被代码化了,所以我们解决这些问题的方式类似于解决代码合并的冲突问题。在环境合并前,先进行一次环境的冲突检测,如果环境中存在不可自动解决的冲突,就将这些冲突罗列出来,由用户选择合适的服务版本。
3.5 容器技术之环境管理¶
3.5.1 容器和虚拟机的主要差异,包括三个方面¶
- 首先,多个容器可以共享同一个宿主机的内核,所以容器的体积要比虚拟机小很多,这就使得容器在分发和存储上比较有优势;
- 其次,启动容器不需要启动整个操作系统,所以容器部署和启动速度更快、开销更小,也更容易迁移,这使得容器拥有更强的恢复能力;
- 最后,容器连带代码和环境一起部署的方式,保证了它所包含的程序的运行依赖不会被变更,这就使得容器有效解决了不同环境不同结果的问题。
3.5.2 重新定义交付标准¶
没有容器之前,交付标准包括软件环境(也就所谓的机器)和软件代码两部分。交付系统更关注的是软件代码,环境一旦产生后,我们就不再关心或者很难再干预用户后期是如何对其做变更的了。
容器技术统一了软件环境和软件代码,交付产物中既包括了软件环境,又包括了软件代码。也就是说,容器帮我们重新定义了交付标准。
第一,交付结果一致
容器镜像可以把软件的运行环境以及代码打包在一起,因此可以基于同一个镜像,在不同的地方生成一模一样的运行环境,也就是说单个镜像的交付结果不可变。
当然,单个容器只能提供一个服务,而实际场景下,应用都是跑在 SOA 或微服务的框架下的。所以,还需要利用如 Mesos 或 Kubernetes 这样的编排系统,将多个容器组织起来,并固化编排过程。
基于这两个特性,一旦形成了固定的容器镜像和对应的编排(也成为应用模板),那在不同的环境下,一定可以重复部署,且部署结果保持一致。
第二,交付自动化
容器镜像及容器编排技术很好地解决了 CI 和 CD 问题:
- CI 方面,与传统方式的不同只在于,原先交付的是安装包或软件包,而容器交付的则是镜像;
- CD 方面,与传统方式相比则有了长足的进步。 对传统方式而言,部署和安装方式与软件类型、开发方式有直接关系,存在多种多样的可能。 而容器技术则没有这样的问题,唯一的方式就是拉起容器镜像。这就大大简化了部署的复杂度,而且在编排系统的支持下,完成 CD 越来越容易了。
第三,交付个性化
有了容器之后,我们可以使用统一的接口完成任何应用的部署,几乎可以很好地满足所有的个性化需求
第四,交付版本控制
对于容器来说,遵循的是不可变基础设施(Immutable Infrastructure)的理念,也就是说任何变化,包括代码、环境、配置的变更,都需要重新制作镜像,产生一个新的版本。这与版本往往只和代码变更有关的传统方式有所不同。