持续交付:发布可靠软件的系统方法

第1章 软件交付的问题

常见的发布反模式

手工部署软件

  1. 有一份非常详尽的文档,该文档描述了执行步骤及每个步骤中易出错的地方。
  2. 以手工测试来确认该应用程序是否运行正确。
  3. 在发布当天开发团队频繁地接到电话,客户要求解释部署为何会出错。
  4. 在发布时,常常会修正一些在发布过程中发现的问题。
  5. 如果是集群环境部署,常常发现在集群中各环境的配置都不相同,比如应用服务器的连接池设置不同或文件系统有不同的目录结构等。
    6.发布过程需要较长的时间(超过几分钟)。
  6. 发布结果不可预测,常常不得不回滚或遇到不可预见的问题。
  7. 发布之后凌晨两点还睡眼惺忪地坐在显示器前,绞尽脑汁想着怎么让刚刚部署的应用程序能够正常工作。

开发完成之后才向类生产环境部署

由于部署工作中的很多步骤根本没有在试运行环境上测试过,所以常常遇到问题。比如,文档中漏掉了一些重要的步骤,文档和脚本对目标环境的版本或配置作出错误的假设,从而使部署失败。部署团队必须猜测开发团队的意图。

生产环境的手工配置管理

很多组织通过专门的运维团队来管理生产环境的配置。如果需要修改一些东西,比如修改数据库的连接配置或者增加应用服务器线程池中的线程数,就由这个团队登录到生产服务器上进行手工修改。

  1. 多次部署到试运行环境都非常成功,但当部署到生产环境时就失败。

  2. 集群中各节点的行为有所不同。例如,与其他节点相比,某个节点所承担的负载少一些,或者处理请求的时间花得多一些。

  3. 运维团队需要较长时间为每次发布准备环境。

  4. 系统无法回滚到之前部署的某个配置,这些配置包括操作系统、应用服务器、关系型数据库管理系统、Web服务器或其他基础设施设置。

  5. 不知道从什么时候起,集群中的某些服务器所用的操作系统、第三方基础设施、依赖库的版本或补丁级别就不同了。

6.直接修改生产环境上的配置来改变系统配置。

要有统一的配置管理,其责任之一就是让你能够重复地创建那些你开发的应用程序所依赖的每个基础设施。这意味着操作系统、补丁级别、操作系统配置、应用程序所依赖的其他软件及其配置、基础设施的配置等都应该处于受控状态。你应该具有重建生产环境的能力,最好是能通过自动化的方式重建生产环境。

如何实现目标

  1. 自动化。如果构建、部署、测试和发布流程不是自动化的,那它就是不可重复的。由于软件本身、系统配置、环境以及发布过程的不同,每次做完这些活动以后,其结果可能都会有所不同。
    
  2. 频繁做。如果能够做到频繁发布,每个发布版本之间的差异会很小。这会大大减少与发布相关的风险,且更容易回滚。

反馈流程

  1. 创建可执行代码的流程必须是能奏效的。这用于验证源代码是否符合语法。
    q 2. 软件的单元测试必须是成功的。这可以检查应用程序的行为是否与期望相同。
    q 3. 软件应该满足一定的质量标准,比如测试覆盖率以及其他与技术相关的度量项。
    q 4. 软件的功能验收测试必须是成功的。这可以检查应用是否满足业务验收条件,交付了所期望的业务价值。
    q 5. 软件的非功能测试必须是成功的。这可以检查应用程序是否满足用户对性能、有效性、安全性等方面的要求。
    q 6. 软件必须通过了探索性测试,并给客户以及部分用户做过演示。这些通常在一个手工测试环境上完成。此时,产品负责人可能认为软件功能还有缺失,我们自己也可能发现需要修复的缺陷,还要为其写自动化测试来避免回归测试

快速反应

接受到反馈的信息之后需要快速做出反应。

第2章 配置管理

引言

假如项目中有良好的配置管理策略,那么你对下列所有问题的回答都应该是“ YES”。
q 1. 你能否完全再现你所需要的任何环境(这里的环境包括操作系统的版本及其补丁级别、网络配置、软件组合,以及部署在其上的软件应用及其配置)?
q 2. 你能很轻松地对上述内容进行增量式修改,并将修改部署到任意一种或所有环境中吗?
q 3. 你能否很容易地看到已被部署到某个具体环境中的某次修改,并能追溯到修改源,知道是谁做的修改,什么时候做的修改吗?
你能满足所有必须遵守的规程章则吗?
q 4. 是否每个团队成员都能很容易地得到他们所需要的信息,并进行必要的修改呢?这个配置管理策略是否会妨碍高效交付,导致周期时间增加,反馈减少呢?

终极配置

就像一个平衡游标,一端是只有单一用途的软件,而且工作得很好,但很难或根本无法改变它的行为。然而另一端则是编程语言,你可以用它编写游戏、应用服务器或股票管理系统,这就是灵活性!显然,大多数软件都在两点之间,而不是这两端点中的任何一个。

根据需求,要为用户提供的软件配置能力越强,你能置于系统配置的约束就应越少,而你的编程环境也会变得越复杂。

“修改配置信息的风险要比修改代码的风险低”这句话就是个错觉。就拿“停止一个正在运行的应用系统”这个需求来说,通过修改代码或修改配置都很容易办到。如果使用修改源代码的方式,可以有多种方式来保证质量,比如编译器会帮我们查语法错误,自动化测试可以拦截很多其他方面的错误。然而,大多数配置信息是没有格式检查,且未经测试的。

在构建高度可配置的软件的道路上有很多陷阱,而最糟糕的可能莫过于下面这些。

  1. 经常导致分析瘫痪,即问题看上去很严重,而且很棘手,以至于团队花费很多时间思考如何解决它,但最终还是无法解决。
  2. 系统配置工作变得非常复杂,以至于抵消了其在灵活性上带来的好处。更有甚者,可能在配置灵活性上花费的成本与定制开发的成本相当。

配置并非天生邪恶,但需要采取谨慎的态度来一致地管理它们。现代计算机语言已经采用各种各样的特性和技术来帮助减少错误。在大多数情况下,配置信息却无法使用它们,甚至这些配置的正确性在测试环境和生产环境中也根本无法得到验证。

软件配置管理

管理配置信息的原则

当创建应用程序的配置信息时,应该考虑以下几个方面。
q 1. 在应用程序的生命周期中,我们应该在什么时候注入哪类配置信息。是在打包的时候,还是在部署或安装的时候?是在软件启动时,还是在运行时?要与系统运维和支持团队一同讨论,看看他们有什么样的需求。(ps:这个一般是项目最初始创建的时候已经确定,按照目前的结构,很大概率是有配置中心,或者不同环境使用不同的配置文件)
q 2. 将应用程序的配置项与源代码保存在同一个存储库中,但要把配置项的值保存在别处。另外,配置设置与代码的生命周期完全不同,而像用户密码这类的敏感信息就不应该放到版本控制库中。(ps:这个根据实际情况吧。要是没有配置中心,估计是做不到的吧。)
q 3. 应该总是通过自动化的过程将配置项从保存配置信息的存储库中取出并设置好,这样就能很容易地掌握不同环境中的配置信息了。(ps:配置项通过自动化搞?这就有点不是很懂是干啥了)
q 4. 配置系统应该能依据应用、应用软件的版本、将要部署的环境,为打包、安装以及部署脚本提供不同的配置值。每个人都应该能够非常容易地看到当前软件的某个特定版本部署到各种环境上的具体配置信息。

  1. 对每个配置项都应用明确的命名习惯,避免使用晦涩难懂的名称,使其他人不需要说明手册就能明白这些配置项的含义。
    q 6. 确保配置信息是模块化且封闭的,使得对某处配置项的修改不会影响到那些与其无关的配置项。

  2. DRY( Don’t Repeat Yourself )原则。定义好配置中的每个元素,使每个配置元素在整个系统中都是唯一的,其含义绝不与其他元素重叠。

  3. 最少化,即配置信息应尽可能简单且集中。除非有要求或必须使用,否则不要新增配置项。

  4. 避免对配置信息的过分设计,应尽可能简单。

  5. 确保测试已覆盖到部署或安装时的配置操作。检查应用程序所依赖的其他服务是否有效,使用冒烟测试来诊断依赖于配置项的相关功能是否都能正常工作。

环境管理

环境管理的关键在于通过一个全自动过程来创建环境,使创建全新的环境总是要比修复已受损的旧环境容易得多。(ps:感觉docker可以很简单实现这个场景)

变更管理过程

对环境的变更过程进行管理是必要的。应该严格控制生产环境,未经组织内部正式的变更管理过程,任何人不得对其进行修改。这么做的原因很简单:即便很微小的变化也可能把环境破坏掉。任何变更在上线之前都必须经过测试,因而要将其编成脚本,放在版本控制系统中。

第3章 持续集成

持续集成要求每当有人提交代码时,就对整个应用进行构建,并对其执行全面的自动化测试集合。而且至关重要的是,假如构建或测试过程失败,开发团队就要停下手中的工作,立即修复它。持续集成的目标是让正在开发的软件一直处于可工作状态。

准备工作

版本控制

与项目相关的所有内容都必须提交到一个版本控制库中,包括产品代码、测试代码、数据库脚本、构建与部署脚本,以及所有用于创建、安装、运行和测试该应用程序的东西。

自动化构建

人和计算机都能通过命令行自动执行应用的构建、测试以及部署过程。(ps:以来maven就可以实现)

团队共识

持续集成不是一种工具,而是一种实践。它需要开发团队能够给予一定的投入并遵守一些准则,需要每个人都能以小步增量的方式频繁地将修改后的代码提交到主干上,并一致认同“修复破坏应用程序的任意修改是最高优先级的任务”。如果大家不能接受这样的准则,则根本无法如预期般通过持续集成提高质量。

一个基本的持续集成系统

一旦准备好要提交最新修改代码时,请遵循如下步骤。
(1) 查看一下是否有构建正在运行。如果有的话,你要等它运行完。如果它失败了,你要与团队中的其他人一起将其修复,然后再提交自己的代码。
(2) 一旦构建完成且测试全部通过,就从版本控制库中将该版本的代码更新到自己的开发环境上。
(3) 在自己的开发机上执行构建脚本,运行测试,以确保在你机器上的所有代码都工作正常。当然你也可以利用持续集成工具中的个人构建功能来完成这一步骤。
(4) 如果本地构建成功,就将你的代码提交到版本控制库中。
(5) 然后等待包含你的这次提交的构建结果。
(6) 如果这次构建失败了,就停下手中做的事,在自己的开发机上立即修复这个问题,然后再转到步骤(3)。
(7) 如果这次构建成功,你可以小小地庆祝一下,并开始下一项任务。如果团队中的每个人在每次提交代码时都能够遵循这些简单的步骤,你就可以很有把握地说:“只要是在与持续集成一模一样的环境上,我的软件就可以工作。”

持续集成的前提条件

频繁提交

对于持续集成来说,我们最重要的工作就是频繁提交代码到版本控制库。每天至少应该提交几次代码。

定期地将代码提交到代码主干上会给我们带来很多其他好处。比如,它使每次的修改都比较小,所以很少会使构建失败。当你做了错事或者走错了路线时,可以轻松地回滚到某个已知的正确版本上。它使你的重构更有规则,使每次重构都是小步修改,从而保证可预期的行为。它有助于保证那些涉及多个文件的修改尽量不会影响其他人的工作。

ps:对于修订简单bug这种可能比较简单,但是对于一个功能开发,往往都是等功能开发完成,至少自己测试完成才会合并到分支上的。频繁提交的前提可能也是方案要确定才行吧,要是方案都没有确定,修订往往是反反复复的。所以这个也是根据实际的情况来搞的。

创建全面的自动化测试套件

单元测试用于单独测试应用程序中某些小单元的行为(比如一个方法、一个函数,或一小组方法或函数之间的交互)。它们通常不需要启动整个应用程序就可以执行,而且也不需要连接数据库(如果应用程序需要数据库的话)、文件系统或网络。它们也不需要将应用程序部署到类生产环境中运行。单元测试应该运行得非常快,即使对于一个大型应用来说,整个单元测试套件也应该在十分钟之内完成。(ps:使用jmokit就可以实现本功能吧。)

组件测试用于测试应用程序中几个组件的行为。与单元测试一样,它通常不必启动整个应用程序,但有可能需要连接数据库、访问文件系统或其他外部系统或接口(这些可以使用“桩”,即stub技术)。组件测试的运行时间通常较长。(ps:直接使用spring自带的测试组件似乎能够实现,但是这种往往又要以来其他组件的代码也是最新的,反而没有单元测试那么可控。)

验收测试的目的是验证应用程序是否满足业务需求所定义的验收条件,包括应用程序提供的功能,以及其他特定需求,比如容量、有效性、安全性等。验收测试最好采用将整个应用程序运行于类生产环境的运作方式。当然,验收测试的运行时间也较长。一个验收测试套件连续运行一整天是很平常的事儿。(ps:这想要一天搞定恐怕很难吧。得要有专门的自动化测试系统,性能、安全性这些也需要专门的处理)

保持较短的构建和测试过程

如果代码构建和单元测试的执行需要花很长时间的话,你会遇到一些麻烦,如下所示。

  1. 大家在提交代码之前不愿意在本地环境进行全量构建和运行测试,导致构建失败的几率越来越大。

  2. 持续集成过程需要花太长时间,从而导致再次运行构建时,该构建会包含很多次提交,所以很难确定到底是哪次提交破坏了本次构建。

  3. 大家提交的频率会变少,因为每运行一次构建和测试,都要坐在那儿等上一阵子。

理想情况下,提交前的预编译和测试过程,以及持续集成服务器上的编译和测试过程应该都能在几分钟内结束。我们认为,十分钟是一个极限了,最好是在五分钟以内,九十秒内完成是最理想的。

ps:确实构建的时间非常重要,过长的时间会影响大家持续构建的积极性

管理开发工作区

当开发人员刚开始新任务时,应该总是从一个已知正确的状态开始。他们应该能够运行构建、执行自动化测试,以及在其可控的环境上部署其开发的应用程序。

ps:保证新的开发人员介入,不需要额外的人工或者特殊的配置,将对应代码下载下来即可正常工作。并且新的开发人员也可以在本地进行编译、构建、单元测试。

使用持续集成软件

基本操作

本质上,持续集成软件包括两个部分。第一部分是一个一直运行的进程,它每隔一定的时间就执行一个简单的工作流程。第二部分就是提供展现这个流程运行结果的视图,通知你构建和测试成功与否,让你可以找到测试报告,拿到生成的安装文件等。

必不可少的实践

持续集成系统的目标是,确保软件在任何时候都可以工作。为了做到这一点,下面是我们在自己的团队中使用的一些实践。

构建失败之后不要提交新代码

第一准则谁的构建失败,谁处理,并且得要优先处理。

ps:往往需要增加人工的审核步骤,不然按照人性,大家都是偷工减料,不愿意处理的。比如构建失败之后,管理人员会立即收到对应的邮件,并且提醒本次构建失败的提交人员,让其修复。

提交前在本地运行所有的提交测试,或者让持续集成服务器完成此事

如果以前未听说或使用过这种方法,你可能会问:“为什么在提交前还要运行本地提交测试呢?这样的话,我们的编译和提交测试不是要运行两次了吗?”这么做,有两个理由。
(1) 如果在你根据版本控制进行更新之前,其他人已经向版本控制库中提交了新代码,那么你的变更与那些新代码合并后,可能会导致测试失败。如果你自己先在本地更新代码并运行提交测试的话,假如有问题,就会在本地提前发现,提前修复,从而不会令持续集成服务器上的构建失败,不至于影响其他人及时提交。
(2) 在提交时经常犯的错误是,忘记提交那些刚刚新增加的东西到存储库中。如果遵守这个流程的话,当本地构建成功,而持续集成系统中的提交阶段失败了的话,那么你就知道要么是由于别人与你同时提交了代码,要么就是你遗漏了一部分类或配置文件没有提交到版本控制系统中。

等提交测试通过后再继续工作

构建失败是持续集成过程中一个平常且预料之中的事情。我们的目标是尽快发现错误,并消灭它们。

ps:如果构建的时间足够短,那么是可以等待构建完成再进行下一步动作的。如果构建时间太长,则可以进行下一步工作,但是要关注构建完成之后的消息吧。

回家之前,构建必须处于成功状态

倘若在回家的时间节点发现构建失败了,就应该立即修复,而不是等回家,或者第二天来搞。当下修复的成本和影响肯定是最低的。

时刻准备着回滚到前一个版本

ps:使用版本控制工具还是有办法做到的,只是也有成本。如果单次提交功能过大,是没法子很好的回滚的。因而作者整本书其实都在强调小提交。

在回滚之前要规定一个修复时间

如果因某次提交而导致构建失败,必须在十分钟之内修复它。如果在十分钟内还没有找到解决方案的话,就将其回滚到版本控制系统中前一个好的版本。

ps:回滚的代价可能会更大,所以只能监督构建失败的小伙伴尽快修复了。

不要将失败的测试注释掉

将失败的测试注释掉应该是最后不得已的选择,除非你马上就去修改它,否则尽量不要这么做。偶尔注释掉一个测试是可以的,比如,当某个非常严重的问题需要解决,或者是某些内容需要与客户进一步探讨时。

ps:根据实际的情况处理,做不到一刀切。失败的单元测试,得要找到失败的原因,可能是需求已经不存在,或者需求变动,这样都可以直接删除,反之则要修订该单元测试。

为自己导致的问题负责

假如提交代码后,你写的测试都通过了,但其他人的测试失败了,构建结果还是会失败。发现构建失败就应该去解决。

若测试运行变慢,就让构建失败

ps:这个时间不好界定吧,服务器的波动,或者其他的东西,都可能导致有问题吧。

若有编译警告或代码风格问题,就让测试失败

编译器发出警告时,通常理由都足够充分。我们曾经用过一个比较成功的策略,即只要有编译警告,就让构建失败

ps:checkstyle属于是代码风格的统一校验,属于静态代码检查,还是属于比较好遵守的。对于开发的语法方法的校验,可能就得要使用PMD之类的。

分布式团队

使用场景比较少。

第4章测试策略的实现

引言

测试会建立我们的信心,使我们相信软件可按预期正常运行。

一个全面的自动化测试套件甚至可以提供最完整和最及时的应用软件说明文档,这个文档不仅是说明系统应该如何运行的需求规范,还能证明这个软件系统的确是按照需求来运行的。

测试的分类

业务导向且支持开发过程

自动化验收测试有很多很有价值的特性。

  1. 它加快了反馈速度,因为开发人员可以通过运行自动化测试,来确认是否完成了一个特定需求,而不用去问测试人员。

  2. 它减少了测试人员的工作负荷。

  3. 它让测试人员集中精力做探索性测试和高价值的活动,而不是被无聊的重复性工作所累。

  4. 这些验收测试也是一组回归测试套件。当开发大型应用或者在大规模团队中工作时,由于采用了框架或许多模块,对应用某一部分的更改很可能会影响其余特性,所以这一点尤其重要。

  5. 就像行为驱动开发( BDD)所建议的那样,使用人类可读的测试以及测试套件
    名,我们就可以从这些测试中自动生成需求说明文档。像Cucumber和Twist这样的工具,就是为让分析人员可以把需求写成可执行的测试脚本而设计的。这种方法的好处在于通过验收测试生成的需求文档从来都不会过时,因为每次构建都会自动生成它。

对于软件开发的各个方面,各个项目之间都会有所不同,你需要监控到底花了多长时间做重复性的手工测试,以便决定什么时候把它们自动化。一个很好的经验法则就是,一旦对同一个测试重复做过多次手工操作,并且你确信不会花太多时间来维护这个测试时,就要把它自动化。

技术导向且支持开发过程的测试

这些自动化测试单独由开发人员创建并维护。 有三种测试属于这一分类:单元测试、组件测试和部署测试。

业务导向且评价项目的测试

这类手工测试可以验证我们实际交付给用户的应用软件是否符合其期望。这并不只是验证应用是否满足需求规格说明,还验证需求规格说明的正确性。

技术导向且评价项目的测试

验收测试分为两类:功能测试和非功能测试。非功能测试是指除功能之外的系统其他方面的质量,比如容量、可用性、安全性等。正如我们之前提到的,功能测试与其依据是非功能需求测试不是面向业务的。这似乎是显而易见的,但是很多项目并不把非功能需求放在与功能需求同等重要的地位来对待,而且可能会更糟糕,他们根本不去验证这些非功能需求。虽然用户很少花时间提前对容量和安全性做要求,但一旦他们的信用卡信息被盗,或者网站由于容量问题总是停止运行,他们就会非常生气。

现实中的情况与应对策略

新项目

相对于项目开发几个迭代后再写验收测试来说,在项目开始就采用这样的流程是比较容易的。在项目开始一段时间以后再考虑这一问题时,你的代码框架很可能并不支持这种验收测试的书写,所以你不仅必须寻找一些方法实现这些验收测试,还要说服那些持怀疑态度的开发人员,让他们认真遵守这个流程。如果在项目一开始就做自动化测试,可能更容易让开发人员接受。

当然,必须让团队的每个人(包括客户和项目经理在内)都接受这种做法。我们曾看到过一些项目取消这种做法,因为客户觉得写自动化验收测试花费了太多的时间。假如客户真的愿意以牺牲自动化验收测试套件的质量为代价达到快速将软件推向市场的目标,那么,作出这样的决定也无可厚非。当然,其后果也应该非常明显啦。

项目进行中

引入自动化测试最好的方式是选择应用程序中那些最常见、最重要且高价值的用例为起点。这就需要与客户沟通,以便清楚地识别真正的业务价值是什么,然后使用测试来做回归,以防止功能被破坏。

ps:进行中的项目想要全面实现自动化测试时不现实的,所以可以选择优先级高的用例。

遗留系统

如果没有自动化构建流程,那么最高优先级的事儿就是创建一个,然后再创建更多的自动化功能测试来丰富它。如果有文档,或能够找到那些曾经或正工作在这个系统之上的成员的话,创建自动化测试套件会更容易一些。然而现实往往并非如此。

ps:遗留系统,要是修订量或者后续的开发量比较少,则维护的价值就很低了,一般公司是不会花费时间去处理这个的。

集成测试

假如你的应用程序需要通过一系列不同的协议与各种外部系统进行交互,或者它由很多松散耦合的模块组成,而模块之间还有很复杂的交互操作的话,集成测试就非常重要了。在组件测试和集成测试之间的分界线并不十分清晰(尤其当“集成测试”这个词被赋予了太多的意义)。我们所说的“集成测试”是指那些确保系统的每个独立部分都能够正确作用于其依赖的那些服务的测试。

通常来说,集成测试应该在两种上下文中运行:首先是被测试的应用程序使用其真正依赖的外部系统来运行时,或者是使用由外部服务供应商所提供的替代系统;其次是应用程序运行于你自己创建的一个测试用具( testharness)之上,而且这些测试用具也是代码库的一部分。

在理想情况下,服务提供商会提供一个复制版的测试服务,除了性能以外,它可以提供与真正的服务完全相同的行为。你可以在此之上进行测试。然而,在现实世界中,你常常需要开发一个测试用具。比如当:

  1. 外部系统还没有开发完成,但接口已经提前定义好了(此时你需要有心理准备,因为这些接口很可能会发生变化);
  2. 外部系统已经开发完了,但是还不能为了测试而部署它,或者用于测试目的的外部系统运行太慢,或缺陷太多,无法支持正常自动化测试的运行;
  3. 虽然有测试系统,但它的响应具有不确定性,从而导致无法对自动化测试结果进行验证(比如,某个股票市场的实时数据);
  4. 外部系统很难安装或者需要通过用户界面进行手工干预;
  5. 需要为涉及外部系统服务的功能写一份标准的自动化验收测试,而这些测试应该一直在测试替身上运行
  6. 自动化持续集成系统需要承担的工作量太大且其所需要的服务水平太高,远不是一个仅用于做手工探索性测试的轻量级测试环境所能承受或提供的。

ps:这边提到的测试用具,应该类似于mock服务吧。与第三方系统约定的借口已经完成,但是第三方还没有就绪的情况下,可以通过mock服务,模拟第三方服务的返回值。

测试用具不但应该能返回服务调用所期望的响应,而且还要能返回不可预期的响应。

ps:通过测试用具,应该是能够更加方便的模拟很多异常场景的请求的,从而使得代码更加的健壮。

ps:测试用具的编写本身也是一个有工作量的事情。如果有多个系统,则可能需要编写多个系统的借口功能。如果本系统要做到自动化测试,那测试用具估计也得要做到增量性开发吧。

流程

在用户故事的开发过程中,开发人员和测试人员的紧密合作是保证平稳发布的关键。无论开发人员什么时候完成一个功能,他们都应该把测试人员叫到身边,让他们检查一下。测试人员应该在开发人员的机器上做一下测试。此时,开发人员可以在附近的某个终端或笔记本电脑上继续工作,比如修改某些回归缺陷。这样他们仍旧在工作(因为测试可能需要花上一点儿时间),但测试人员随时能找他来讨论问题。

ps:作者应该是敏捷性的开发团队,作者的描述也是在功能完成之后测试立即就可以介入,但是一般的开发团队都是整个大功能完成之后才介入的。

带着一堆缺陷继续前进是有风险的。过去,很多开发团队和开发过程都对大量缺陷采取视而不见的不关注态度,总是希望以后能找个适当时机来修复它们。然而,几个月之后,他们就会看到堆积如山的缺陷,其中有些缺陷以后根本不会被修复,而有些因为功能需求的变化已不再是缺陷了。但是,还有一些缺陷对于某个用户是非常严重的,但它们却被淹没在缺陷海洋中了。

像对待功能特性一样来对待缺陷。毕竟,修复缺陷和开发新功能一样,都需要花时间和精力。因此,客户可以将某个缺陷与要开发的新功能进行对比,得出它们的相对优先级。比如,一个出现概率很小的缺陷,只会影响少量用户,而且还有一个已知的临时解决方案,那么修复它的重要性可能要低于那些可以为用户带来收入的新功能。至少,我们可以把缺陷分为严重( critical)、 阻塞( blocker)、 中( medium)和低( low)四个级别。要想找到更全面的评估方法,我们可能还要考虑缺陷发生的频率,对用户的影响是什么,以及是否有临时解决方案等。

ps:一般的团队估计是定义bug的等级会更加方便与合理吧。应该要有bug系统统一管理现在的bug。锐捷网络的bug等级更加细化,critical、blocker、normal、minor 、enhancement。normal级以上的bug就是需要重点关注要解决的。不过bug的等级得要是准确的标注才行,随意的标注等级,可能造成严重的bug被忽略了。可能要有一个bug评审的环境,避免严重bug被忽略。

第二部分 部署流水线

什么是部署流水线

部署流水线是指软件从版本控制库到用户手中这一过程的自动化表现形式。

ps:网龙的一键发布就是这样子的。点击发布就能够将服务部署上去,然后通过界面就可以访问与测试了。

  1. 提交阶段是从技术角度上断言整个系统是可以工作的。这个阶段会进行编译,运行一套自动化测试(主要是单元级别的测试),并进行代码分析。
  2. 自动化验收测试阶段是从功能和非功能角度上断言整个系统是可以工作的,即从系统行为上看,它满足用户的需要并且符合客户的需求规范。
  3. 手工测试阶段用于断言系统是可用的,满足了它的系统要求,试图发现那些自动化测试未能捕获的缺陷,并验证系统是否为用户提供了价值。这一阶段通常包括探索性测试、集成环境上的测试以及UAT( User Acceptance Testing,用户验收测试)。
  4. 发布阶段旨在将软件交付给用户,既可能是以套装软件的形式,也可能是直接将其部署到生产环境,或试运行环境(这里的试运行环境是指和生产环境相同的测试环境)。

部署流水线就是由上述这些阶段,以及为软件交付流程建模所需的其他阶段组成,有时候也称为持续集成流水线、构建流水线、部署生产线或现行构建( living build)。无论把它叫做什么,从根本上讲,它就是一个自动化的软件交付流程。这并不是说该发布过程不需要人的参与,而是说在执行过程中那些易出错且复杂的步骤被变成可靠且可重复的自动化步骤。事实上,人工参与的活动反而有增加的趋势,因为在开发流程中所有阶段均可进行一键式部署这一事实,会促使测试人员、分析人员、开发人员以及(最重要的)用户更频繁地执行它。

ps:网龙的一建部署类似于这个效果了。提交之后,先进行编译--》测试环境通过--》代码mr到master分支,预生产发布--》预生产发布测试人员介入测试,测试通过之后--》发布

部署流水线的相关实践

只生成一次二进制包

很多构建系统将版本控制库中的源代码作为多个步骤中最权威的源,不同上下文中会重复编译这个源,比如在提交时、做验收测试时或做容量测试时。而且,在每个不同的环境上部署时都要重新编译一次。但是,对于同一份源代码,每次都重新编译的话,会引入“编译结果不一致”的风险。

假如重新创建二进制包,就会存在这样的风险,即从第一次创建二进制包到最后发布这两个时间点之间会引入某种变化,比如在不同阶段里,编译时所用的软件工具链有差异,此时这个即将发布的二进制包就不是我们曾经测试过的那个二进制包了。出于审计的目的,确保从二进制包的创建到发布之间不会因失误或恶意攻击而引入任何变化是非常关键的。如果是解释性语言的话,有些组织甚至要求只有资深人员才有权在某个特定的环境里进行编译、组装或打包,其他人不得插手。所以一旦创建了二进制包,在需要时最好是重用,而不是重新创建它们。

ps:多个源相当于是多个分支吧。比如测试对应debug分支,开发对应development分支,预生产和生产对应master分支。想要保证所有的流程都使用一个二进制文件还是挺难得,首先至少是要做到配置文件与二进制人间解耦。网龙在流程上是做了一个折中,预生产和生产得要使用同一个二进制文件,凡是提交到master分支的mr都需要经过评审人员确认才行。

对不同环境使用同一部署方式

当然,每个环境多多少少都会有所不同,至少IP地址肯定是不一样的。通常还会有其他不同之处,比如操作系统和中间件的配置设置,数据库的安装位置和外部服务的位置,以及在部署时需要设置的其他配置信息。但这并不意味着你应该为每个环境都建立一个单独的部署脚本,而只要把那些与特定环境相关的特定配置分开放置就行了。一种方法是使用属性文件保存配置信息,比如分别为每个环境保存一个属性文件,并将其放在版本控制库中。

对部署进行冒烟测试

当做应用程序部署时,你应该用一个自动化脚本做一下冒烟测试,用来确保应用程序已经正常启动并运行了。这个测试应该非常简单,比如只要启动应用程序,检查一下,能看到主页面,并在主页面上能看到正确的内容就行了。这个冒烟测试还应该检查一下应用程序所依赖的服务是否都已经启动,并且正常运行了,比如数据库、消息总线或外部服务等。

ps:作者提到的冒烟测试属于最基础的了,实际上的冒烟测试可能还包括做一些最基础的操作。锐捷网络的云桌面最基础的模块就是镜像,有了镜像才能够通过它创建后续的一系列活动,因而冒烟的动作就要包括镜像的创建。

向生产环境的副本中部署

为了对系统上线充满信心,你要尽可能在与生产环境相似的环境中进行测试和持续集成。

ps:论预生产环境的重要性

每次构建都要立即在流水线中传递

很多项目都有一个各阶段的执行时间表,比如每小时构建一次,每天晚上运行一次验收测试,每个周末运行一次容量测试。部署流水线则使用了不同的方式:每次提交都要触发第一个阶段的执行,后续阶段在第一个阶段成功结束后,立即被触发。

ps:如果是定时构建,则可能会丢失某一部分的提交没有被正确构建。部署流水线方式则要求每次的提交都会触发构建。

只要有环节失败,就停止整个流水线

每次提交代码到版本控制系统中后,都能够构建成功并通过所有的测试。对于整个部署流水线来说,都适用这一要求。假如在某个环境上的某次部署失败了,整个团队就要对这次失败负责,应该停下手头的工作,把它修复后再做其他事情。

提交阶段

代码提交阶段,就应该做一些校验。比较有用的度量项包括:

  1. 测试覆盖率(如果提交测试只覆盖了代码库的5%,那么这些测试发挥不了太大的作用);
  2. 重复代码的数量;
  3. 圈复杂度( cyclomatic complexity);
  4. 输入耦合度( afferent coupling)和输出耦合度( efferent coupling);
  5. 编译警告的数量;
  6. 代码风格。

ps:提交阶段尽量做校验是合理的,因为能够具体到某个特定的人。

自动化验收测试之门

提交阶段往往只是进行了单元测试,只能发现本项目的一些问题,而无法依赖之间的问题,例如依赖的第三方服务没有启动成功。

手工测试

验收测试之后一定会有一些手工的探索性测试、易用性测试和演示。在这个过程中,测试人员所扮演的角色并不是回归测试该系统,而是首先通过手工证明验收条件已被满足,从而确保这些验收测试的确是验证了系统行为。

非功能测试

几乎每个系统都有容量和安全性方面的要求,或者必须遵守服务水平协议等。通常应该用某些自动化测试衡量应用程序是否满足这些需求。可以在部署流水线中创建一个阶段,用于运行这些自动化的非功能测试。

ps:非功能测试涵盖范围太广了,在部署流水线上不一定合适。比如性能的测试,就需要一个单独的环境,也有可能需要造数据等,并且可能整个过程需要的时间比较长,因而在部署流水线上有这个东西,可能就太浪费时间了。

变更的撤销

传统上,人们对新版本的发布常常存在着恐惧心理,原因有两个。一是害怕引入问题,因为手工的软件发布过程很可能引入难以发现的人为错误,或者部署手册本身就隐藏着某个错误。二是担心由于发布过程中的一个问题或新版本的某个缺陷,使你原来承诺的发布失败。无论是哪种情况,你的唯一希望就是足够聪明且非常迅速地解决这个问题。
我们可以通过每天练习发布多次来证明自动化部署系统是可以工作的,这样就可以缓解第一种问题。对于第二个问题,可以准备一个撤销策略。最糟的情况也就是回滚到发布之前的状态,这样你就有足够的时间评估刚发现的问题,并找到一个合理的解决方案。

ps:想要回滚是挺难的,尤其是大系统,在涉及到多个组件之间的协作,单单回滚一个系统是解决不了问题的。因为回滚往往是下下策。

第5章 实现一个部署流水线

无论是从零创建新项目,还是想为已有的系统创建一个自动化的流水线,通常都应该使用增量方法来实现部署流水线。接下来,我们将描述如何从无到有,建立一个完整流水线的策略。一般来说,步骤是这样的:

  • 对价值流建模,并创建一个可工作的简单框架;
  • 将构建和部署流程自动化;
  • 将单元测试和代码分析自动化;
  • 将验收测试自动化;
  • 将发布自动化。

对价值流进行建模并创建简单的可工作框架

可以在同一组织中找个与你的项目相似的项目,思考它的价值流,也可以从最简单的价值流开始,即第一个阶段是提交阶段,用来构建应用程序并运行基本的度量和单元测试,第二个阶段用来运行验收测试,第三个阶段用来向类生产环境部署应用,以便用它来做演示。

如果是使用“最简单模型”,每当有人提交代码到版本控制系统时,就应该触发提交阶段。当提交阶段通过以后,验收测试阶段就应该被自动触发,并使用提交阶段刚刚创建的二进制包。为手工测试或发布应用而向类生产环境部署二进制包的阶段,都应该会要求你具有通过单击按钮来选择到底部署哪个版本的能力,而这种能力通常都需要授权。

构建和部署自动化

实现部署流水线的第一步是将构建和部署流程自动化。构建过程的输入是源代码,输出结果是二进制包。

自动化单元测试和代码分析

开发部署流水线的下一步就是实现全面的提交阶段,也就是运行单元测试、进行代码分析,并对每次提交都运行那些挑选出来的验收测试和集成测试。

自动化验收测试

一套好的自动化验收测试会帮助你追查随机问题和难以重现的问题,如竞争条件、死锁,以及资源争夺。这些问题在应用发布之后,就很难再被发现。

ps:自动化测试是最费时费力的

部署流水线的演进

我们发现,每个价值流图和流水线中几乎都有上面描述的步骤。通常这些是自动化的第一个目标。随着项目越来越复杂,价值流图也会演进。另外,对于流水线来说,还有两个常见的外延:组件和分支。大型应用程序最好由多个组件拼装而成。在这样的项目中,每个组件都应该有一个对应的“迷你流水线”,然后再用一个流水线把所有组件拼装在一起,并运行整个验收测试集(包括自动化的非功能测试),然后再部署到测试环境、试运行环境和生产环境中。

ps:多个组件之间的交互,有点像锐捷的极光部署,但是每次构建都需要选择每个组件的版本,这也是需要各个组件之间信息需要统一才行。不然组件之间接口的变化,将会导致打包之后的版本是有问题的。感觉最好是组件之间要有约束,每次发布都是统一发布。都使用最新的版本。

第6章 构建与部署的脚本化

核心思想将操作流程脚本化,使用现成的一些工具,比如maven、ant等的构建工具。

小贴士

总是使用相对路径

构建中最常见的错误就是默认使用绝对路径。这会让构建流程和某台特定机器的配置形成强依赖,从而很难被用于配置和维护其他服务器。
应该默认所有位置都使用相对路径。这样,构建的每个实例都是一个完整的自包含结构,你提交到版本控制库的镜像就会自然而然地确保将所有内容放在正确的位置上,并以其应有的方式运行。

消除手工步骤

文档常常存在错误或过时,所以在生产环境中部署时,经常会有大量的演练成本。每次部署都各不相同,因为某个缺陷的修复或小的系统改动,可能只需对系统的个别部分进行重新部署。因此,对于每次发布来说,这个部署过程都必须修订一下。前面部署中留下来的知识和物件无法重用。对于部署执行人来说,每次部署都是对其记忆力以及对系统理解程度的考验,并且基本上都会出错。

那么,我们什么时候应该考虑将流程自动化呢?最简单的回答就是:“当你需要做第二次的时候。”到第三次时就应该采取行动,通过自动化过程来完成这件工作了。这种细粒度的渐进方法,可以迅速建立起一个系统,将开发、构建、测试和部署过程中的可重复部分实现自动化。

从二进制包到版本控制库的内建可追溯性

能够确定“某个二进制包是由版本控制库中的哪个具体版本生成的”是非常必要的。假如在生产环境中出了问题,能够轻松确定机器上每个组件的版本号,以及它们的来源,你的生活会轻松很多。

不要把二进制包作为构建的一部分放到版本控制库中

取而代之的是,我们可以把二进制包和结果报告放在一个共享的文件系统中存储。如果你把它们弄丢了或者需要重新生成它们的话,最好是从源代码中重新构建一份。假如你无法根据源代码重新构建出一份一模一样的副本,这说明你的配置管理没达到标准,需要加以改进。
一般的经验法则是不要将构建、测试和部署过程中生成的任何产物提交到版本控制库中,而要将这些产物作为元数据,与触发该次构建的版本的标识关联在一起。

ps:放到版本管理里边可能也不合适,版本控制的体积会无形中变大了,可能下载同步就变慢了。应该是有单独的文件系统,用于放置这些东西。

“test”不应该让构建失败

在某些构建系统中,一旦某个任务失败,便默认令本次构建立即失败。也就是说,假如你有一个“test”任务,如果在其运行时,任何测试失败了,整个构建就将立即失败。通常来说,这种做法是不好的。相反,应该将当前失败的任务记录下来,然后继续构建流程的后续部分。最后,在过程结束时,如果发现有任意一个任务失败了,就退出并返回一个失败码。

ps:如果能够实现这种效果,是挺好的,但是如果业务之间有以来关系,也就不好实现。能够实现这种效果,应该能够节省很多时间。

用集成冒烟测试来限制应用程序

比如,可以在部署之前令部署脚本先检查一下是否被部署在了正确的机器上。对于测试和生产环境配置来说,这尤其重要。

ps:应用部署完成之后,跑最基础的用例,保证程序是没有问题的。

第7章 提交阶段

版本控制--》提交阶段(编译、单元测试、组装打包、代码分析)--》制品库(结果报告二进制包元数据)

当某人向版本控制库的主干上提交了一次变更后,持续集成服务器会发现这次变更,并将代码签出,执行一系列的任务,包括:

  • 编译(如果需要的话),并在集成后的源代码上运行提交测试;
  • 创建能部署在所有环境中的二进制包(如果使用需要编译的语言,则包括编译和组装);
  • 执行必要的分析,检查代码库的健康状况;
  • 创建部署流水线的后续阶段需要使用的其他产物(比如数据库迁移或测试数据)

ps:每次提交都构建,需要jenkins构建足够快,不然也是很耗费时间的事情

提交阶段的原则和实践

提供快速有用的反馈

提交失败要立即有反馈,通知对应提交人解决

何时令提交阶段失败

当单元测试覆盖率低于60%就令提交阶段失败,但是如果它高于60%,低于80%的话,就令提交阶段成功通过,但显示成黄色
当某次构建的编译警告的数量比前一次增多或者没有减少时,就让提交阶段失败(这就是“渐进式”实践)
如果重复代码的数量超出了某个事先约定的限制,或者有关代码质量的其他度量项不符合约束条件时,就令提交阶段失败,这是完全可以接受的

精心对待提交阶段

提交阶段中有构建用的脚本和运行单元测试、静态分析等的脚本。这些脚本需要小心维护,就像对待应用程序的其他部分一样。和其他所有软件系统一样如果构建脚本设计得很差,还没得到很好维护的话,那么保持它能够正常工作所需投入的精力会呈指数级增长。这相当于双重打击。一个较差的构建系统不但会把昂贵的开发资源从创造业务功能的工作中拖走,而且会令那些仍在创建业务功能的开发人员的工作效率降低。

让开发也拥有权限

如果真的只有那些专家才有权维护持续集成系统的话,那就是一种失败的管理方式。交付团队对提交阶段(也包括流水线基础设施的其他部分)拥有所有权是至关重要的,这与交付团队的工作和生产效率是紧密联系在一起的。如果你设置了人为障碍,使开发人员不能快速有效地作出修改,就会减缓他们的工作进程,并在其前进的道路上埋下地雷。

ps:这个观点不知道可行性如何,所有开发都能够处理,就得要制定其他的策略才行。

在超大项目团队中制定一个构建负责人

让某个(或多个)人扮演构建负责人的角色是必要的。他们不但要监督和指导对构建的维护,而且还要鼓励和加强构建纪律。如果构建失败,构建负责人要知会当事人并礼貌地(如果时间太长的话,不礼貌也没问题)提醒他们为团队修复失败的构建,否则就将他们的修改回滚。

ps:锐捷有类似的实现,比如负责审核代码并合入的人员,在发现失败时要制定对应人员处理。

避免使用数据库

首先,这种测试运行得非常慢。当想重复测试,或者连续运行几次相似的测试时,这种有状态的测试就是个障碍。其次,基础设施准备工作的复杂性令这种测试方法的建立和管理更加复杂。最后,如果从测试中很难消除数据库依赖的话,这也暗示着,你的代码在通过分层进行复杂性隔离方面做得不好。

ps:什么场景下需要依赖数据库,这个倒是没有碰到

在单元测试中避免异步

异步实现的单元测试,比如发送消息,等待消息相应,这样的测试会依赖其他的组件,并需要等待返回值,单元测试应该通过mock或者桩的方式解决。

我们建议尽量消除提交阶段测试中的异步测试。依赖于基础设施(比如消息机制或是数据库)的测试可以算做组件测试,而不是单元测试。

使用测试替身

在这种设计得比较好的模块化系统中,为了测试一个在关系网中心的某个类,可能需要对它周边的很多类进行冗长的设置。解决办法就是与其依赖类进行模拟交互。

ps:java可以使用jmock这样的工具。

时间的伪装

对于定时器任务,得要构建当前时间注入方式实现。要不然测试将不好覆盖

第8章 自动化验收测试

引言

部署流水线的验收测试阶段的工作流程
环境和应用程序配置信息--》验收测试阶段(配置环境、部署二进制文件、冒烟测试、验收测试)--》制品库
对于一个单独的验收测试,它的目的是验证一个用户故事或需求的验收条件是否被满足。验收条件有多种类型,如功能性验收条件和非功能性验收条件。非功能性验收条件:容量、性能、可修改性、可用性、安全性、易用性

单元测试的确是自动化测试策略的关键部分,但是,它通常并不足以使人们确信程序能够发布。而验收测试的目标就是要证明应用程序的确实现了客户想要的,而不是以编程人员所认为的正确方式来运行的。单元测试的目标就是要证明:应用程序的某个单一部分的确是按开发人
员的思路运行的,但这并不能断言它也就是用户想要的功能

为什么验收测试至关重要

通过合理地创建和维护自动验收测试套件,其成本就会远低于频繁执行手工验收和回归测试的成本,或者低于发布低质量软件带来的成本。我们还发现,自动化验收测试能捕获那些即使单元或组件测试特别全面也都无法捕获的一些问题。

大家不喜欢自动化验收测试的真正原因是认为它太昂贵了。然而,我们还是能够把它的成本降到投入产出比可接受程度的。

第9章 非功能需求的测试

非功能需求的测试

把非功能需求与功能需求区别对待,就很容易把它从项目计划中移除,或者不给予它们足够的分析。对于实现来说,非功能需求是很复杂的,因为它们通常对系统架构有很大的影响。比如,任何需要高性能的系统都不应该让一个请求横跨系统中的多个层。由于在交付过程的后期很难对系统架构进行修改,所以在项目一开始就要考虑非功能需求,这是至关重要的。这意味着需要做一些恰到好处的预分析,定为系统选择什么样的架构

非功能需求之间可能彼此排斥:对安全性要求极高的系统常常在易用性上做一些妥协,而非常灵活的系统经常在性能方面有所妥协。

ps:文中提到了很多非功能测试:扩展性测试、容量测试、性能测试等等,但是测试感觉可以通过其他专业的书籍了解。

容量测试系统的附加价值

  • 重现生产环境中发现的复杂缺陷。
  • 探测并调试内存泄漏。
  • 持久性( longevity)测试。
  • 评估垃圾回收( garbage collection)的影响。
  • 垃圾回收的调优。
  • 应用程序参数的调优。
  • 第三方应用程序配置的调优,比如操作系统、应用程序服务器和数据库配置。
  • 模拟非正常的、最糟糕情况的场景。
  • 评估一些复杂问题的不同解决方案。
  • 模拟集成失败的情况。
  • 度量应用程序在不同硬件配置下的可扩展性。
  • 与外部系统进行交互的负载测试,即使容量测试的初衷是与桩替身接口( stubbederface)打交道。
  • 复杂部署的回滚演练。
  • 有选择地使系统的部分或全部瘫痪,从而评估服务的优雅降级( gracefulradation)。
  • 在短期可用的生产硬件上执行真实世界的容量基准,以便能计算出长期且低配的容量测试环境中更准确的扩展因素。

第十章 应用程序的发布与部署

从理论上讲,软件发布到生产环境和部署到测试环境的差异应该被封装在一组配置文件中。当在生产环境部署时,应遵循与其他任何环境部署同样的过程。启动自动部署系统,将要部署的软件版本和目标环境的名称告诉它,并点击“开始”就行了。所有后续部署和发布都要使用同样的流程。

发布计划

通常来说,第一次发布风险最高,需要细致地做个计划。而这种计划活动的结果可能是产出一些文档、自动化脚本或其他形式的流程步骤( procedure),用来保证应用程序在生产环境上的部署过程具有可靠性和可重复性。除了在发布策略中的这些材料以外,还要包括以下内容。

  • 第一次部署应用程序时所需的步骤。
  • 作为部署过程的一部分,如何对应用程序以及它所使用的服务进行冒烟测试。
  • 如果部署出现问题,需要哪些步骤来撤销部署。
  • 对应用程序的状态进行备份和恢复的步骤是什么。
  • 在不破坏应用程序状态的前提下,升级应用程序所需要的步骤是什么。
  • 如果发布失败,重新启动或重新部署应用程序的步骤是什么。
  • 日志文件放在哪里,以及它包括什么样的信息描述。
  • 如何对应用程序进行监控。
  • 作为发布的一部分,对必要的数据进行迁移的步骤有哪些。
  • 前一次部署中存在问题的记录以及它们的解决方案是什么

应用程序的部署与升级

首次部署

虚拟化和chicken-counting (0, 1, many)是你的好朋友。利用虚拟化技术在一个物理机器上创建一个环境来模拟生产环境的某些重要特征,还是非常容易的。chickencounting意味着,假如生产环境里有250个Web服务器的话,用两个服务器就足以代表进程边界了。随着开发工作的进行,可在以后适当的时间再根据需要不断完善它。一般来说,类生产环境具有如下特点。

  • 它的操作系统应该与生产环境一致。
  • 其中安装的软件应该与生产环境一致,尤其不能在其上安装开发工具(比如编译器或IDE)

对发布过程进行建模并让构建晋级

构建版本在各种不同环境之间的晋级,然而我们还要考虑更多的细节。尤其重要的是注意以下内容。

  • 为了达到发布质量,一个构建版本要通过哪些测试阶段(例如,集成测试、 QA验收测试、用户验收测试、试运行以及生产环境)。
  • 每个阶段需要设置什么样的晋级门槛或需要什么样的签字许可。
  • 对于每个晋级门槛来说,谁有权批准让某个构建通过该阶段。

ps:也就是说生产环境不能随意发布,需要经过严格的流程处理。

在项目一开始,就应该准备好试运行环境。如果你已经为生产环境准备好了硬件,而这些硬件尚没有其他用途的话,那么在第一次发布之前就可以把它作为试运行环境。以下是项目开始时就需要计划的一些事。
q 确保生产环境、容量测试环境和试运行环境已准备好。尤其是在一个全新的项目上,在发布前的一段时间就准备好生产环境,并把它作为部署流水线的一部分向其进行部署。

  • 准备好一个自动化过程,对环境进行配置,包括网络配置、外部服务和基础设施。
  • 确保部署流程是经过充分冒烟测试的。
  • 度量应用程序的“预热”时长。如果应用程序使用了缓存,这一点就尤其重要了。将这也纳入到部署计划中。
  • 与外部系统进行测试集成。你肯定不想在第一次发布时才让应用程序与真实的外部系统集成。
  • 如果可能的话,在发布之前就把应用程序放在生产环境上部署好。如果“发布”能像重新配置一下路由器那样简单,让它直接指向生产环境,那就更好了。这种被称作蓝绿部署( blue-green deployment)的技术会在本章后面详细描述。
  • 如果可能的话,在把应用程序发布给所有人之前,先试着把它发布给一小撮用户群。这种技术叫做金丝雀发布,也会在本章后续部分描述。
  • 将每次已通过验收测试的变更版本部署在试运行环境中(尽管不必部署到生产环境)。

部署回滚与零停机发布

金丝雀发布和蓝绿部署能够在一定程度上实现零停机发布和回滚。
在开始讨论之前,先要声明两个重要的约束。首先是数据。如果发布流程会修改数据,回滚操作就比较困难。另一个是需要与其他系统集成。如果发布中涉及两个以上的系统(也称联合环境的发布, orchestrated releases),回滚流程也会变得比较复杂。

当制定发布回滚计划时,需要遵循两个通用原则。首先,在发布之前,确保生产系统的状态(包括数据库和保存在文件系统中的状态)已备份。其次,在每次发布之前都练习一下回滚计划,包括从备份中恢复或把数据库备份迁移回来,确保这个回滚计划可以正常工作。

通过重新部署原有的正常版本来进行回滚

这通常是最简单的回滚方法。如果你有自动化部署应用程序的流程,让应用程序恢复到良好状态的最简单方法就是从头开始把前一个没有问题的版本重新部署一遍。这包括重新配置运行环境,让它能够完全和从前一样。这也是能够从头开始重建环境如此重要的原因之一。为什么创建环境和部署要从头开始呢?有以下几个理由。

  • 如果还没有自动回滚流程,但是已有自动部署流程了,那么重新部署前一版本是一种可预知时长的操作,而且风险较低(因为重新部署相对更不容易出错)。
  • 在此之前,已经对这个操作做过数百次测试(希望如此)。另外,执行回滚的频率相对比较低,所以包含bug的可能性要大一些。

通过重新部署原有的正常版本来进行回滚,有如下一些缺点。

  • 尽管重新部署旧版本所需的时间固定,但并不是不需要时间。所以一定会有一段停机时间。
  • 更难做调试,找到问题原因。重新部署旧版本通常是覆盖那个新版本,所以也失去了找到问题原因的最佳机会。
  • 如果你在部署新版本前已经备份了数据库,那么在重新安装旧版本时把数据库备份文件恢复回来的话,那些在新版本运行时产生的数据就丢失了。这大部分时候都是个严重的问题

零停机发布

零停机发布(也称为热部署),是一种将用户从一个版本几乎瞬间转移到另一个版本上的方法。更重要的是,如果出了什么问题,它还要能在瞬间把用户从这个版本转回到原先的版本上。

蓝绿部署

做法是有两个相同的生产环境版本,一个叫做“蓝环境”,一个叫做“绿环境”。新版本发布到蓝环境中,然后让应用程序先热身一下(你想多长时间都行),这根本不会影响绿环境。我们可以在蓝环境上运行冒烟测试,来检查它是否可以正常工作。当一切准备就绪以后,向新版本迁移就非常简单了,只要修改一下路由配置,将用户从绿环境导向蓝环境即可。这样,蓝环境就成了生产环境。

在做这种蓝绿部署时,要小心管理数据库。通常来说,直接从绿数据库切换到蓝数据库是不可能的,因为如果数据库结构有变化的话,数据迁移要花一定的时间。

ps:相当于随时冗余一个完全相同的环境

金丝雀发布

先部署新版本到一部分服务器上,而此时用户不会用到这些服务器。然后就在这个新版本上做冒烟测试,如果必要,还可以做一些容量测试。最后,你再选择一部分用户,把他们引导到这个新版本上。有些公司会首先选择一些“超级用户”来使用这个新版本。
金丝雀发布有以下几个好处。

  • 非常容易回滚。只要不把用户引到这个有问题的版本上就行了。此时就可以来分析日志,查找问题。
  • 还可以将同一批用户引至新版本和旧版本上,从而作A/B测试。某些公司会度量新特性的使用率,如果用的人不多,就会废弃它。
  • 可以通过逐渐增加负载,慢慢地把更多的用户引到新版本,记录并衡量应用程序的响应时间、 CPU使用率、I/O、内存使用率以及日志中是否有异常报告这种方式,来检查一下应用程序是否满足容量需求。如果生产环境太大,无法创建一个与实际情况相差不多的容量测试环境,那么这对于容量测试来说,是一个风险相对比较低的办法

在生产环境中保留尽可能少的版本也是非常重要的,最好限制在两个版本之内。支持多个版本是非常痛苦的,所以要将金丝雀的数目减少到最低限度。

紧急修复

有时候并不真正需要紧急修复一个缺陷。你需要考虑多少人会受到缺陷的影响,这个缺陷是否经常发生,发生后对用户有多大的影响。如果缺陷只影响少数人,而且发生频率不高,影响较低,而部署一个新版本的风险相对较高的话,可能就没有必要做紧急修复了。当然,通过有效的配置管理和自动部署过程来减少部署风险还有一些争议。紧急修复的另一种做法是回滚到以前使用的好版本上,如前所述。
下面是处理生产环境中的缺陷时应该考虑的一些因素。

  • 别自己加班到深夜来做这事儿,应该与别人一起结对做这事儿。
  • 确保有一个已经测试过的紧急修复流程。
  • 对于应用程序的变更,避免绕过标准的流程,除非在极端情况下。
  • 确保在试运行环境上对紧急修复版本做过测试。
  • 有时候回滚比部署新的修复版本更划算。做一些分析工作,找到最好的解决方案。想一想,假如数据丢失了,或者面对集成或联合环境时,会发生什么事?

持续部署

持续部署并不是适合所有人。有时候,你并不想立即将最新版本发布到生产环境中。在某些公司,由于制度的约束,产品上线需要审批。产品公司通常还要对已发布出去的每个版本做技术支持。

持续部署迫使你做正确的事儿。没有完整的自动化构建、部署、测试和发布流程,你无法做到持续部署。没有全面且可靠的自动化测试集合,你也无法做到持续部署。没有在类生产环境中运行的系统测试,你同样做不到持续部署。

ps:持续部署估计大部分公司都很难实现的,因为一般都有一个严格的评审流程。

不要删除旧文件,而是移动到别的位置

当做部署操作时,确保已保留了旧版本的一份副本。然后,在部署新版本之前清除旧版本的所有文件。如果旧版本的某个文件被遗忘在了最新部署版本的环境当中,出现问题后就很难追查了。更糟糕的是,如果旧版本的管理接口页面还留在那儿,那么很可能引起错误数据。

为新部署预留预热期

系统需要运行一段时间,足以让应用服务器和数据库建立好它们的缓存,准备好所有的连接,并完成了“预热”。

第11章 基础设施和环境管理

文档与审计

任何人在任何时候想修改一下测试环境或生产环境,都必须提出申请,并被审批。

基础设施的建模和管理

基础设施的访问控制

控制包括以下三方面。

  • 在没有批准的情况下,不允许他人修改基础设施。
  • 制定一个对基础设施进行变更的自动化过程。
  • 对基础设施进行监控,一旦发生问题,能尽早发现。

锁定生产环境以避免非授权访问是非常必要的,其对象既包括组织之外的人,也包括组织之内的人,甚至是运维团队的员工。

对基础设施进行修改

高效的变更管理流程有如下几个关键特征。

  • 无论是更新防火墙规则,还是部署flagship服务的新版本,每个变更都应该走同样的变更管理流程。
  • 这个流程应该使用一个所有人都需要登录的ticketing系统来管理。这样就可以得到有用的度量数据,比如每个变化的平均周期时间。
  • 做过的变更应该详细清楚地记录到日志中,这样便于以后做审计
  • 能够看到对每个环境进行变更的历史,包括部署活动。
  • 想做修改的话,首先必须在一个类生产环境中测试通过,而且自动化测试也已经运行完成,以确保这次变更不会破坏该环境中的所有应用程序。
  • 对每次修改都应该进行版本控制,并通过自动化流程对基础设施进行变更。
  • 需要有一个测试来验证这次变更已经起作用了。

服务器的准备以及配置管理

服务器的准备

  • 完全手工过程。
  • 自动化的远程安装。
  • 虚拟化。一个服务器启动多个虚拟机

ps:没懂自动化的远程安装怎么实现的

服务器的持续管理

一旦这个系统准备好之后,就能用一个被集中版本控制的配置管理系统对基础设施中的所有测试环境和生产环境进行管理了。之后,就可得到如下收益。

  • 确保所有环境的一致性
  • 很容易准备一个与当前环境配置相同的新环境,比如创建一个与生产环境相同的试运行环境。
  • 如果某个机器出现硬件故障,可以用一个全自动化过程配置一个与旧机器完全相同的新机器。

第12章 数据管理

数据库脚本化

与系统中其他变更一样,作为构建、部署、测试和发布过程的一部分,任何对数据库的修改都应该通过自动化过程来管理。也就是说,数据库的初始化和所有的迁移都需要脚本化,并提交到版本控制库中。

增量式修改

对数据库进行版本控制

以自动化方式迁移数据最有效的机制是对数据库进行版本控制。首先,要在数据库中创建一个,用来保存含它的版本号。然后每次对数据库进行修改时,你需要创建两个脚本:一个是将数据库从版本x升级到版本x+1(升级脚本),一个是将数据库版本x+1降级到版本x(回滚脚本)。还需要有一个配置项来设置应用程序使用数据库的哪个具体版本(它也可以作为一个常量放在版本控制库中,每次有数据修改时更新一下)。

数据库回滚和无停机发布

  1. 一种方法是将那些不想丢失的数据库事务( transaction)缓存一下,并提供某种方法重新执行它们。当升级数据库和应用程序到新版本时,确保记录了每次在新版本上发生的事务。
  2. 如果使用蓝— 绿部署(参见第10章)的话,可以考虑第二种方法。提示一下,在蓝—绿部署环境中,应用程序会同时有新旧两个版本同时运行,一个运行于蓝环境,另一个在绿环境。“发布”只是将用户请求从旧版本转到新版本上,而“回滚”只是再把用户请求转到旧版本上而已。

将应用程序部署与数据库迁移解耦

ps:改动前要做全量备份才行。例如删除字段,回滚流程则会变得比较麻烦。很多场景,全量备份是做不到的,比如数据量太大,而且在数据库写入过程全量备份,可能造成脏数据。另外如果是发布了一段时间之后才发现升级有问题,那么就不是简单的回滚了,新增和修改的数据得要考虑一下如何处理。

第13章 组件和依赖管理

在开发一个大型软件系统时,常常能看到这三个维度同时出现。在这样的系统中,组件间会形成一种依赖关系,而且也会依赖于外部库( external library)。每个组件可能会有几个发布分支。在这些组件中找到各组件的某个好用的版本进行编译,并组成一个完整的系统是一个极具难度的过程,有点类似于一个叫做“打地鼠”的游戏——我们曾听说有个项目曾花了几个月来做这件事。只要你遇到过类似的情况,就应该开始通过部署流水线来完成这样的事情了。

保持软件可发布

在开发过程中,团队会不断地增加新特性,有时候还要做较大的架构改变。在这些活动期间,应用程序是不能发布的,尽管它能够成功通过持续集成的提交测试阶段。通常,在发布之前,团队会停止开发新功能,并进入一个只做缺陷修复的稳定期。当应用程序发布后,就会在版本控制中拉出一个发布分支,而新功能的开发仍会在主干上进行。可是,这个流程常常会导致两次发布的时间间隔是几个星期或几个月。而持续交付的目标是让应用程序总是保持在可发布状态。那么如何做到这一点呢?

一种方法是在版本控制库中创建分支,当工作完成后再合并,以便主干一直是可发布的(下一章将会详细讨论这种方法)。然而,我们认为这种方法只是一种次优选择,因为如果工作成果是在分支上,那么应用程序就不是持续集成的。相,我们提倡每个人都应该提交到主干。可是,怎么既能让每个人都在主干上开发,又让应用程序一直保持在可发布状态呢?

  • 将新功能隐蔽起来,直到它完成为止。
  • 将所有的变更都变成一系列的增量式小修改,而且每次小的修改都是可发布的。
  • 使用通过抽象来模拟分支( branch by abstraction)的方式对代码库进行大范围的
    变更。
  • 使用组件,根据不同部分修改的频率对应用程序进行解耦。

将新功能隐蔽起来,直到它完成为止

有一种解决方案,就是把新功能直接放进主干,但对用户不可见。例如,某网站提供了旅行服务。运维这个网站的公司想提供一种新的服务:酒店预订。为了做到这一点,先把它作为一个单独的组件来开发,通过一个单独的URI“ /hotel”来访问。

另一种让半成品组件可以发布而不让用户访问的方法是通过配置项开关来管理。比如,在一个富客户端应用中,可能有两个菜单,一个包含新功能,另一个不包含新功能。可以用一个配置项在两个菜单之间进行切换。

所有修改都是增量式的

为了能够将大块变更分解成一系列的小修改,分析工作就要扮演非常重要的角色了。首先需要用各种各样的方式将一个需求分解成较小的任务。然后将这些任务再划分成更小的增量修改。这种额外的分析工作常常会使修改的错误更少、目的性更强。当然如果修改是增量式的,也就可以“边走边评估”( take stock as you go along),并决定是否需要继续做和如何继续。

通过抽象来模拟分支

当应用程序的某个部分需要做改进,但却无法使用一系列小步增量开发时,就要按如下步骤这么做。
(1) 在需要修改的那部分系统代码上创建一个抽象层。
(2) 重构系统的其他部分,让它使用这个抽象层。
(3) 创建一种新的实现代码,在它完成之前不要将其作为产品代码的一部分。
(4) 更新抽象层,让它使用这个新的实现代码。
(5) 移除原来的实现代码。
(6) 如果不再需要抽象层了,就移除它。
“通过抽象来模拟分支”是一次性实现复杂修改或分支开发的替代方法。它让团队在持续集成的支撑下持续开发应用程序的同时替换其中的一大块代码,而且这一切都是在主干上完成的。

ps:感觉是在代码中增加了一层代码,相当于通过该代码控制走哪个实现类,但是这样子怎么进行测试?能够动态修改走哪个实现类的配置项?

依赖

库管理

在软件项目中,有两种适当的方法来管理库文件。一种是将它们提交到版本控制库中,另一种是显式地声明它们,并使用像Maven或Ivy这样的工具从因特网上或者(最好)从你所在组织的公共库中下载。

第14章 版本控制进阶

创建分支的原因

第一,为了发布应用程序的一个新版本,需要创建一个分支。
这使开发人员能够不断开发新功能,而不会影响稳定地对外发布版本。当发现bug时,首先在相应的对外发布分支上修复,然后再把补丁合并到主干上。而该发布分支从来不会合并回主干。第二,当需要调研一个新功能或做一次重构时——调研分支最终会被丢弃并且从来不会被合并回主干。第三,当需要对应用程序做比较大的修改,但又无法使用上一章所描述的办法来避免分支时,也可以使用生命周期较短的分支(其实,如果代码基结构良好的话,这种情况也很少见)。分支的唯一目的就是可以对代码进行增量式或“通过抽象来模拟分支”方式的修改。

分支、流和持续集成

使用分支和持续集成之间会有某种相互制约的关系。如果一个团队的不同成员在不同分支或流上工作的话,那么根据定义,他们就不是在做持续集成。让持续集成成为可能的一个最重要实践就是每个人每天至少向主干提交一次。因此,如果你每天将分支合并到主线一次(而不只是拉分支出去),那就没什么。如果你没这么做,你就没有做持续集成。

每个新特性,或者新的开发都切一个分支。这种策略的问题在于:这些分支会在很长时间内一直处于不可发布的状态。而且,这些分支通常对其他分支都有一些软依赖( softdependency)。在上面的例子中,每个分支都要从集成分支上将修复缺陷的代码拿过去,而且每个分支还要从性能调优的分支上将性能调优的代码拿过去。而应用程序的一个定制版本( custom version)还在开发中,且长时间处于不可部署的状态。

每个分支都进行构建:在这种模式下,新开发的代码总是被提交到主干上。只有在发布分支上修改缺陷时才需要合并,而且这个合并是从分支合并回主干。而只有非常严重的缺陷修复才会从主干合并到发布分支上。这种模式要好一些,因为代码一直处于可发布状态,所以也就更容易发布。分支越少,合并和跟踪分支的工作就越少。

ps:实际工作中,使用jenkins就可以做到对每个新建分支的实时构建与编译,就是对jenkins的要求会高一些。# 第1章 软件交付的问题

常见的发布反模式

手工部署软件

  1. 有一份非常详尽的文档,该文档描述了执行步骤及每个步骤中易出错的地方。
  2. 以手工测试来确认该应用程序是否运行正确。
  3. 在发布当天开发团队频繁地接到电话,客户要求解释部署为何会出错。
  4. 在发布时,常常会修正一些在发布过程中发现的问题。
  5. 如果是集群环境部署,常常发现在集群中各环境的配置都不相同,比如应用服务器的连接池设置不同或文件系统有不同的目录结构等。
    6.发布过程需要较长的时间(超过几分钟)。
  6. 发布结果不可预测,常常不得不回滚或遇到不可预见的问题。
  7. 发布之后凌晨两点还睡眼惺忪地坐在显示器前,绞尽脑汁想着怎么让刚刚部署的应用程序能够正常工作。

开发完成之后才向类生产环境部署

由于部署工作中的很多步骤根本没有在试运行环境上测试过,所以常常遇到问题。比如,文档中漏掉了一些重要的步骤,文档和脚本对目标环境的版本或配置作出错误的假设,从而使部署失败。部署团队必须猜测开发团队的意图。

生产环境的手工配置管理

很多组织通过专门的运维团队来管理生产环境的配置。如果需要修改一些东西,比如修改数据库的连接配置或者增加应用服务器线程池中的线程数,就由这个团队登录到生产服务器上进行手工修改。

  1. 多次部署到试运行环境都非常成功,但当部署到生产环境时就失败。

  2. 集群中各节点的行为有所不同。例如,与其他节点相比,某个节点所承担的负载少一些,或者处理请求的时间花得多一些。

  3. 运维团队需要较长时间为每次发布准备环境。

  4. 系统无法回滚到之前部署的某个配置,这些配置包括操作系统、应用服务器、关系型数据库管理系统、Web服务器或其他基础设施设置。

  5. 不知道从什么时候起,集群中的某些服务器所用的操作系统、第三方基础设施、依赖库的版本或补丁级别就不同了。

6.直接修改生产环境上的配置来改变系统配置。

要有统一的配置管理,其责任之一就是让你能够重复地创建那些你开发的应用程序所依赖的每个基础设施。这意味着操作系统、补丁级别、操作系统配置、应用程序所依赖的其他软件及其配置、基础设施的配置等都应该处于受控状态。你应该具有重建生产环境的能力,最好是能通过自动化的方式重建生产环境。

如何实现目标

  1. 自动化。如果构建、部署、测试和发布流程不是自动化的,那它就是不可重复的。由于软件本身、系统配置、环境以及发布过程的不同,每次做完这些活动以后,其结果可能都会有所不同。
    
  2. 频繁做。如果能够做到频繁发布,每个发布版本之间的差异会很小。这会大大减少与发布相关的风险,且更容易回滚。

反馈流程

  1. 创建可执行代码的流程必须是能奏效的。这用于验证源代码是否符合语法。
    q 2. 软件的单元测试必须是成功的。这可以检查应用程序的行为是否与期望相同。
    q 3. 软件应该满足一定的质量标准,比如测试覆盖率以及其他与技术相关的度量项。
    q 4. 软件的功能验收测试必须是成功的。这可以检查应用是否满足业务验收条件,交付了所期望的业务价值。
    q 5. 软件的非功能测试必须是成功的。这可以检查应用程序是否满足用户对性能、有效性、安全性等方面的要求。
    q 6. 软件必须通过了探索性测试,并给客户以及部分用户做过演示。这些通常在一个手工测试环境上完成。此时,产品负责人可能认为软件功能还有缺失,我们自己也可能发现需要修复的缺陷,还要为其写自动化测试来避免回归测试

快速反应

接受到反馈的信息之后需要快速做出反应。

第2章 配置管理

引言

假如项目中有良好的配置管理策略,那么你对下列所有问题的回答都应该是“ YES”。
q 1. 你能否完全再现你所需要的任何环境(这里的环境包括操作系统的版本及其补丁级别、网络配置、软件组合,以及部署在其上的软件应用及其配置)?
q 2. 你能很轻松地对上述内容进行增量式修改,并将修改部署到任意一种或所有环境中吗?
q 3. 你能否很容易地看到已被部署到某个具体环境中的某次修改,并能追溯到修改源,知道是谁做的修改,什么时候做的修改吗?
你能满足所有必须遵守的规程章则吗?
q 4. 是否每个团队成员都能很容易地得到他们所需要的信息,并进行必要的修改呢?这个配置管理策略是否会妨碍高效交付,导致周期时间增加,反馈减少呢?

终极配置

就像一个平衡游标,一端是只有单一用途的软件,而且工作得很好,但很难或根本无法改变它的行为。然而另一端则是编程语言,你可以用它编写游戏、应用服务器或股票管理系统,这就是灵活性!显然,大多数软件都在两点之间,而不是这两端点中的任何一个。

根据需求,要为用户提供的软件配置能力越强,你能置于系统配置的约束就应越少,而你的编程环境也会变得越复杂。

“修改配置信息的风险要比修改代码的风险低”这句话就是个错觉。就拿“停止一个正在运行的应用系统”这个需求来说,通过修改代码或修改配置都很容易办到。如果使用修改源代码的方式,可以有多种方式来保证质量,比如编译器会帮我们查语法错误,自动化测试可以拦截很多其他方面的错误。然而,大多数配置信息是没有格式检查,且未经测试的。

在构建高度可配置的软件的道路上有很多陷阱,而最糟糕的可能莫过于下面这些。

  1. 经常导致分析瘫痪,即问题看上去很严重,而且很棘手,以至于团队花费很多时间思考如何解决它,但最终还是无法解决。
  2. 系统配置工作变得非常复杂,以至于抵消了其在灵活性上带来的好处。更有甚者,可能在配置灵活性上花费的成本与定制开发的成本相当。

配置并非天生邪恶,但需要采取谨慎的态度来一致地管理它们。现代计算机语言已经采用各种各样的特性和技术来帮助减少错误。在大多数情况下,配置信息却无法使用它们,甚至这些配置的正确性在测试环境和生产环境中也根本无法得到验证。

软件配置管理

管理配置信息的原则

当创建应用程序的配置信息时,应该考虑以下几个方面。
q 1. 在应用程序的生命周期中,我们应该在什么时候注入哪类配置信息。是在打包的时候,还是在部署或安装的时候?是在软件启动时,还是在运行时?要与系统运维和支持团队一同讨论,看看他们有什么样的需求。(ps:这个一般是项目最初始创建的时候已经确定,按照目前的结构,很大概率是有配置中心,或者不同环境使用不同的配置文件)
q 2. 将应用程序的配置项与源代码保存在同一个存储库中,但要把配置项的值保存在别处。另外,配置设置与代码的生命周期完全不同,而像用户密码这类的敏感信息就不应该放到版本控制库中。(ps:这个根据实际情况吧。要是没有配置中心,估计是做不到的吧。)
q 3. 应该总是通过自动化的过程将配置项从保存配置信息的存储库中取出并设置好,这样就能很容易地掌握不同环境中的配置信息了。(ps:配置项通过自动化搞?这就有点不是很懂是干啥了)
q 4. 配置系统应该能依据应用、应用软件的版本、将要部署的环境,为打包、安装以及部署脚本提供不同的配置值。每个人都应该能够非常容易地看到当前软件的某个特定版本部署到各种环境上的具体配置信息。

  1. 对每个配置项都应用明确的命名习惯,避免使用晦涩难懂的名称,使其他人不需要说明手册就能明白这些配置项的含义。
    q 6. 确保配置信息是模块化且封闭的,使得对某处配置项的修改不会影响到那些与其无关的配置项。

  2. DRY( Don’t Repeat Yourself )原则。定义好配置中的每个元素,使每个配置元素在整个系统中都是唯一的,其含义绝不与其他元素重叠。

  3. 最少化,即配置信息应尽可能简单且集中。除非有要求或必须使用,否则不要新增配置项。

  4. 避免对配置信息的过分设计,应尽可能简单。

  5. 确保测试已覆盖到部署或安装时的配置操作。检查应用程序所依赖的其他服务是否有效,使用冒烟测试来诊断依赖于配置项的相关功能是否都能正常工作。

环境管理

环境管理的关键在于通过一个全自动过程来创建环境,使创建全新的环境总是要比修复已受损的旧环境容易得多。(ps:感觉docker可以很简单实现这个场景)

变更管理过程

对环境的变更过程进行管理是必要的。应该严格控制生产环境,未经组织内部正式的变更管理过程,任何人不得对其进行修改。这么做的原因很简单:即便很微小的变化也可能把环境破坏掉。任何变更在上线之前都必须经过测试,因而要将其编成脚本,放在版本控制系统中。

第3章 持续集成

持续集成要求每当有人提交代码时,就对整个应用进行构建,并对其执行全面的自动化测试集合。而且至关重要的是,假如构建或测试过程失败,开发团队就要停下手中的工作,立即修复它。持续集成的目标是让正在开发的软件一直处于可工作状态。

准备工作

版本控制

与项目相关的所有内容都必须提交到一个版本控制库中,包括产品代码、测试代码、数据库脚本、构建与部署脚本,以及所有用于创建、安装、运行和测试该应用程序的东西。

自动化构建

人和计算机都能通过命令行自动执行应用的构建、测试以及部署过程。(ps:以来maven就可以实现)

团队共识

持续集成不是一种工具,而是一种实践。它需要开发团队能够给予一定的投入并遵守一些准则,需要每个人都能以小步增量的方式频繁地将修改后的代码提交到主干上,并一致认同“修复破坏应用程序的任意修改是最高优先级的任务”。如果大家不能接受这样的准则,则根本无法如预期般通过持续集成提高质量。

一个基本的持续集成系统

一旦准备好要提交最新修改代码时,请遵循如下步骤。
(1) 查看一下是否有构建正在运行。如果有的话,你要等它运行完。如果它失败了,你要与团队中的其他人一起将其修复,然后再提交自己的代码。
(2) 一旦构建完成且测试全部通过,就从版本控制库中将该版本的代码更新到自己的开发环境上。
(3) 在自己的开发机上执行构建脚本,运行测试,以确保在你机器上的所有代码都工作正常。当然你也可以利用持续集成工具中的个人构建功能来完成这一步骤。
(4) 如果本地构建成功,就将你的代码提交到版本控制库中。
(5) 然后等待包含你的这次提交的构建结果。
(6) 如果这次构建失败了,就停下手中做的事,在自己的开发机上立即修复这个问题,然后再转到步骤(3)。
(7) 如果这次构建成功,你可以小小地庆祝一下,并开始下一项任务。如果团队中的每个人在每次提交代码时都能够遵循这些简单的步骤,你就可以很有把握地说:“只要是在与持续集成一模一样的环境上,我的软件就可以工作。”

持续集成的前提条件

频繁提交

对于持续集成来说,我们最重要的工作就是频繁提交代码到版本控制库。每天至少应该提交几次代码。

定期地将代码提交到代码主干上会给我们带来很多其他好处。比如,它使每次的修改都比较小,所以很少会使构建失败。当你做了错事或者走错了路线时,可以轻松地回滚到某个已知的正确版本上。它使你的重构更有规则,使每次重构都是小步修改,从而保证可预期的行为。它有助于保证那些涉及多个文件的修改尽量不会影响其他人的工作。

ps:对于修订简单bug这种可能比较简单,但是对于一个功能开发,往往都是等功能开发完成,至少自己测试完成才会合并到分支上的。频繁提交的前提可能也是方案要确定才行吧,要是方案都没有确定,修订往往是反反复复的。所以这个也是根据实际的情况来搞的。

创建全面的自动化测试套件

单元测试用于单独测试应用程序中某些小单元的行为(比如一个方法、一个函数,或一小组方法或函数之间的交互)。它们通常不需要启动整个应用程序就可以执行,而且也不需要连接数据库(如果应用程序需要数据库的话)、文件系统或网络。它们也不需要将应用程序部署到类生产环境中运行。单元测试应该运行得非常快,即使对于一个大型应用来说,整个单元测试套件也应该在十分钟之内完成。(ps:使用jmokit就可以实现本功能吧。)

组件测试用于测试应用程序中几个组件的行为。与单元测试一样,它通常不必启动整个应用程序,但有可能需要连接数据库、访问文件系统或其他外部系统或接口(这些可以使用“桩”,即stub技术)。组件测试的运行时间通常较长。(ps:直接使用spring自带的测试组件似乎能够实现,但是这种往往又要以来其他组件的代码也是最新的,反而没有单元测试那么可控。)

验收测试的目的是验证应用程序是否满足业务需求所定义的验收条件,包括应用程序提供的功能,以及其他特定需求,比如容量、有效性、安全性等。验收测试最好采用将整个应用程序运行于类生产环境的运作方式。当然,验收测试的运行时间也较长。一个验收测试套件连续运行一整天是很平常的事儿。(ps:这想要一天搞定恐怕很难吧。得要有专门的自动化测试系统,性能、安全性这些也需要专门的处理)

保持较短的构建和测试过程

如果代码构建和单元测试的执行需要花很长时间的话,你会遇到一些麻烦,如下所示。

  1. 大家在提交代码之前不愿意在本地环境进行全量构建和运行测试,导致构建失败的几率越来越大。

  2. 持续集成过程需要花太长时间,从而导致再次运行构建时,该构建会包含很多次提交,所以很难确定到底是哪次提交破坏了本次构建。

  3. 大家提交的频率会变少,因为每运行一次构建和测试,都要坐在那儿等上一阵子。

理想情况下,提交前的预编译和测试过程,以及持续集成服务器上的编译和测试过程应该都能在几分钟内结束。我们认为,十分钟是一个极限了,最好是在五分钟以内,九十秒内完成是最理想的。

ps:确实构建的时间非常重要,过长的时间会影响大家持续构建的积极性

管理开发工作区

当开发人员刚开始新任务时,应该总是从一个已知正确的状态开始。他们应该能够运行构建、执行自动化测试,以及在其可控的环境上部署其开发的应用程序。

ps:保证新的开发人员介入,不需要额外的人工或者特殊的配置,将对应代码下载下来即可正常工作。并且新的开发人员也可以在本地进行编译、构建、单元测试。

使用持续集成软件

基本操作

本质上,持续集成软件包括两个部分。第一部分是一个一直运行的进程,它每隔一定的时间就执行一个简单的工作流程。第二部分就是提供展现这个流程运行结果的视图,通知你构建和测试成功与否,让你可以找到测试报告,拿到生成的安装文件等。

必不可少的实践

持续集成系统的目标是,确保软件在任何时候都可以工作。为了做到这一点,下面是我们在自己的团队中使用的一些实践。

构建失败之后不要提交新代码

第一准则谁的构建失败,谁处理,并且得要优先处理。

ps:往往需要增加人工的审核步骤,不然按照人性,大家都是偷工减料,不愿意处理的。比如构建失败之后,管理人员会立即收到对应的邮件,并且提醒本次构建失败的提交人员,让其修复。

提交前在本地运行所有的提交测试,或者让持续集成服务器完成此事

如果以前未听说或使用过这种方法,你可能会问:“为什么在提交前还要运行本地提交测试呢?这样的话,我们的编译和提交测试不是要运行两次了吗?”这么做,有两个理由。
(1) 如果在你根据版本控制进行更新之前,其他人已经向版本控制库中提交了新代码,那么你的变更与那些新代码合并后,可能会导致测试失败。如果你自己先在本地更新代码并运行提交测试的话,假如有问题,就会在本地提前发现,提前修复,从而不会令持续集成服务器上的构建失败,不至于影响其他人及时提交。
(2) 在提交时经常犯的错误是,忘记提交那些刚刚新增加的东西到存储库中。如果遵守这个流程的话,当本地构建成功,而持续集成系统中的提交阶段失败了的话,那么你就知道要么是由于别人与你同时提交了代码,要么就是你遗漏了一部分类或配置文件没有提交到版本控制系统中。

等提交测试通过后再继续工作

构建失败是持续集成过程中一个平常且预料之中的事情。我们的目标是尽快发现错误,并消灭它们。

ps:如果构建的时间足够短,那么是可以等待构建完成再进行下一步动作的。如果构建时间太长,则可以进行下一步工作,但是要关注构建完成之后的消息吧。

回家之前,构建必须处于成功状态

倘若在回家的时间节点发现构建失败了,就应该立即修复,而不是等回家,或者第二天来搞。当下修复的成本和影响肯定是最低的。

时刻准备着回滚到前一个版本

ps:使用版本控制工具还是有办法做到的,只是也有成本。如果单次提交功能过大,是没法子很好的回滚的。因而作者整本书其实都在强调小提交。

在回滚之前要规定一个修复时间

如果因某次提交而导致构建失败,必须在十分钟之内修复它。如果在十分钟内还没有找到解决方案的话,就将其回滚到版本控制系统中前一个好的版本。

ps:回滚的代价可能会更大,所以只能监督构建失败的小伙伴尽快修复了。

不要将失败的测试注释掉

将失败的测试注释掉应该是最后不得已的选择,除非你马上就去修改它,否则尽量不要这么做。偶尔注释掉一个测试是可以的,比如,当某个非常严重的问题需要解决,或者是某些内容需要与客户进一步探讨时。

ps:根据实际的情况处理,做不到一刀切。失败的单元测试,得要找到失败的原因,可能是需求已经不存在,或者需求变动,这样都可以直接删除,反之则要修订该单元测试。

为自己导致的问题负责

假如提交代码后,你写的测试都通过了,但其他人的测试失败了,构建结果还是会失败。发现构建失败就应该去解决。

若测试运行变慢,就让构建失败

ps:这个时间不好界定吧,服务器的波动,或者其他的东西,都可能导致有问题吧。

若有编译警告或代码风格问题,就让测试失败

编译器发出警告时,通常理由都足够充分。我们曾经用过一个比较成功的策略,即只要有编译警告,就让构建失败

ps:checkstyle属于是代码风格的统一校验,属于静态代码检查,还是属于比较好遵守的。对于开发的语法方法的校验,可能就得要使用PMD之类的。

分布式团队

使用场景比较少。

第4章测试策略的实现

引言

测试会建立我们的信心,使我们相信软件可按预期正常运行。

一个全面的自动化测试套件甚至可以提供最完整和最及时的应用软件说明文档,这个文档不仅是说明系统应该如何运行的需求规范,还能证明这个软件系统的确是按照需求来运行的。

测试的分类

业务导向且支持开发过程

自动化验收测试有很多很有价值的特性。

  1. 它加快了反馈速度,因为开发人员可以通过运行自动化测试,来确认是否完成了一个特定需求,而不用去问测试人员。

  2. 它减少了测试人员的工作负荷。

  3. 它让测试人员集中精力做探索性测试和高价值的活动,而不是被无聊的重复性工作所累。

  4. 这些验收测试也是一组回归测试套件。当开发大型应用或者在大规模团队中工作时,由于采用了框架或许多模块,对应用某一部分的更改很可能会影响其余特性,所以这一点尤其重要。

  5. 就像行为驱动开发( BDD)所建议的那样,使用人类可读的测试以及测试套件
    名,我们就可以从这些测试中自动生成需求说明文档。像Cucumber和Twist这样的工具,就是为让分析人员可以把需求写成可执行的测试脚本而设计的。这种方法的好处在于通过验收测试生成的需求文档从来都不会过时,因为每次构建都会自动生成它。

对于软件开发的各个方面,各个项目之间都会有所不同,你需要监控到底花了多长时间做重复性的手工测试,以便决定什么时候把它们自动化。一个很好的经验法则就是,一旦对同一个测试重复做过多次手工操作,并且你确信不会花太多时间来维护这个测试时,就要把它自动化。

技术导向且支持开发过程的测试

这些自动化测试单独由开发人员创建并维护。 有三种测试属于这一分类:单元测试、组件测试和部署测试。

业务导向且评价项目的测试

这类手工测试可以验证我们实际交付给用户的应用软件是否符合其期望。这并不只是验证应用是否满足需求规格说明,还验证需求规格说明的正确性。

技术导向且评价项目的测试

验收测试分为两类:功能测试和非功能测试。非功能测试是指除功能之外的系统其他方面的质量,比如容量、可用性、安全性等。正如我们之前提到的,功能测试与其依据是非功能需求测试不是面向业务的。这似乎是显而易见的,但是很多项目并不把非功能需求放在与功能需求同等重要的地位来对待,而且可能会更糟糕,他们根本不去验证这些非功能需求。虽然用户很少花时间提前对容量和安全性做要求,但一旦他们的信用卡信息被盗,或者网站由于容量问题总是停止运行,他们就会非常生气。

现实中的情况与应对策略

新项目

相对于项目开发几个迭代后再写验收测试来说,在项目开始就采用这样的流程是比较容易的。在项目开始一段时间以后再考虑这一问题时,你的代码框架很可能并不支持这种验收测试的书写,所以你不仅必须寻找一些方法实现这些验收测试,还要说服那些持怀疑态度的开发人员,让他们认真遵守这个流程。如果在项目一开始就做自动化测试,可能更容易让开发人员接受。

当然,必须让团队的每个人(包括客户和项目经理在内)都接受这种做法。我们曾看到过一些项目取消这种做法,因为客户觉得写自动化验收测试花费了太多的时间。假如客户真的愿意以牺牲自动化验收测试套件的质量为代价达到快速将软件推向市场的目标,那么,作出这样的决定也无可厚非。当然,其后果也应该非常明显啦。

项目进行中

引入自动化测试最好的方式是选择应用程序中那些最常见、最重要且高价值的用例为起点。这就需要与客户沟通,以便清楚地识别真正的业务价值是什么,然后使用测试来做回归,以防止功能被破坏。

ps:进行中的项目想要全面实现自动化测试时不现实的,所以可以选择优先级高的用例。

遗留系统

如果没有自动化构建流程,那么最高优先级的事儿就是创建一个,然后再创建更多的自动化功能测试来丰富它。如果有文档,或能够找到那些曾经或正工作在这个系统之上的成员的话,创建自动化测试套件会更容易一些。然而现实往往并非如此。

ps:遗留系统,要是修订量或者后续的开发量比较少,则维护的价值就很低了,一般公司是不会花费时间去处理这个的。

集成测试

假如你的应用程序需要通过一系列不同的协议与各种外部系统进行交互,或者它由很多松散耦合的模块组成,而模块之间还有很复杂的交互操作的话,集成测试就非常重要了。在组件测试和集成测试之间的分界线并不十分清晰(尤其当“集成测试”这个词被赋予了太多的意义)。我们所说的“集成测试”是指那些确保系统的每个独立部分都能够正确作用于其依赖的那些服务的测试。

通常来说,集成测试应该在两种上下文中运行:首先是被测试的应用程序使用其真正依赖的外部系统来运行时,或者是使用由外部服务供应商所提供的替代系统;其次是应用程序运行于你自己创建的一个测试用具( testharness)之上,而且这些测试用具也是代码库的一部分。

在理想情况下,服务提供商会提供一个复制版的测试服务,除了性能以外,它可以提供与真正的服务完全相同的行为。你可以在此之上进行测试。然而,在现实世界中,你常常需要开发一个测试用具。比如当:

  1. 外部系统还没有开发完成,但接口已经提前定义好了(此时你需要有心理准备,因为这些接口很可能会发生变化);
  2. 外部系统已经开发完了,但是还不能为了测试而部署它,或者用于测试目的的外部系统运行太慢,或缺陷太多,无法支持正常自动化测试的运行;
  3. 虽然有测试系统,但它的响应具有不确定性,从而导致无法对自动化测试结果进行验证(比如,某个股票市场的实时数据);
  4. 外部系统很难安装或者需要通过用户界面进行手工干预;
  5. 需要为涉及外部系统服务的功能写一份标准的自动化验收测试,而这些测试应该一直在测试替身上运行
  6. 自动化持续集成系统需要承担的工作量太大且其所需要的服务水平太高,远不是一个仅用于做手工探索性测试的轻量级测试环境所能承受或提供的。

ps:这边提到的测试用具,应该类似于mock服务吧。与第三方系统约定的借口已经完成,但是第三方还没有就绪的情况下,可以通过mock服务,模拟第三方服务的返回值。

测试用具不但应该能返回服务调用所期望的响应,而且还要能返回不可预期的响应。

ps:通过测试用具,应该是能够更加方便的模拟很多异常场景的请求的,从而使得代码更加的健壮。

ps:测试用具的编写本身也是一个有工作量的事情。如果有多个系统,则可能需要编写多个系统的借口功能。如果本系统要做到自动化测试,那测试用具估计也得要做到增量性开发吧。

流程

在用户故事的开发过程中,开发人员和测试人员的紧密合作是保证平稳发布的关键。无论开发人员什么时候完成一个功能,他们都应该把测试人员叫到身边,让他们检查一下。测试人员应该在开发人员的机器上做一下测试。此时,开发人员可以在附近的某个终端或笔记本电脑上继续工作,比如修改某些回归缺陷。这样他们仍旧在工作(因为测试可能需要花上一点儿时间),但测试人员随时能找他来讨论问题。

ps:作者应该是敏捷性的开发团队,作者的描述也是在功能完成之后测试立即就可以介入,但是一般的开发团队都是整个大功能完成之后才介入的。

带着一堆缺陷继续前进是有风险的。过去,很多开发团队和开发过程都对大量缺陷采取视而不见的不关注态度,总是希望以后能找个适当时机来修复它们。然而,几个月之后,他们就会看到堆积如山的缺陷,其中有些缺陷以后根本不会被修复,而有些因为功能需求的变化已不再是缺陷了。但是,还有一些缺陷对于某个用户是非常严重的,但它们却被淹没在缺陷海洋中了。

像对待功能特性一样来对待缺陷。毕竟,修复缺陷和开发新功能一样,都需要花时间和精力。因此,客户可以将某个缺陷与要开发的新功能进行对比,得出它们的相对优先级。比如,一个出现概率很小的缺陷,只会影响少量用户,而且还有一个已知的临时解决方案,那么修复它的重要性可能要低于那些可以为用户带来收入的新功能。至少,我们可以把缺陷分为严重( critical)、 阻塞( blocker)、 中( medium)和低( low)四个级别。要想找到更全面的评估方法,我们可能还要考虑缺陷发生的频率,对用户的影响是什么,以及是否有临时解决方案等。

ps:一般的团队估计是定义bug的等级会更加方便与合理吧。应该要有bug系统统一管理现在的bug。锐捷网络的bug等级更加细化,critical、blocker、normal、minor 、enhancement。normal级以上的bug就是需要重点关注要解决的。不过bug的等级得要是准确的标注才行,随意的标注等级,可能造成严重的bug被忽略了。可能要有一个bug评审的环境,避免严重bug被忽略。

第二部分 部署流水线

什么是部署流水线

部署流水线是指软件从版本控制库到用户手中这一过程的自动化表现形式。

ps:网龙的一键发布就是这样子的。点击发布就能够将服务部署上去,然后通过界面就可以访问与测试了。

  1. 提交阶段是从技术角度上断言整个系统是可以工作的。这个阶段会进行编译,运行一套自动化测试(主要是单元级别的测试),并进行代码分析。
  2. 自动化验收测试阶段是从功能和非功能角度上断言整个系统是可以工作的,即从系统行为上看,它满足用户的需要并且符合客户的需求规范。
  3. 手工测试阶段用于断言系统是可用的,满足了它的系统要求,试图发现那些自动化测试未能捕获的缺陷,并验证系统是否为用户提供了价值。这一阶段通常包括探索性测试、集成环境上的测试以及UAT( User Acceptance Testing,用户验收测试)。
  4. 发布阶段旨在将软件交付给用户,既可能是以套装软件的形式,也可能是直接将其部署到生产环境,或试运行环境(这里的试运行环境是指和生产环境相同的测试环境)。

部署流水线就是由上述这些阶段,以及为软件交付流程建模所需的其他阶段组成,有时候也称为持续集成流水线、构建流水线、部署生产线或现行构建( living build)。无论把它叫做什么,从根本上讲,它就是一个自动化的软件交付流程。这并不是说该发布过程不需要人的参与,而是说在执行过程中那些易出错且复杂的步骤被变成可靠且可重复的自动化步骤。事实上,人工参与的活动反而有增加的趋势,因为在开发流程中所有阶段均可进行一键式部署这一事实,会促使测试人员、分析人员、开发人员以及(最重要的)用户更频繁地执行它。

ps:网龙的一建部署类似于这个效果了。提交之后,先进行编译--》测试环境通过--》代码mr到master分支,预生产发布--》预生产发布测试人员介入测试,测试通过之后--》发布

部署流水线的相关实践

只生成一次二进制包

很多构建系统将版本控制库中的源代码作为多个步骤中最权威的源,不同上下文中会重复编译这个源,比如在提交时、做验收测试时或做容量测试时。而且,在每个不同的环境上部署时都要重新编译一次。但是,对于同一份源代码,每次都重新编译的话,会引入“编译结果不一致”的风险。

假如重新创建二进制包,就会存在这样的风险,即从第一次创建二进制包到最后发布这两个时间点之间会引入某种变化,比如在不同阶段里,编译时所用的软件工具链有差异,此时这个即将发布的二进制包就不是我们曾经测试过的那个二进制包了。出于审计的目的,确保从二进制包的创建到发布之间不会因失误或恶意攻击而引入任何变化是非常关键的。如果是解释性语言的话,有些组织甚至要求只有资深人员才有权在某个特定的环境里进行编译、组装或打包,其他人不得插手。所以一旦创建了二进制包,在需要时最好是重用,而不是重新创建它们。

ps:多个源相当于是多个分支吧。比如测试对应debug分支,开发对应development分支,预生产和生产对应master分支。想要保证所有的流程都使用一个二进制文件还是挺难得,首先至少是要做到配置文件与二进制人间解耦。网龙在流程上是做了一个折中,预生产和生产得要使用同一个二进制文件,凡是提交到master分支的mr都需要经过评审人员确认才行。

对不同环境使用同一部署方式

当然,每个环境多多少少都会有所不同,至少IP地址肯定是不一样的。通常还会有其他不同之处,比如操作系统和中间件的配置设置,数据库的安装位置和外部服务的位置,以及在部署时需要设置的其他配置信息。但这并不意味着你应该为每个环境都建立一个单独的部署脚本,而只要把那些与特定环境相关的特定配置分开放置就行了。一种方法是使用属性文件保存配置信息,比如分别为每个环境保存一个属性文件,并将其放在版本控制库中。

对部署进行冒烟测试

当做应用程序部署时,你应该用一个自动化脚本做一下冒烟测试,用来确保应用程序已经正常启动并运行了。这个测试应该非常简单,比如只要启动应用程序,检查一下,能看到主页面,并在主页面上能看到正确的内容就行了。这个冒烟测试还应该检查一下应用程序所依赖的服务是否都已经启动,并且正常运行了,比如数据库、消息总线或外部服务等。

ps:作者提到的冒烟测试属于最基础的了,实际上的冒烟测试可能还包括做一些最基础的操作。锐捷网络的云桌面最基础的模块就是镜像,有了镜像才能够通过它创建后续的一系列活动,因而冒烟的动作就要包括镜像的创建。

向生产环境的副本中部署

为了对系统上线充满信心,你要尽可能在与生产环境相似的环境中进行测试和持续集成。

ps:论预生产环境的重要性

每次构建都要立即在流水线中传递

很多项目都有一个各阶段的执行时间表,比如每小时构建一次,每天晚上运行一次验收测试,每个周末运行一次容量测试。部署流水线则使用了不同的方式:每次提交都要触发第一个阶段的执行,后续阶段在第一个阶段成功结束后,立即被触发。

ps:如果是定时构建,则可能会丢失某一部分的提交没有被正确构建。部署流水线方式则要求每次的提交都会触发构建。

只要有环节失败,就停止整个流水线

每次提交代码到版本控制系统中后,都能够构建成功并通过所有的测试。对于整个部署流水线来说,都适用这一要求。假如在某个环境上的某次部署失败了,整个团队就要对这次失败负责,应该停下手头的工作,把它修复后再做其他事情。

提交阶段

代码提交阶段,就应该做一些校验。比较有用的度量项包括:

  1. 测试覆盖率(如果提交测试只覆盖了代码库的5%,那么这些测试发挥不了太大的作用);
  2. 重复代码的数量;
  3. 圈复杂度( cyclomatic complexity);
  4. 输入耦合度( afferent coupling)和输出耦合度( efferent coupling);
  5. 编译警告的数量;
  6. 代码风格。

ps:提交阶段尽量做校验是合理的,因为能够具体到某个特定的人。

自动化验收测试之门

提交阶段往往只是进行了单元测试,只能发现本项目的一些问题,而无法依赖之间的问题,例如依赖的第三方服务没有启动成功。

手工测试

验收测试之后一定会有一些手工的探索性测试、易用性测试和演示。在这个过程中,测试人员所扮演的角色并不是回归测试该系统,而是首先通过手工证明验收条件已被满足,从而确保这些验收测试的确是验证了系统行为。

非功能测试

几乎每个系统都有容量和安全性方面的要求,或者必须遵守服务水平协议等。通常应该用某些自动化测试衡量应用程序是否满足这些需求。可以在部署流水线中创建一个阶段,用于运行这些自动化的非功能测试。

ps:非功能测试涵盖范围太广了,在部署流水线上不一定合适。比如性能的测试,就需要一个单独的环境,也有可能需要造数据等,并且可能整个过程需要的时间比较长,因而在部署流水线上有这个东西,可能就太浪费时间了。

变更的撤销

传统上,人们对新版本的发布常常存在着恐惧心理,原因有两个。一是害怕引入问题,因为手工的软件发布过程很可能引入难以发现的人为错误,或者部署手册本身就隐藏着某个错误。二是担心由于发布过程中的一个问题或新版本的某个缺陷,使你原来承诺的发布失败。无论是哪种情况,你的唯一希望就是足够聪明且非常迅速地解决这个问题。
我们可以通过每天练习发布多次来证明自动化部署系统是可以工作的,这样就可以缓解第一种问题。对于第二个问题,可以准备一个撤销策略。最糟的情况也就是回滚到发布之前的状态,这样你就有足够的时间评估刚发现的问题,并找到一个合理的解决方案。

ps:想要回滚是挺难的,尤其是大系统,在涉及到多个组件之间的协作,单单回滚一个系统是解决不了问题的。因为回滚往往是下下策。

第5章 实现一个部署流水线

无论是从零创建新项目,还是想为已有的系统创建一个自动化的流水线,通常都应该使用增量方法来实现部署流水线。接下来,我们将描述如何从无到有,建立一个完整流水线的策略。一般来说,步骤是这样的:

  • 对价值流建模,并创建一个可工作的简单框架;
  • 将构建和部署流程自动化;
  • 将单元测试和代码分析自动化;
  • 将验收测试自动化;
  • 将发布自动化。

对价值流进行建模并创建简单的可工作框架

可以在同一组织中找个与你的项目相似的项目,思考它的价值流,也可以从最简单的价值流开始,即第一个阶段是提交阶段,用来构建应用程序并运行基本的度量和单元测试,第二个阶段用来运行验收测试,第三个阶段用来向类生产环境部署应用,以便用它来做演示。

如果是使用“最简单模型”,每当有人提交代码到版本控制系统时,就应该触发提交阶段。当提交阶段通过以后,验收测试阶段就应该被自动触发,并使用提交阶段刚刚创建的二进制包。为手工测试或发布应用而向类生产环境部署二进制包的阶段,都应该会要求你具有通过单击按钮来选择到底部署哪个版本的能力,而这种能力通常都需要授权。

构建和部署自动化

实现部署流水线的第一步是将构建和部署流程自动化。构建过程的输入是源代码,输出结果是二进制包。

自动化单元测试和代码分析

开发部署流水线的下一步就是实现全面的提交阶段,也就是运行单元测试、进行代码分析,并对每次提交都运行那些挑选出来的验收测试和集成测试。

自动化验收测试

一套好的自动化验收测试会帮助你追查随机问题和难以重现的问题,如竞争条件、死锁,以及资源争夺。这些问题在应用发布之后,就很难再被发现。

ps:自动化测试是最费时费力的

部署流水线的演进

我们发现,每个价值流图和流水线中几乎都有上面描述的步骤。通常这些是自动化的第一个目标。随着项目越来越复杂,价值流图也会演进。另外,对于流水线来说,还有两个常见的外延:组件和分支。大型应用程序最好由多个组件拼装而成。在这样的项目中,每个组件都应该有一个对应的“迷你流水线”,然后再用一个流水线把所有组件拼装在一起,并运行整个验收测试集(包括自动化的非功能测试),然后再部署到测试环境、试运行环境和生产环境中。

ps:多个组件之间的交互,有点像锐捷的极光部署,但是每次构建都需要选择每个组件的版本,这也是需要各个组件之间信息需要统一才行。不然组件之间接口的变化,将会导致打包之后的版本是有问题的。感觉最好是组件之间要有约束,每次发布都是统一发布。都使用最新的版本。

第6章 构建与部署的脚本化

核心思想将操作流程脚本化,使用现成的一些工具,比如maven、ant等的构建工具。

小贴士

总是使用相对路径

构建中最常见的错误就是默认使用绝对路径。这会让构建流程和某台特定机器的配置形成强依赖,从而很难被用于配置和维护其他服务器。
应该默认所有位置都使用相对路径。这样,构建的每个实例都是一个完整的自包含结构,你提交到版本控制库的镜像就会自然而然地确保将所有内容放在正确的位置上,并以其应有的方式运行。

消除手工步骤

文档常常存在错误或过时,所以在生产环境中部署时,经常会有大量的演练成本。每次部署都各不相同,因为某个缺陷的修复或小的系统改动,可能只需对系统的个别部分进行重新部署。因此,对于每次发布来说,这个部署过程都必须修订一下。前面部署中留下来的知识和物件无法重用。对于部署执行人来说,每次部署都是对其记忆力以及对系统理解程度的考验,并且基本上都会出错。

那么,我们什么时候应该考虑将流程自动化呢?最简单的回答就是:“当你需要做第二次的时候。”到第三次时就应该采取行动,通过自动化过程来完成这件工作了。这种细粒度的渐进方法,可以迅速建立起一个系统,将开发、构建、测试和部署过程中的可重复部分实现自动化。

从二进制包到版本控制库的内建可追溯性

能够确定“某个二进制包是由版本控制库中的哪个具体版本生成的”是非常必要的。假如在生产环境中出了问题,能够轻松确定机器上每个组件的版本号,以及它们的来源,你的生活会轻松很多。

不要把二进制包作为构建的一部分放到版本控制库中

取而代之的是,我们可以把二进制包和结果报告放在一个共享的文件系统中存储。如果你把它们弄丢了或者需要重新生成它们的话,最好是从源代码中重新构建一份。假如你无法根据源代码重新构建出一份一模一样的副本,这说明你的配置管理没达到标准,需要加以改进。
一般的经验法则是不要将构建、测试和部署过程中生成的任何产物提交到版本控制库中,而要将这些产物作为元数据,与触发该次构建的版本的标识关联在一起。

ps:放到版本管理里边可能也不合适,版本控制的体积会无形中变大了,可能下载同步就变慢了。应该是有单独的文件系统,用于放置这些东西。

“test”不应该让构建失败

在某些构建系统中,一旦某个任务失败,便默认令本次构建立即失败。也就是说,假如你有一个“test”任务,如果在其运行时,任何测试失败了,整个构建就将立即失败。通常来说,这种做法是不好的。相反,应该将当前失败的任务记录下来,然后继续构建流程的后续部分。最后,在过程结束时,如果发现有任意一个任务失败了,就退出并返回一个失败码。

ps:如果能够实现这种效果,是挺好的,但是如果业务之间有以来关系,也就不好实现。能够实现这种效果,应该能够节省很多时间。

用集成冒烟测试来限制应用程序

比如,可以在部署之前令部署脚本先检查一下是否被部署在了正确的机器上。对于测试和生产环境配置来说,这尤其重要。

ps:应用部署完成之后,跑最基础的用例,保证程序是没有问题的。

第7章 提交阶段

版本控制--》提交阶段(编译、单元测试、组装打包、代码分析)--》制品库(结果报告二进制包元数据)

当某人向版本控制库的主干上提交了一次变更后,持续集成服务器会发现这次变更,并将代码签出,执行一系列的任务,包括:

  • 编译(如果需要的话),并在集成后的源代码上运行提交测试;
  • 创建能部署在所有环境中的二进制包(如果使用需要编译的语言,则包括编译和组装);
  • 执行必要的分析,检查代码库的健康状况;
  • 创建部署流水线的后续阶段需要使用的其他产物(比如数据库迁移或测试数据)

ps:每次提交都构建,需要jenkins构建足够快,不然也是很耗费时间的事情

提交阶段的原则和实践

提供快速有用的反馈

提交失败要立即有反馈,通知对应提交人解决

何时令提交阶段失败

当单元测试覆盖率低于60%就令提交阶段失败,但是如果它高于60%,低于80%的话,就令提交阶段成功通过,但显示成黄色
当某次构建的编译警告的数量比前一次增多或者没有减少时,就让提交阶段失败(这就是“渐进式”实践)
如果重复代码的数量超出了某个事先约定的限制,或者有关代码质量的其他度量项不符合约束条件时,就令提交阶段失败,这是完全可以接受的

精心对待提交阶段

提交阶段中有构建用的脚本和运行单元测试、静态分析等的脚本。这些脚本需要小心维护,就像对待应用程序的其他部分一样。和其他所有软件系统一样如果构建脚本设计得很差,还没得到很好维护的话,那么保持它能够正常工作所需投入的精力会呈指数级增长。这相当于双重打击。一个较差的构建系统不但会把昂贵的开发资源从创造业务功能的工作中拖走,而且会令那些仍在创建业务功能的开发人员的工作效率降低。

让开发也拥有权限

如果真的只有那些专家才有权维护持续集成系统的话,那就是一种失败的管理方式。交付团队对提交阶段(也包括流水线基础设施的其他部分)拥有所有权是至关重要的,这与交付团队的工作和生产效率是紧密联系在一起的。如果你设置了人为障碍,使开发人员不能快速有效地作出修改,就会减缓他们的工作进程,并在其前进的道路上埋下地雷。

ps:这个观点不知道可行性如何,所有开发都能够处理,就得要制定其他的策略才行。

在超大项目团队中制定一个构建负责人

让某个(或多个)人扮演构建负责人的角色是必要的。他们不但要监督和指导对构建的维护,而且还要鼓励和加强构建纪律。如果构建失败,构建负责人要知会当事人并礼貌地(如果时间太长的话,不礼貌也没问题)提醒他们为团队修复失败的构建,否则就将他们的修改回滚。

ps:锐捷有类似的实现,比如负责审核代码并合入的人员,在发现失败时要制定对应人员处理。

避免使用数据库

首先,这种测试运行得非常慢。当想重复测试,或者连续运行几次相似的测试时,这种有状态的测试就是个障碍。其次,基础设施准备工作的复杂性令这种测试方法的建立和管理更加复杂。最后,如果从测试中很难消除数据库依赖的话,这也暗示着,你的代码在通过分层进行复杂性隔离方面做得不好。

ps:什么场景下需要依赖数据库,这个倒是没有碰到

在单元测试中避免异步

异步实现的单元测试,比如发送消息,等待消息相应,这样的测试会依赖其他的组件,并需要等待返回值,单元测试应该通过mock或者桩的方式解决。

我们建议尽量消除提交阶段测试中的异步测试。依赖于基础设施(比如消息机制或是数据库)的测试可以算做组件测试,而不是单元测试。

使用测试替身

在这种设计得比较好的模块化系统中,为了测试一个在关系网中心的某个类,可能需要对它周边的很多类进行冗长的设置。解决办法就是与其依赖类进行模拟交互。

ps:java可以使用jmock这样的工具。

时间的伪装

对于定时器任务,得要构建当前时间注入方式实现。要不然测试将不好覆盖

第8章 自动化验收测试

引言

部署流水线的验收测试阶段的工作流程
环境和应用程序配置信息--》验收测试阶段(配置环境、部署二进制文件、冒烟测试、验收测试)--》制品库
对于一个单独的验收测试,它的目的是验证一个用户故事或需求的验收条件是否被满足。验收条件有多种类型,如功能性验收条件和非功能性验收条件。非功能性验收条件:容量、性能、可修改性、可用性、安全性、易用性

单元测试的确是自动化测试策略的关键部分,但是,它通常并不足以使人们确信程序能够发布。而验收测试的目标就是要证明应用程序的确实现了客户想要的,而不是以编程人员所认为的正确方式来运行的。单元测试的目标就是要证明:应用程序的某个单一部分的确是按开发人
员的思路运行的,但这并不能断言它也就是用户想要的功能

为什么验收测试至关重要

通过合理地创建和维护自动验收测试套件,其成本就会远低于频繁执行手工验收和回归测试的成本,或者低于发布低质量软件带来的成本。我们还发现,自动化验收测试能捕获那些即使单元或组件测试特别全面也都无法捕获的一些问题。

大家不喜欢自动化验收测试的真正原因是认为它太昂贵了。然而,我们还是能够把它的成本降到投入产出比可接受程度的。

第9章 非功能需求的测试

非功能需求的测试

把非功能需求与功能需求区别对待,就很容易把它从项目计划中移除,或者不给予它们足够的分析。对于实现来说,非功能需求是很复杂的,因为它们通常对系统架构有很大的影响。比如,任何需要高性能的系统都不应该让一个请求横跨系统中的多个层。由于在交付过程的后期很难对系统架构进行修改,所以在项目一开始就要考虑非功能需求,这是至关重要的。这意味着需要做一些恰到好处的预分析,定为系统选择什么样的架构

非功能需求之间可能彼此排斥:对安全性要求极高的系统常常在易用性上做一些妥协,而非常灵活的系统经常在性能方面有所妥协。

ps:文中提到了很多非功能测试:扩展性测试、容量测试、性能测试等等,但是测试感觉可以通过其他专业的书籍了解。

容量测试系统的附加价值

  • 重现生产环境中发现的复杂缺陷。
  • 探测并调试内存泄漏。
  • 持久性( longevity)测试。
  • 评估垃圾回收( garbage collection)的影响。
  • 垃圾回收的调优。
  • 应用程序参数的调优。
  • 第三方应用程序配置的调优,比如操作系统、应用程序服务器和数据库配置。
  • 模拟非正常的、最糟糕情况的场景。
  • 评估一些复杂问题的不同解决方案。
  • 模拟集成失败的情况。
  • 度量应用程序在不同硬件配置下的可扩展性。
  • 与外部系统进行交互的负载测试,即使容量测试的初衷是与桩替身接口( stubbederface)打交道。
  • 复杂部署的回滚演练。
  • 有选择地使系统的部分或全部瘫痪,从而评估服务的优雅降级( gracefulradation)。
  • 在短期可用的生产硬件上执行真实世界的容量基准,以便能计算出长期且低配的容量测试环境中更准确的扩展因素。

第十章 应用程序的发布与部署

从理论上讲,软件发布到生产环境和部署到测试环境的差异应该被封装在一组配置文件中。当在生产环境部署时,应遵循与其他任何环境部署同样的过程。启动自动部署系统,将要部署的软件版本和目标环境的名称告诉它,并点击“开始”就行了。所有后续部署和发布都要使用同样的流程。

发布计划

通常来说,第一次发布风险最高,需要细致地做个计划。而这种计划活动的结果可能是产出一些文档、自动化脚本或其他形式的流程步骤( procedure),用来保证应用程序在生产环境上的部署过程具有可靠性和可重复性。除了在发布策略中的这些材料以外,还要包括以下内容。

  • 第一次部署应用程序时所需的步骤。
  • 作为部署过程的一部分,如何对应用程序以及它所使用的服务进行冒烟测试。
  • 如果部署出现问题,需要哪些步骤来撤销部署。
  • 对应用程序的状态进行备份和恢复的步骤是什么。
  • 在不破坏应用程序状态的前提下,升级应用程序所需要的步骤是什么。
  • 如果发布失败,重新启动或重新部署应用程序的步骤是什么。
  • 日志文件放在哪里,以及它包括什么样的信息描述。
  • 如何对应用程序进行监控。
  • 作为发布的一部分,对必要的数据进行迁移的步骤有哪些。
  • 前一次部署中存在问题的记录以及它们的解决方案是什么

应用程序的部署与升级

首次部署

虚拟化和chicken-counting (0, 1, many)是你的好朋友。利用虚拟化技术在一个物理机器上创建一个环境来模拟生产环境的某些重要特征,还是非常容易的。chickencounting意味着,假如生产环境里有250个Web服务器的话,用两个服务器就足以代表进程边界了。随着开发工作的进行,可在以后适当的时间再根据需要不断完善它。一般来说,类生产环境具有如下特点。

  • 它的操作系统应该与生产环境一致。
  • 其中安装的软件应该与生产环境一致,尤其不能在其上安装开发工具(比如编译器或IDE)

对发布过程进行建模并让构建晋级

构建版本在各种不同环境之间的晋级,然而我们还要考虑更多的细节。尤其重要的是注意以下内容。

  • 为了达到发布质量,一个构建版本要通过哪些测试阶段(例如,集成测试、 QA验收测试、用户验收测试、试运行以及生产环境)。
  • 每个阶段需要设置什么样的晋级门槛或需要什么样的签字许可。
  • 对于每个晋级门槛来说,谁有权批准让某个构建通过该阶段。

ps:也就是说生产环境不能随意发布,需要经过严格的流程处理。

在项目一开始,就应该准备好试运行环境。如果你已经为生产环境准备好了硬件,而这些硬件尚没有其他用途的话,那么在第一次发布之前就可以把它作为试运行环境。以下是项目开始时就需要计划的一些事。
q 确保生产环境、容量测试环境和试运行环境已准备好。尤其是在一个全新的项目上,在发布前的一段时间就准备好生产环境,并把它作为部署流水线的一部分向其进行部署。

  • 准备好一个自动化过程,对环境进行配置,包括网络配置、外部服务和基础设施。
  • 确保部署流程是经过充分冒烟测试的。
  • 度量应用程序的“预热”时长。如果应用程序使用了缓存,这一点就尤其重要了。将这也纳入到部署计划中。
  • 与外部系统进行测试集成。你肯定不想在第一次发布时才让应用程序与真实的外部系统集成。
  • 如果可能的话,在发布之前就把应用程序放在生产环境上部署好。如果“发布”能像重新配置一下路由器那样简单,让它直接指向生产环境,那就更好了。这种被称作蓝绿部署( blue-green deployment)的技术会在本章后面详细描述。
  • 如果可能的话,在把应用程序发布给所有人之前,先试着把它发布给一小撮用户群。这种技术叫做金丝雀发布,也会在本章后续部分描述。
  • 将每次已通过验收测试的变更版本部署在试运行环境中(尽管不必部署到生产环境)。

部署回滚与零停机发布

金丝雀发布和蓝绿部署能够在一定程度上实现零停机发布和回滚。
在开始讨论之前,先要声明两个重要的约束。首先是数据。如果发布流程会修改数据,回滚操作就比较困难。另一个是需要与其他系统集成。如果发布中涉及两个以上的系统(也称联合环境的发布, orchestrated releases),回滚流程也会变得比较复杂。

当制定发布回滚计划时,需要遵循两个通用原则。首先,在发布之前,确保生产系统的状态(包括数据库和保存在文件系统中的状态)已备份。其次,在每次发布之前都练习一下回滚计划,包括从备份中恢复或把数据库备份迁移回来,确保这个回滚计划可以正常工作。

通过重新部署原有的正常版本来进行回滚

这通常是最简单的回滚方法。如果你有自动化部署应用程序的流程,让应用程序恢复到良好状态的最简单方法就是从头开始把前一个没有问题的版本重新部署一遍。这包括重新配置运行环境,让它能够完全和从前一样。这也是能够从头开始重建环境如此重要的原因之一。为什么创建环境和部署要从头开始呢?有以下几个理由。

  • 如果还没有自动回滚流程,但是已有自动部署流程了,那么重新部署前一版本是一种可预知时长的操作,而且风险较低(因为重新部署相对更不容易出错)。
  • 在此之前,已经对这个操作做过数百次测试(希望如此)。另外,执行回滚的频率相对比较低,所以包含bug的可能性要大一些。

通过重新部署原有的正常版本来进行回滚,有如下一些缺点。

  • 尽管重新部署旧版本所需的时间固定,但并不是不需要时间。所以一定会有一段停机时间。
  • 更难做调试,找到问题原因。重新部署旧版本通常是覆盖那个新版本,所以也失去了找到问题原因的最佳机会。
  • 如果你在部署新版本前已经备份了数据库,那么在重新安装旧版本时把数据库备份文件恢复回来的话,那些在新版本运行时产生的数据就丢失了。这大部分时候都是个严重的问题

零停机发布

零停机发布(也称为热部署),是一种将用户从一个版本几乎瞬间转移到另一个版本上的方法。更重要的是,如果出了什么问题,它还要能在瞬间把用户从这个版本转回到原先的版本上。

蓝绿部署

做法是有两个相同的生产环境版本,一个叫做“蓝环境”,一个叫做“绿环境”。新版本发布到蓝环境中,然后让应用程序先热身一下(你想多长时间都行),这根本不会影响绿环境。我们可以在蓝环境上运行冒烟测试,来检查它是否可以正常工作。当一切准备就绪以后,向新版本迁移就非常简单了,只要修改一下路由配置,将用户从绿环境导向蓝环境即可。这样,蓝环境就成了生产环境。

在做这种蓝绿部署时,要小心管理数据库。通常来说,直接从绿数据库切换到蓝数据库是不可能的,因为如果数据库结构有变化的话,数据迁移要花一定的时间。

ps:相当于随时冗余一个完全相同的环境

金丝雀发布

先部署新版本到一部分服务器上,而此时用户不会用到这些服务器。然后就在这个新版本上做冒烟测试,如果必要,还可以做一些容量测试。最后,你再选择一部分用户,把他们引导到这个新版本上。有些公司会首先选择一些“超级用户”来使用这个新版本。
金丝雀发布有以下几个好处。

  • 非常容易回滚。只要不把用户引到这个有问题的版本上就行了。此时就可以来分析日志,查找问题。
  • 还可以将同一批用户引至新版本和旧版本上,从而作A/B测试。某些公司会度量新特性的使用率,如果用的人不多,就会废弃它。
  • 可以通过逐渐增加负载,慢慢地把更多的用户引到新版本,记录并衡量应用程序的响应时间、 CPU使用率、I/O、内存使用率以及日志中是否有异常报告这种方式,来检查一下应用程序是否满足容量需求。如果生产环境太大,无法创建一个与实际情况相差不多的容量测试环境,那么这对于容量测试来说,是一个风险相对比较低的办法

在生产环境中保留尽可能少的版本也是非常重要的,最好限制在两个版本之内。支持多个版本是非常痛苦的,所以要将金丝雀的数目减少到最低限度。

紧急修复

有时候并不真正需要紧急修复一个缺陷。你需要考虑多少人会受到缺陷的影响,这个缺陷是否经常发生,发生后对用户有多大的影响。如果缺陷只影响少数人,而且发生频率不高,影响较低,而部署一个新版本的风险相对较高的话,可能就没有必要做紧急修复了。当然,通过有效的配置管理和自动部署过程来减少部署风险还有一些争议。紧急修复的另一种做法是回滚到以前使用的好版本上,如前所述。
下面是处理生产环境中的缺陷时应该考虑的一些因素。

  • 别自己加班到深夜来做这事儿,应该与别人一起结对做这事儿。
  • 确保有一个已经测试过的紧急修复流程。
  • 对于应用程序的变更,避免绕过标准的流程,除非在极端情况下。
  • 确保在试运行环境上对紧急修复版本做过测试。
  • 有时候回滚比部署新的修复版本更划算。做一些分析工作,找到最好的解决方案。想一想,假如数据丢失了,或者面对集成或联合环境时,会发生什么事?

持续部署

持续部署并不是适合所有人。有时候,你并不想立即将最新版本发布到生产环境中。在某些公司,由于制度的约束,产品上线需要审批。产品公司通常还要对已发布出去的每个版本做技术支持。

持续部署迫使你做正确的事儿。没有完整的自动化构建、部署、测试和发布流程,你无法做到持续部署。没有全面且可靠的自动化测试集合,你也无法做到持续部署。没有在类生产环境中运行的系统测试,你同样做不到持续部署。

ps:持续部署估计大部分公司都很难实现的,因为一般都有一个严格的评审流程。

不要删除旧文件,而是移动到别的位置

当做部署操作时,确保已保留了旧版本的一份副本。然后,在部署新版本之前清除旧版本的所有文件。如果旧版本的某个文件被遗忘在了最新部署版本的环境当中,出现问题后就很难追查了。更糟糕的是,如果旧版本的管理接口页面还留在那儿,那么很可能引起错误数据。

为新部署预留预热期

系统需要运行一段时间,足以让应用服务器和数据库建立好它们的缓存,准备好所有的连接,并完成了“预热”。

第11章 基础设施和环境管理

文档与审计

任何人在任何时候想修改一下测试环境或生产环境,都必须提出申请,并被审批。

基础设施的建模和管理

基础设施的访问控制

控制包括以下三方面。

  • 在没有批准的情况下,不允许他人修改基础设施。
  • 制定一个对基础设施进行变更的自动化过程。
  • 对基础设施进行监控,一旦发生问题,能尽早发现。

锁定生产环境以避免非授权访问是非常必要的,其对象既包括组织之外的人,也包括组织之内的人,甚至是运维团队的员工。

对基础设施进行修改

高效的变更管理流程有如下几个关键特征。

  • 无论是更新防火墙规则,还是部署flagship服务的新版本,每个变更都应该走同样的变更管理流程。
  • 这个流程应该使用一个所有人都需要登录的ticketing系统来管理。这样就可以得到有用的度量数据,比如每个变化的平均周期时间。
  • 做过的变更应该详细清楚地记录到日志中,这样便于以后做审计
  • 能够看到对每个环境进行变更的历史,包括部署活动。
  • 想做修改的话,首先必须在一个类生产环境中测试通过,而且自动化测试也已经运行完成,以确保这次变更不会破坏该环境中的所有应用程序。
  • 对每次修改都应该进行版本控制,并通过自动化流程对基础设施进行变更。
  • 需要有一个测试来验证这次变更已经起作用了。

服务器的准备以及配置管理

服务器的准备

  • 完全手工过程。
  • 自动化的远程安装。
  • 虚拟化。一个服务器启动多个虚拟机

ps:没懂自动化的远程安装怎么实现的

服务器的持续管理

一旦这个系统准备好之后,就能用一个被集中版本控制的配置管理系统对基础设施中的所有测试环境和生产环境进行管理了。之后,就可得到如下收益。

  • 确保所有环境的一致性
  • 很容易准备一个与当前环境配置相同的新环境,比如创建一个与生产环境相同的试运行环境。
  • 如果某个机器出现硬件故障,可以用一个全自动化过程配置一个与旧机器完全相同的新机器。

第12章 数据管理

数据库脚本化

与系统中其他变更一样,作为构建、部署、测试和发布过程的一部分,任何对数据库的修改都应该通过自动化过程来管理。也就是说,数据库的初始化和所有的迁移都需要脚本化,并提交到版本控制库中。

增量式修改

对数据库进行版本控制

以自动化方式迁移数据最有效的机制是对数据库进行版本控制。首先,要在数据库中创建一个,用来保存含它的版本号。然后每次对数据库进行修改时,你需要创建两个脚本:一个是将数据库从版本x升级到版本x+1(升级脚本),一个是将数据库版本x+1降级到版本x(回滚脚本)。还需要有一个配置项来设置应用程序使用数据库的哪个具体版本(它也可以作为一个常量放在版本控制库中,每次有数据修改时更新一下)。

数据库回滚和无停机发布

  1. 一种方法是将那些不想丢失的数据库事务( transaction)缓存一下,并提供某种方法重新执行它们。当升级数据库和应用程序到新版本时,确保记录了每次在新版本上发生的事务。
  2. 如果使用蓝— 绿部署(参见第10章)的话,可以考虑第二种方法。提示一下,在蓝—绿部署环境中,应用程序会同时有新旧两个版本同时运行,一个运行于蓝环境,另一个在绿环境。“发布”只是将用户请求从旧版本转到新版本上,而“回滚”只是再把用户请求转到旧版本上而已。

将应用程序部署与数据库迁移解耦

ps:改动前要做全量备份才行。例如删除字段,回滚流程则会变得比较麻烦。很多场景,全量备份是做不到的,比如数据量太大,而且在数据库写入过程全量备份,可能造成脏数据。另外如果是发布了一段时间之后才发现升级有问题,那么就不是简单的回滚了,新增和修改的数据得要考虑一下如何处理。

第13章 组件和依赖管理

在开发一个大型软件系统时,常常能看到这三个维度同时出现。在这样的系统中,组件间会形成一种依赖关系,而且也会依赖于外部库( external library)。每个组件可能会有几个发布分支。在这些组件中找到各组件的某个好用的版本进行编译,并组成一个完整的系统是一个极具难度的过程,有点类似于一个叫做“打地鼠”的游戏——我们曾听说有个项目曾花了几个月来做这件事。只要你遇到过类似的情况,就应该开始通过部署流水线来完成这样的事情了。

保持软件可发布

在开发过程中,团队会不断地增加新特性,有时候还要做较大的架构改变。在这些活动期间,应用程序是不能发布的,尽管它能够成功通过持续集成的提交测试阶段。通常,在发布之前,团队会停止开发新功能,并进入一个只做缺陷修复的稳定期。当应用程序发布后,就会在版本控制中拉出一个发布分支,而新功能的开发仍会在主干上进行。可是,这个流程常常会导致两次发布的时间间隔是几个星期或几个月。而持续交付的目标是让应用程序总是保持在可发布状态。那么如何做到这一点呢?

一种方法是在版本控制库中创建分支,当工作完成后再合并,以便主干一直是可发布的(下一章将会详细讨论这种方法)。然而,我们认为这种方法只是一种次优选择,因为如果工作成果是在分支上,那么应用程序就不是持续集成的。相,我们提倡每个人都应该提交到主干。可是,怎么既能让每个人都在主干上开发,又让应用程序一直保持在可发布状态呢?

  • 将新功能隐蔽起来,直到它完成为止。
  • 将所有的变更都变成一系列的增量式小修改,而且每次小的修改都是可发布的。
  • 使用通过抽象来模拟分支( branch by abstraction)的方式对代码库进行大范围的
    变更。
  • 使用组件,根据不同部分修改的频率对应用程序进行解耦。

将新功能隐蔽起来,直到它完成为止

有一种解决方案,就是把新功能直接放进主干,但对用户不可见。例如,某网站提供了旅行服务。运维这个网站的公司想提供一种新的服务:酒店预订。为了做到这一点,先把它作为一个单独的组件来开发,通过一个单独的URI“ /hotel”来访问。

另一种让半成品组件可以发布而不让用户访问的方法是通过配置项开关来管理。比如,在一个富客户端应用中,可能有两个菜单,一个包含新功能,另一个不包含新功能。可以用一个配置项在两个菜单之间进行切换。

所有修改都是增量式的

为了能够将大块变更分解成一系列的小修改,分析工作就要扮演非常重要的角色了。首先需要用各种各样的方式将一个需求分解成较小的任务。然后将这些任务再划分成更小的增量修改。这种额外的分析工作常常会使修改的错误更少、目的性更强。当然如果修改是增量式的,也就可以“边走边评估”( take stock as you go along),并决定是否需要继续做和如何继续。

通过抽象来模拟分支

当应用程序的某个部分需要做改进,但却无法使用一系列小步增量开发时,就要按如下步骤这么做。
(1) 在需要修改的那部分系统代码上创建一个抽象层。
(2) 重构系统的其他部分,让它使用这个抽象层。
(3) 创建一种新的实现代码,在它完成之前不要将其作为产品代码的一部分。
(4) 更新抽象层,让它使用这个新的实现代码。
(5) 移除原来的实现代码。
(6) 如果不再需要抽象层了,就移除它。
“通过抽象来模拟分支”是一次性实现复杂修改或分支开发的替代方法。它让团队在持续集成的支撑下持续开发应用程序的同时替换其中的一大块代码,而且这一切都是在主干上完成的。

ps:感觉是在代码中增加了一层代码,相当于通过该代码控制走哪个实现类,但是这样子怎么进行测试?能够动态修改走哪个实现类的配置项?

依赖

库管理

在软件项目中,有两种适当的方法来管理库文件。一种是将它们提交到版本控制库中,另一种是显式地声明它们,并使用像Maven或Ivy这样的工具从因特网上或者(最好)从你所在组织的公共库中下载。

第14章 版本控制进阶

创建分支的原因

第一,为了发布应用程序的一个新版本,需要创建一个分支。
这使开发人员能够不断开发新功能,而不会影响稳定地对外发布版本。当发现bug时,首先在相应的对外发布分支上修复,然后再把补丁合并到主干上。而该发布分支从来不会合并回主干。第二,当需要调研一个新功能或做一次重构时——调研分支最终会被丢弃并且从来不会被合并回主干。第三,当需要对应用程序做比较大的修改,但又无法使用上一章所描述的办法来避免分支时,也可以使用生命周期较短的分支(其实,如果代码基结构良好的话,这种情况也很少见)。分支的唯一目的就是可以对代码进行增量式或“通过抽象来模拟分支”方式的修改。

分支、流和持续集成

使用分支和持续集成之间会有某种相互制约的关系。如果一个团队的不同成员在不同分支或流上工作的话,那么根据定义,他们就不是在做持续集成。让持续集成成为可能的一个最重要实践就是每个人每天至少向主干提交一次。因此,如果你每天将分支合并到主线一次(而不只是拉分支出去),那就没什么。如果你没这么做,你就没有做持续集成。

每个新特性,或者新的开发都切一个分支。这种策略的问题在于:这些分支会在很长时间内一直处于不可发布的状态。而且,这些分支通常对其他分支都有一些软依赖( softdependency)。在上面的例子中,每个分支都要从集成分支上将修复缺陷的代码拿过去,而且每个分支还要从性能调优的分支上将性能调优的代码拿过去。而应用程序的一个定制版本( custom version)还在开发中,且长时间处于不可部署的状态。

每个分支都进行构建:在这种模式下,新开发的代码总是被提交到主干上。只有在发布分支上修改缺陷时才需要合并,而且这个合并是从分支合并回主干。而只有非常严重的缺陷修复才会从主干合并到发布分支上。这种模式要好一些,因为代码一直处于可发布状态,所以也就更容易发布。分支越少,合并和跟踪分支的工作就越少。

ps:实际工作中,使用jenkins就可以做到对每个新建分支的实时构建与编译,就是对jenkins的要求会高一些。

posted on 2021-11-21 19:19  xiaoheike  阅读(366)  评论(0编辑  收藏  举报

导航