《持续交付:发布可靠软件的系统方法》- 读书笔记(五)

第二部分——部署流水线

第 5 章 部署流水线解析

5.1 引言

对于大多数项目来说,采纳持续集成实践是向高效率和高质量迈进的一大步。它保证那些创建大型复杂系统的团队具有高度的自信心和控制力。一旦代码提交引入了问题,持续集成就能为我们提供快速的反馈,从而确保我们作为一个团队所开发的软件是可以正常工作的。它主要关注于代码是否可以编译成功以及是否可通过单元测试和验收测试。但持续集成并不足以满足我们的需要。

持续集成的主要关注对象是开发团队。持续集成系统的输出通常作为手工测试流程和后续发布流程的输入。在软件的发布过程中,很多浪费来自于测试和运维环节。例如,我们常常看到:

  • 构建和运维团队的人员一直在等待说明文档或缺陷修复;
  • 测试人员等待“好的”版本构建出来;
  • 在新功能开发完成几周之后,开发团队才能收到缺陷报告;
  • 开发快完成时,才发现当前的软件架构无法满足该系统的一些非功能需求。

当然,我们能找到很多种能很快得到收益的方法,来渐进改善软件交付过程,比如教开发人员如何才能写出可以随时在生产环境上运行的软件,在类生产环境中进行持续集成,以及组建跨功能团队等。然而,尽管这种实践肯定会让情况得到改善,但它们无法帮助你洞悉哪里是交付流程的瓶颈,以及如何进行优化。

解决方案就是采取一种更完整的端到端的方法来交付软件。在前面几章中,我们已经解决了配置管理以及自动化大量构建、部署、测试和发布流程的很多问题。现在,我们通常能通过一键式方式把软件的某个版本部署好,甚至可以将其一键式部署到生产环境中,这样就建立了一个非常有效的反馈环——由于很容易将应用程序部署到测试环境中,所以团队可以同时得到软件功能和部署流程两个方面的快速反馈。因为部署流程(无论是在开发机器上部署,还是为最后发布而进行的部署)是自动化的,所以可以频繁且有规律地运行并被测试,从而降低发布风险,也降低了向开发团队传递有关部署流程的知识时的风险。

从精益的角度来看,我们实现了一个“拉式系统”(pull system),即测试团队只要自己单击按钮,就能将某个特定的软件版本部署到测试环境中。运维人员也可以通过单击一下按钮就把软件部署到试运行环境和生产环境中。在整个发布流程中,开发人员能看到每个目标环境上部署了哪个版本,发现了哪些问题。管理人员也很容易就能看到一些关键的度量指标,比如周期时间(cycle time),吞吐量(throughput)以及代码质量等。整个交付过程中的所有人都因此具有两种能力,即他能使用任何他想使用的东西,也能看到整个发布流程,从而可以改善反馈循环,识别、优化并解决瓶颈。这样就形成了一个更加快速且更加安全的交付流程。

实现端到端的自动化构建、部署、测试和发布流程会带来一些连锁反应,还会带来一些意料之外的收益。通过在很多项目里使用这种技术,我们找到了这些项目中各种部署流水线系统之间的共同点。而且通过这种抽象总结出来的一些通用模式在我们尝试过的项目中都获得了成功。这种抽象使我们在项目开始就能很快建立一个相当成熟且能快速运行的构建、测试和部署系统。在交付项目里,这种端到端的部署流水线系统使我们获得了一定程度的自由和灵活性,而这在几年前是根本无法想象的。我们确信,这种方法能让我们以更高的质量和相当低的成本与风险来创建、测试、部署复杂系统。

这正是部署流水线的功用。

5.2 什么是部署流水线

从某种抽象层次上讲,部署流水线是指软件从版本控制库到用户手中这一过程的自动化表现形式。对软件的每次变更都会经历一个复杂流程才能发布。这一流程包括构建软件,以及后续一系列不同阶段的测试与部署,而这些活动通常都需要多人或者多个团队之间的协作。部署流水线是对这一流程的建模,在持续集成和发布管理工具上,它体现为支持查看并控制整个流程,包括每次变更从被提交到版本控制库开始,直到通过各类测试和部署,再到发布给用户的过程。

因此,这个由部署流水线建模而成的流程(从代码提交到软件发布的这个流程)实际上就是“将客户或用户脑中的一个想法变成其手中真实可用的特性”这一过程的一部分,而整个流程(从概念到概念兑现)可以用一个价值流图来描述。关于创建新产品的一个抽象价值流图如图5-1所示。
在这里插入图片描述
这个价值流图讲述了一个故事。整个过程一共需要大约三个半月的时间,其中真正的工作时间只有大约两个半月,其余时间都是从概念到概念兑现整个流程中各阶段之间的等待时间。例如,在开发完成首次要发布的版本与测试开始之间有五天的等待时间。这个时间有可能是将应用部署到某个类生产环境上所需的时间。顺便说一句,图中故意没有表明该产品是否为迭代开发。如果是迭代开发流程,开发阶段本身就会包含几个迭代,而每个迭代都包括测试和演示。而且,从发现到发布这个过程也会被重复很多次。

本书中,我们仅讨论从开发到发布的价值流,也就是图5-1中的阴影部分。这部分价值流的一个关键不同点在于会有很多次构建通过这一流程走向最后的发布。要理解部署流水线以及代码变更在其上流动的方法,是把它看成一个序列图,如图5-2所示。
在这里插入图片描述
请注意,流水线的输入是版本控制中的某个具体版本。每次变更都会生成一次构建,这个构建像神话中的英雄一样,闯过一系列的测试,希望成为一个能到达生成环境中的发布版本。在这一系列的测试阶段中,每个阶段都从不同的角度评估这个构建版本,且和持续集成一样,它的起点是向版本控制库的每一次提交。

随着某个构建逐步通过每个测试阶段,我们对它的信心也在不断提高。当然,我们在每个阶段上花在环境方面的资源也在不断增加,即越往后的阶段,其环境与生产环境越相似,其目的就是在这个过程中尽早发现那些不满足发布条件的构建版本,并尽快将失败根源反馈给团队。一般来说,只要某个构建使无论是这一流程中的哪个阶段失败了,它都不会进入下一个阶段。这在图5-3中有所反映。
在这里插入图片描述
使用这种模式的话,有些非常重要的积极影响。

  • 首先,它可以有效地阻止那些没有经过充分测试或不满足功能需求的版本进入生产环境,也能避免回归缺陷,尤其是对于那些需要紧急修复并部署到生产环境的情况(因为和其他变更一样,这种紧急修复版本也需要走同样的流程)。根据我们的经验,最新发布的软件由于系统组件和其环境之间的未预期交互导致出现故障的事情是很常见的,比如使用了新的网络拓扑结构,或者生产环境的服务器在配置方面有些许不同。部署流水线的纪律会缓解这种现象。
  • 其次,当部署和产品发布都被自动化之后,这些活动就变成快速、可重复且可靠的了。一旦被自动化,发布工作会变得非常容易,以至于会变成一件“平常”事,即只要你愿意,就可以做频繁发布。另外,如果支持自动安全回滚,发布风险也会大大降低,那么频繁发布就更不成问题了。一旦具有这种能力,发布就根本不会有什么风险了。最不济也就是引入一个严重缺陷,可这时只要回滚到之前没有缺陷的那个版本,然后在线下修复这个缺陷就可以了,没什么大不了的,详见第10章。

为了达到这种令人羡慕的状态,我们必须把那些用于证明某些版本满足业务要求的测试集合进行自动化。而且,我们还要把测试环境、试运行环境和生产环境上的部署过程自动化,这样可以避免那些手工密集型的易出错的步骤。对于很多系统来说,可能还需要其他形式的测试或者阶段,但对所有项目有一些阶段是共同具有的。

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

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

最基本的部署流水线
图5-4中显示了一个典型的部署流水线,体现了这种方法的本质。当然,一个真正的流水线应该反映真实的软件交付流程。
在这里插入图片描述
这个流程的起点是开发人员向版本控制库提交代码。此时,持续集成管理系统对这次提交作出响应,触发该流水线的一个实例。

  • 第一个(提交)阶段会编译代码,运行单元测试,执行代码分析,创建软件二进制包。如果所有的单元测试都通过了,并且代码符合编码标准,就将可执行代码打包成可执行文件,并放到一个制品库(artifact repository)中。时新的持续集成服务器都提供保存这种过程产物的功能,并让用户和流水线的后续阶段能以某种非常简便的方式获取并使用。另外,还有很多像NexusArtifactory这样的工具可帮助管理这类过程产物。在提交阶段,你也许还会执行另外一些任务,比如为验收测试准备测试数据库。时新的持续集成服务器都支持通过构建网格并行执行这些任务。
  • 第二个阶段通常由运行时间较长的自动化验收测试组成。因此,持续集成服务器最好支持将测试分成多组的做法,以便在构建网络中并行执行任务,这样会提高执行效率,使你更快地得到反馈(通常要在一两个小时之内返回结果)。这个阶段应该是流水线中第一个阶段成功完成以后自动触发的。
    在此之后,部署流水线可能会有分支出现,这样就可以将该构建版本独立部署到多个不同的环境中,比如部署到用户验收测试环境、容量测试环境和生产环境。通常情况下,我们并不需要在验收测试阶段成功之后直接自动触发这些阶段。相反,我们希望让测试人员或运维人员可以做到自服务,即自己手工选择需要的某个版本,并将其部署到相应的环境中。
    要将同样的原则应用于后续阶段,仅仅有一点不同,即不同阶段的使用者拥有各自的环境权限,所以只有那些具有相应权限的人才能通过自服务方式部署该应用到各自的环境中。比如,运维团队可能希望自身是唯一有权在对生产环境进行部署的部署工单上签字的一方。

最后,一定要记住,我们所做的这一切都是为了尽快得到反馈。为了加速这个反馈循环,就必须能够看到每个环境中都部署了哪个版本,每个构建版本在流水线中处于哪个阶段。图5-5(产品Go的截屏)展示了这个实践是什么样子的。
在这里插入图片描述
你可能已经注意到,每次提交都列在了页面的一侧,并显示出了每次提交分别走到了流水线中的哪个阶段,以及相应的每个阶段是否成功了。要能够将某次代码提交、构建版本与其在部署流水线上通过了哪些阶段关联在一起,这一点是非常必要的。因为这样你就能立刻发现是哪次代码提交造成了本次验收测试的失败。

5.3 部署流水线的相关实践

接下来,我们将讨论部署流水线中每个阶段的细节。在开始之前,为了能够获得该方法带来的好处,你需要遵循一些实践。

5.3.1 只生成一次二进制包

方便起见,我们将所有可执行代码的集合称作二进制包,例如Jar文件、.NET 程序集和.so文件。有时候代码根本不需要编译,那么这种情况下,二进制包就是指所有源文件的集合。

很多构建系统将版本控制库中的源代码作为多个步骤中最权威的源,不同上下文中会重复编译这个源,比如在提交时、做验收测试时或做容量测试时。而且,在每个不同的环境上部署时都要重新编译一次。但是,对于同一份源代码,每次都重新编译的话,会引入“编译结果不一致”的风险。在后续阶段里,其编译器的版本可能与提交阶段所用版本不一致。对于第三方库,你可能会不小心使用了本未打算使用的版本。甚至编译器的配置都会对应用程序的行为产生影响。我们曾遇到过由于上述原因导致在生产环境中出现问题的情景。

一种相关的反模式就是一直使用源代码,而不是二进制包。

这种反模式违反了两个重要原则。

  • 第一个原则就是“保证部署流水线的高效性,使团队尽早得到反馈”。重复编译违反了这一原则,因为编译需要花时间,在大型软件系统中进行的编译尤其如此。
  • 第二原则就是“始终在已知可靠的基础上进行构建”。被部署到生产环境中的二进制包应该与通过前面验收测试流程的二进制包是完全一样的。在很多实际使用的流水线里,每次生成二进制包时,都会存储其散列,并在后续每个阶段中利用这个散列对二进制包进行验证。

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

5.3.2 对不同环境采用同一部署方式

为了确保构建和部署流程被有效测试,在各种环境中使用相同流程对软件进行部署是非常必要的,这些环境即包括开发人员或分析人员的工作站,也包括测试环境和生产环境。显然,部署风险与部署频率成反比。部署频率最低的环境(生产环境)却是最重要的。因此,只有在很多环境中对部署过程测试过数百次以后,我们才能消除那些由于部署脚本错误而导致的问题。

只要把那些与特定环境相关的特定配置分开放置就行了。一种方法是使用属性文件保存配置信息,比如分别为每个环境保存一个属性文件,并将其放在版本控制库中。在部署时,通过本地服务器的主机名来查找正确的配置,而如果是在有多台服务器的环境中,可以将环境变量提供给部署脚本使用。当然还有一些其他方法提供部署时的配置信息,比如将其放在一个目录服务中(LDAP或ActiveDirectory),也可以将其放在数据库中,通过像ESCAPE这样的工具来访问它。

在同一个源(一个版本控制库、一个目录服务,或一个数据库)中找到所有环境中运行的所有应用程序的配置信息是完全可行的。

如果你所在的公司里,管理生产环境的团队与负责开发和测试的团队不是同一个团队,那么这两个团队就要在一起工作,确保自动化部署过程在所有环境中都是有效的(包括开发环境在内)。能够使用相同的脚本向开发环境和生产环境部署,是避免“它在我的机器上可以工作”病症的法宝

如果对于不同的环境,其部署脚本也不相同的话,你就无法知道某个测试过的脚本是否在上线部署时还能正常工作。相反,如果使用同一个脚本在所有的环境上进行部署,那么当在某个环境上部署失败时,就可以确定其原因一定来自以下三个方面:

  • 与该环境相关的配置文件中,某项配置有问题;
  • 基础设施或应用程序所依赖的某个服务有问题;
  • 环境本身的配置有问题。

那么到底是哪个原因呢?这是接下来的两个实践需要解决的问题。

5.3.3 对部署进行冒烟测试

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

一旦有了单元测试之后,这种冒烟测试(部署测试)可能就是你要马上着手做的最重要测试了,甚至可以说是最最重要的测试。因为它可以让你对“应用程序可以运行起来”建立信心。如果应用程序不能运行,这个冒烟测试应该能够告诉你一些最基本的诊断提示,比如应用程序无法运行是否是因为其依赖的外部服务无法正常工作。

5.3.4 向生产环境的副本中部署

很多团队实际部署应用上线时可能遇到的另一个主要问题是,生产环境与他们的开发环境或测试环境有非常大的差异。为了对系统上线充满信心,你要尽可能在与生产环境相似的环境中进行测试和持续集成。

理想情况下,如果生产环境非常简单,或者有足够多的预算,我们完全可以建立与生产环境一模一样的环境,用于运行手工测试或自动化测试。另外,要想确保所有的环境都一样,需要有很多纪律保障良好的配置管理实践。你要确保:

  • 基础设施是相同的,比如网络拓扑和防火墙的配置等;
  • 操作系统的配置(包括补丁版本)都是相同的;
  • 应用程序所用的软件栈是相同的;
  • 应用程序的数据处于一个已知且有效的状态。系统升级过程中需要进行的数据迁移是部署活动的一个痛点,我们将在第12章讲这个问题。

你可以使用像磁盘镜像或虚拟化技术这类实践,以及Puppet、InstallShield这类工具和某个版本控制系统共同管理环境配置。我们将在第11章详细讨论这个问题。

5.3.5 每次变更都要立即在流水线中传递

在持续集成出现之前,很多项目都有一个各阶段的执行时间表,比如每小时构建一次,每天晚上运行一次验收测试,每个周末运行一次容量测试。部署流水线则使用了不同的方式:每次提交都要触发第一个阶段的执行,后续阶段在第一个阶段成功结束后,立即被触发。当然,假如某些阶段需要花较长的时间,而开发人员(尤其是在大型团队中)的提交又非常频繁,就很难做到这一点了。图5-6中就显示了这样做的问题。
在这里插入图片描述
在本例中,某人将代码提交到版本控制库,生成了版本1,并且触发了流水线的第一个阶段(构建及单元测试)。这一阶段通过之后,紧接着触发了第二个阶段——自动化验收测试。此时另一个人提交了另一个修改,在版本库中生成了版本2,流水线中的第一个阶段(构建及单元测试)被再次触发。然而,即便这次构建也通过了,它仍无法触发下一个自动化验收测试,因为有一个自动化验收测试正在运行。与此同时,又有两个新的版本被提交。可是持续集成系统不能同时构建它们两个,如果遵循这个原则,而开发人员继续以同样的速率提交代码的话,构建就会越来越落后于开发人员的开发速度。

另一种构建策略是,一旦代码构建和单元测试结束,持续集成系统就去检查版本库中是否有新的提交。如果有的话,就将最近还没有构建过的所有变更全部拿来进行构建,即对版本4进行构建。假设这次构建和单元测试失败了,那么构建系统是无法知道究竟是哪个版本(版本3还是版本4)引起的,但开发人员自己可以很容易发现问题在哪儿。有些持续集成系统可以让你执行某个特定版本的构建,而无需按顺序执行。假如持续集成服务器有这种功能的话,开发人员就可以运行一次对版本3的构建和单元测试,看版本3是否能够通过测试,这样就可以弄清楚到底是哪个版本引入了问题。无论哪种方法,开发人员会提交版本5,来修复这个构建。这样,当验收测试结束以后,持续集成系统的调度程序发现版本5已经通过了第一阶段的测试,就会直接触发针对版本5的验收测试。

这种聪明的调度方法对于实现部署流水线来说是非常关键的。一定要确保持续集成服务器支持这种调度方式(事实上,很多持续集成服务器都支持这种调度方式),而且要确保每次变更都能立即在流水线中传递,这样就不用按固定的时间表来执行不同的阶段了。

目前,这些策略只能用于那些完全自动化的阶段,比如包含自动化测试的阶段,而流水线中后续的那些为手工测试环境执行部署的阶段就要按需激活

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

就像我们在3.2节中所说的,为了达到本书所描述的目标(迅速、可重复且可靠的发布),对于团队来说,最重要的是要接受这样的思想:每次提交代码到版本控制系统中后,都能够构建成功并通过所有的测试。对于整个部署流水线来说,都适用这一要求。假如在某个环境上的某次部署失败了,整个团队就要对这次失败负责,应该停下手头的工作,把它修复后再做其他事情。

5.4 提交阶段

每次提交都生成部署流水线的一个新实例。如果提交阶段的测试通过了,这个版本就被视为一个候选发布版本。部署流水线中第一个阶段的目标就是消除那些不适合生产环境的构建,并尽早给团队一个信号——“应用程序出错了”。我们不想在那些明显有问题的版本上花时间和精力,所以当开发人员提交变更到版本控制系统后,我们希望尽快地评估一下这个最新版本。提交者要一直等到构建结果,然后才能做下一项工作。

在提交阶段,我们需要做以下几件事。这些任务通常作为一个工作集合运行在构建网格上(大多数持续集成服务器都提供类似功能),这样,提交阶段就能够在一个可接受的时间之内完成(最好在五分钟之内完成,最多不能超过十分钟)。一般来说,提交阶段包含以下步骤:

  • 编译代码(如果所用开发语言需要的话);
  • 运行一套提交测试;
  • 为后续阶段创建二进制包;
  • 执行代码分析来检查代码的健康状况;
  • 为后续阶段做准备工作,比如准备一下后续测试所用的数据库。

测试非功能特性(比如容量)可能比较困难,但仍旧可以通过一些分析工具,收集一些关于当前代码库的测试覆盖率、可维护性以及安全漏洞方面的信息。为这些度量项设定一个阈值,并像对待测试一样,一旦不满足阈值条件,就让提交阶段失败。比较有用的度量项包括:

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

如果前面这些任务都成功了,提交阶段的最后一步就是生成二进制包,用于后续阶段的部署。当然,只有这步也成功了,提交阶段才能算成功。把生成可执行代码作为成功的验收条件,是确保构建流程本身也能够被持续集成系统不断评估和检查的简单方法。

提交阶段最佳实践
在第3章中所描述的实践大多数都适用于提交阶段。开发人员需要一直等到部署流水线的提交阶段成功完成。如果它失败了,开发人员要么快速修复问题,要么将刚提交的代码回滚。在理想情况下(无限的处理能力和无限的网络带宽),我们希望开发人员能够一直等到所有测试(甚至是手工测试)全部通过,这样一旦出现问题,就可以马上修复。然而,这并不现实,因为部署流水线的后续阶段(自动化验收测试、容量测试和手工验收测试)都需要相对较长的时间。这也是规范测试流程的一个理由,因为当缺陷还比较容易修复时,尽快得到反馈是非常重要的,而不应花更大的代价得到全面的反馈。

5.5 自动化验收测试之门

全面的提交测试套件对于发现许多种错误来说,是非常优秀的试金石。然而,有很多类型的错误是它无法捕获的。在提交测试集合中,大部分是单元测试,而单元测试与底层的API是紧耦合的,以至于开发人员难免落入一个陷阱,即“用某种特殊方式来证明解决方案是正确的”,而不是断言它解决了某个具体问题。

每次提交后就立即运行提交测试的意义在于,它能为最新的一次构建或程序中可能存在的一些较小的代码问题提供及时反馈。然而,如果没有在类生产环境上执行验收测试,我们就根本不知道该应用程序是否符合了客户规范,也不知道它在现实世界中是否能够部署并运行。如果想在这些方面得到及时反馈的话,就必须在持续集成流程中引入更多测试并不断对系统各个方面进行演练。

这个自动化验收测试关卡是识别候选发布版本过程中第二个重要的里程碑。部署流水线只允许后续阶段(比如需要手工干预的手工部署阶段)获取那些已通过自动化验收测试的构建版本。我们当然可以不遵循这样的机制,但可能会导致大量时间和精力的消耗。如果能把这些时间和精力花在修复那些已被部署流水线发现的问题上,花在以受控的可重复方式进行的部署工作上,不是更好吗?在部署流水线的帮助下,我们更容易做正确的事情。

因此,如果一个候选版本不能满足所有的验收条件,就根本不会被交给用户

自动化验收测试最佳实践
实际上,就像整个团队负责流水线的每一个阶段一样,整个团队都是验收测试的所有者。如果验收测试失败了,整个团队都要停下来,马上修复它。

这一实践的一个重要推论是,开发人员必须能在自己的开发环境中运行自动化验收测试。这样,开发人员在发现验收测试失败后,就很容易在自己的机器上修复它,然后在本地再次运行验收测试来验证修复。对于这个实践来说,最常遇到的障碍是没有足够多的测试软件授权,应用程序的架构不允许将其部署到开发环境中,以至于无法运行验收测试。如果你的自动化验收测试策略是为长远打算的话,就应该尽早清除这类障碍。

5.6 后续的测试阶段

验收测试阶段是整个遴选候选发布版本过程中的一个重要里程碑。一旦这个阶段结束了,这个候选版本就会受到开发人员之外更多人的广泛关注。

对于最简单的部署流水线来说,至少就系统的自动化测试而言,一个构建版本通过了验收测试就能够发布给用户了。如果某版本在验收测试阶段失败了,根据定义,它是不能发布的。

5.6.1 手工测试

在迭代开发过程中,验收测试之后一定会有一些手工的探索性测试、易用性测试和演示。在此之前,开发人员可能已经向分析师和测试人员演示了应用程序的功能,但一定是在自动化测试通过之后。在这个过程中,测试人员所扮演的角色并不是回归测试该系统,而是首先通过手工证明验收条件已被满足,从而确保这些验收测试的确是验证了系统行为。

之后,测试人员会做一些机器不太擅长而人比较擅长的测试。他们做探索性测试、易用性测试,在不同平台上测试程序的界面是否正确,并着眼于一些不可控制的最坏情况进行测试。自动化验收测试使测试人员节省出更多的时间做那些高价值的活动,而不是测试脚本的人力执行器。

5.6.2 非功能测试

每个系统都有很多非功能需求。比如,几乎每个系统都有容量和安全性方面的要求,或者必须遵守服务水平协议等。通常应该用某些自动化测试衡量应用程序是否满足这些需求。如何能够做到这一点呢?请参见第9章。对于某些系统,并不需要连续不断地做非功能需求测试。根据我们的经验,如果需要的话,完全可以在部署流水线中创建一个阶段,用于运行这些自动化的非功能测试。

在定义部署流水线结构时,必须回答一个问题,即容量测试阶段的结果是可以作为一个门槛,还是需要由人来决定?对于高性能应用来说,可以在验收测试阶段通过之后,就运行容量测试,作为该版本整个自动化测试的输出结果。如果这个版本不能通过容量测试,就不能把它看成是可部署的版本。

然而,对于很多应用程序来说,判定“什么是可接受的”更加具有主观性。通常根据实际容量测试阶段的结果,由人来判定该版本是否可以作为候选版本来部署会更有意义

5.7 发布准备

每次向生产环境发布时都有业务风险。一旦在发布时发生严重问题,可能最好的结果就是推迟部署有价值的新功能,而最糟糕的结果就是出了问题却没有合适的撤销计划,这可能导致关键业务无法运行,因为新版本中已经替换了原有版本的关键功能。

缓解这类风险非常简单,只要把这个发布环节视为部署流水线的一个自然结果就行。实际上,我们只需要:

  • 让参与项目交付过程的人共同创建并维护一个发布计划(包括开发人员和测试人员,以及运维人员,基础设施和支持人员);
  • 通过尽可能多的自动化过程最小化人为错误发生的可能性,并从最容易出错的环节开始实现自动化;
  • 在类生产环境中经常做发布流程演练,这样就可以对这个流程及其所使用的技术进行调试;
  • 如果事情并没有按计划执行,要有撤销某次发布的能力;
  • 作为升级和撤销过程的一部分,制定配置迁移和数据迁移的策略。

我们的目标是实现一个完全自动化的发布过程。发布就应该简单到这种程度,即只要选择一个需要发布的版本,单击一下按钮就万事大吉了。撤销也应该同样简单。

5.7.1 自动部署与发布

对生产环境的控制权越小,遇到意外情况的可能性就越大。因此,无论何时发布软件系统,我们都希望有完全的控制权。然而,这里至少有两方面的约束。

  • 首先,对于很多应用程序来说,你根本不能完全控制应用程序所在的运行环境。对于由用户自行安装的软件(比如游戏或者办公软件)来说,这一点是必然的。通常解决这个问题的办法就是选择一些具有代表性的目标环境,并分别在这些样本环境上执行自动化验收测试套件。这样就能通过收集结果数据发现哪些测试在哪些平台上无法正常运行了。
  • 第二个约束就是,人们通常认为为了达到完全控制环境所付出的成本会高于因此得到的收益。然而,事实常常恰好相反。生产环境中的大多数问题往往是由不充分的控制导致的。正如我们在第11章中所讲的,生产环境应该是完全受控的,即对生产环境的任何修改都应该通过自动化过程来完成。这不仅包括应用程序的部署,还包括对配置、软件栈、网络拓扑以及状态的所有修改。只有在这种方式下,我们才可能对它们进行可靠地审计和问题诊断,并在可预计的时间内修复它们。随着系统复杂性的增加,不同类型服务器的增多,以及不断提高的性能需求,我们就更需要这种程度的控制力。

管理生产环境的流程也应该用于测试环境,比如试运行环境、集成环境等。通过这种方式,就可以利用自动化变更管理系统来为手工测试环境创建一个完全一致的配置信息。根据容量测试的结果对配置进行不断的评估和调整,就会得到一个非常完美的配置。当满意后,就能在这种可预测且可靠的方式下,把这份配置放在每个需要这种配置的服务器上,也包括生产环境上的服务器。环境的所有方面都应该以这种方式来管理,包括中间件(如数据库、Web服务器、消息代理和应用服务器等)。每个配置都能够被调整,并把可选设置加到配置基线中。

通过自动化的环境准备和管理、最佳的配置管理实践以及虚拟化技术(如果适用的话),环境准备和维护的成本会显著降低。一旦环境配置被正确地管理起来了,就可以部署应用程序了。尽管很多实现细节更多地依赖于系统所使用的技术,但步骤基本上是相似的。这种方法与我们用来创建构建脚本和部署脚本,以及监控流程的方法相似。创建构建脚本与部署脚本的方法将在第6章中加以讨论。

5.7.2 变更的撤销

传统上,人们对新版本的发布常常存在着恐惧心理,原因有两个。一是害怕引入问题,因为手工的软件发布过程很可能引入难以发现的人为错误,或者部署手册本身就隐藏着某个错误。二是担心由于发布过程中的一个问题或新版本的某个缺陷,使你原来承诺的发布失败。无论是哪种情况,你的唯一希望就是足够聪明且非常迅速地解决这个问题。

我们可以通过每天练习发布多次来证明自动化部署系统是可以工作的,这样就可以缓解第一种问题。对于第二个问题,可以准备一个撤销策略。最糟的情况也就是回滚到发布之前的状态,这样你就有足够的时间评估刚发现的问题,并找到一个合理的解决方案。

对于很简单的应用程序来说,这是可以做到的(忽略数据和配置信息的迁移),只要把每个版本都放在一个单独的目标中,再使用符号链接指向当前版本就行了。最复杂的情况就是在部署和撤销中涉及生产数据的迁移。

**撤销流程绝不应该与部署流程、增量部署流程或回滚流程有什么不同。然而,这些流程可能很少被测试,所以也就不可靠。**而且,这些流程也很少基于某个已知良好的版本基线,所以也就比较脆弱。因此,一定要让旧版本保持同步运行一段时间,或者在必要时完全重新部署某个已知良好的旧版本。

5.7.3 在成功的基础上构建

当一个候选发布版本能够部署到生产环境时,我们就确信:

  • 代码可以编译;
  • 代码能够按开发人员的预期运行,因为它通过了单元测试;
  • 系统能够满足分析人员或用户预期,因为它通过了所有的验收测试;
  • 基础设施的配置和基线环境被恰当地管理了,因为应用程序在模拟的生产环境上通过了测试;
  • 系统所有的正确组件都就绪了,因为它是可以部署的;
  • 部署脚本也是可以工作的,因为在该版本到这一阶段之前,部署脚本至少在开发环境中用过一次,在验收测试阶段用过一次,在测试环境中用过一次;
  • 我们需要部署的所有内容都在版本控制库中,而且不需要手工干预,因为我们已经部署这个系统好几次了。

这种“在成功的基础上构建”的方法, 完全符合我们常挂在嘴边的口头禅“尽快让这个流程或其任何环节失败”,这在任何层次都是有用的。

5.8 实现一个部署流水线

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

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

正如本章开始所描述的,第一步就是画出从提交到发布整个过程的价值流图。如果项目已经建好并开始运行,你在半个小时内就能画完。然后和参与其中的每个人聊一下,记录下流程中的每个步骤,包括对经历时间(elapsed time)和增值时间(value-added time)的最佳估计值。如果是还没有启动的新项目,就要先设计一个合适的价值流,可以在同一组织中找个与你的项目相似的项目,思考它的价值流,也可以从最简单的价值流开始,即第一个阶段是提交阶段,用来构建应用程序并运行基本的度量和单元测试,第二个阶段用来运行验收测试,第三个阶段用来向类生产环境部署应用,以便用它来做演示。

一旦有了价值流图,就可以用持续集成和发布管理工具对流程建模了。如果所用工具不支持直接对价值流建模的话,可以使用“项目间依赖”来模拟它。首先,这些项目应该什么也不做,而只是作为可以被依次触发的占位符。如果是使用“最简单模型”,每当有人提交代码到版本控制系统时,就应该触发提交阶段。当提交阶段通过以后,验收测试阶段就应该被自动触发,并使用提交阶段刚刚创建的二进制包。为手工
测试或发布应用而向类生产环境部署二进制包的阶段,都应该会要求你具有通过单击按钮来选择到底部署哪个版本的能力,而这种能力通常都需要授权。

接下来,就让这些占位符真正做些事情。假如项目已经全面展开,那么把已有的构建、测试和部署脚本放进去就可以了。如果还没有的话,就先创建一个“从头到尾的轮廓”,即用最少的工作量将所有的关键元素准备就绪。首先是提交阶段。如果还没有开始写代码和单元测试的话,就写一个最简单的“Hello world”示例(如果是 Web 应用,写个 HTML 页 面 就 行 ),再写个单元测试,而这个测试只是“assert(true)”。其次,完成部署,比如在IIS上建立一个虚拟目标,将你的网页放进去。最后,进行验收测试。注意,要在完成部署阶段后再做验收测试。因为只有部署应用后才能做验收测试。对于Web应用,验收测试可以使用WebDriver或Sahi来验证网页中是否包括文字“Hello world”。

对于一个新项目,上述内容都应在开发工作正式开始之前完成,如果是迭代开发的话,这是迭代0(iteration zero)中的工作内容。另外,系统管理员或运维人员也应该参与到建立演示用的类生产环境和开发部署脚本的活动中。在下面的几节中,我们会更详细地讲述如何创建简单的可工作框架,并随着项目的进行而不断开发。

5.8.2 构建和部署过程的自动化

实现部署流水线的第一步是将构建和部署流程自动化。构建过程的输入是源代码,输出结果是二进制包。“二进制包”是我们故意含糊使用的一个词,因为由于所用开发技术的不同,构建过程的输出也不相同。在这里,二进制包的关键特征是“你能将它复制到一台新机器上(上面没有IDE等开发工具集),只要环境配置正确,且又有应用在该环境中所需的正确配置信息,它就可以启动并运行了,而不必依赖于在这台机器上安装的开发工具链的任何部分。

每当有人提交后,持续集成服务器就应执行构建——使用3.2节所列出的某个工具。持续集成服务器应该监视版本控制系统,每当发现有新提交的代码时,就签出或更新源代码,运行自动化构建流程,并将生成的二进制包放在文件系统的某个地方,使整个团队都能通过持续集成服务器的用户界面获取。

一旦持续构建流程建立并运行起来了,接下来就要做自动化部署了。首先,要找到能够部署应用程序的机器。对于刚启动的新项目,用持续集成服务器所在的机器也行。如果项目已比较成熟,可能就需要找几台专用机器了。这些环境可以称作试运行环境或者用户验收测试(UAT)环境(这在各组织中的叫法不同)。无论怎样,这个环境应该与生产环境相似——如第10章所述,而且它的准备和维护工作都要用全部自动化的流程完成——如第11章所述。

部署自动化的几种常见方法在第6章有详细描述。部署活动可能包含:(1) 为应用程序打包,而如果应用程序的不同组件需要部署在不同的机器上,就要分别打包;(2) 安装和配置过程应该实现自动化;(3) 写自动化部署测试脚本来验证部署是否成功了。部署流程的可靠性是非常重要的,因为它是自动化验收测试的前提条件。

一旦将部署流程自动化后,接下来就要向UAT环境做一键式部署了。配置一下持续集成服务器,使你能自由挑选应用版本,并做到通过单击按钮来触发一个流程,即获取作为构建输出的二进制包,运行部署脚本,再运行部署测试。在开发构建和部署系统的过程中,一定要确保遵循前面说过的那些原则,如只生成一次二进制包,将配置信息与二进制包分离,以便在不同环境的部署中可以使用相同的二进制包。这能确保配置管理有一个健全的基础。除非软件需要用户自行安装,否则发布流程应该与向测试环境部署的流程相同。即使有不同之处,也只能是环境配置信息不同而已。

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

开发部署流水线的下一步就是实现全面的提交阶段,也就是运行单元测试、进行代码分析,并对每次提交都运行那些挑选出来的验收测试和集成测试。运行单元测试应该不需要太复杂的步骤,因为根据单元测试的定义,它并不需要运行整个应用程序,只需要运行在一个xUnit风格的单元测试框架上。

因为单元测试并不需要访问文件系统或数据库(与之对应的是组件测试),所以运行速度应该很快。这也是构建应用程序之后就直接运行单元测试的原因。与此同时,还可以运行一些静态分析工具,得到一些有用的分析数据,比如代码风格、代码覆盖率、圈复杂度、耦合度等。

随着应用软件不断变得复杂,你就需要写更多的单元测试和组件测试了。这些测试也应该出现在提交阶段。一旦提交阶段运行超过五分钟,就应把它们分成几份,以便并行执行。为了做到这一点,就需要多台测试机器(或者一台更强大的机器,它要有足够大的内存和更多的CPU),以及一个支持多任务并行管理的持续集成服务器。

5.8.4 自动化验收测试

流水线的验收测试阶段可以重用向测试环境部署的脚本。唯一的不同之处就是在冒烟测试之后,就要启动验收测试框架,并在结束之后,为进行分析收集所有的测试结果报告。另外,最好也保存一下应用程序的运行日志文件。如果应用程序有图形用户界面的话,也可以在验收测试运行时使用一个像Vnc2swf这样的软件来进行屏幕录像,这对于诊断问题比较有用。

验收测试可分为两种类型:功能测试和非功能测试。在项目初期就开始非功能需求测试(比如测试容量和可扩展性等)是非常关键的,这样你就能得到一些数据,用来分析当前的应用程序是否满足这些非功能需求。关于安装和部署,我们可以使用与功能验收测试同样的方法。但是,测试内容有所不同(关于如何创建这些测试请参见第9章)。刚开始时,你完全可以把验收测试和性能测试放在同一个阶段里接连运行。之后,为了能很容易知道哪类测试失败了,你可以再将它们分开。一套好的自动化验收测试会帮助你追查随机问题和难以重现的问题,如竞争条件、死锁,以及资源争夺。这些问题在应用发布之后,就很难再被发现。

当然,在部署流水线中,提交测试阶段和验收阶段需要运行哪些测试取决于你的测试策略(参见第4章)。在项目初期,应该至少有每种测试的一到两个测试可以自动化运行,并把它们放到部署流水线中。这样,初步框架就建好了,今后随着项目的进展,就比较容易增加测试了。

5.8.5 部署流水线的演进

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

当实现了部署流水线后,你会发现与相关人士的谈话以及效率的提高反过来又会对你的流程有影响。所以,一定要记住三件事。

  • 首先,并不需要一次实现整个流水线,而应该是增量式实现。
  • 其次,部署流水线是构建、部署、测试和发布应用程序整个流程中有效的,也是最重要的统计数据来源。
  • 最后,部署流水线是一个有生命的系统。随着不断改进交付流程,部署流水线也应该不断变化,加以改善和重构,就像改善和重构要交付的应用一样。

5.9 度量

反馈是所有软件交付流程的核心。改善反馈的最佳方法是缩短反馈周期,并让结果可视化。你应该持续度量,并把度量结果以一种让人无法回避的方式传播出去,比如使用张贴在墙上的海报或者用一个专门的计算机显示器以大号粗体字显示结果,这些设备就是信息辐射器。

根据精益思想,应该做整体优化,而不是局部优化。如果你花很多时间去解决某个瓶颈,而这个瓶颈在整个交付流程中并不是一个真正约束的话,整个交付流程并不会有什么根本性的变化。因此,应该对整个流程进行度量,从而判定这个交付流程作为一个整体是否存在问题

对于软件交付过程来说,最重要的全局度量指标就是周期时间(cycle time)。它指的是从决定要做某个特性开始,直到把这个特性交付给用户的这段时间。正如Mary Poppendieck所问的那样:“你所在的组织中,如果仅仅修改一行代码,需要多长时间才能把它部署到生产环境中?你们是否以一种可重复且可靠的方式做这类事情?”这个指标很难度量,因为它涉及软件交付过程中的很多环节(从分析到开发,直至发布)。然而,这个指标比其他任何度量项都更能反映软件交付过程的真实情况。

一旦知道了应用程序的周期时间,就能找到最佳办法来缩短它。你可以利用约束理论,按照下面的流程来做优化。

  • (1) 识别系统中的约束,也就是构建、测试、部署和发布这个流程中的瓶颈。随便举个例子,比如手工测试部分。
  • (2) 确保供应,即确保最大限度地提高流程中这部分的产出。在我们的例子中(手工测试),就是保证总是有用户故事在等待手工测试,并确保手工测试所需的资源不会被其他工作占用。
  • (3) 根据这一约束调整其他环节的产出,即其他资源都不会100%满负荷工作。比如,开发人员全力开发用户故事时,等待测试的用户故事会越积越多。因此,只要开发人员开发用户故事的速度能及时供应手工测试就可以了,其他时间他们可以写些自动化测试来捕获缺陷,这样测试人员就不需要在手工测试上花太长时间了。
  • (4) 为约束环节扩容。如果周期时间还是太长(换句话说,第(2)步和第(3)步都没有什么太多的帮助),就要向该瓶颈环节增加资源了,比如聘用更多的测试人员,或者在自动化测试方面投入更多的精力。
  • (5) 理顺约束环节并重复上述步骤,即在系统中找到下一个约束,并重复第(1)步。

尽管周期时间是软件交付中最重要的度量项,但还有一些其他度量项可以对问题起到警报作用。这些度量项如下所示。

  • 自动化测试覆盖率。
  • 代码库的某些特征,比如重复代码量、圈复杂度、输入耦合度、输出耦合度、代码风格问题等。
  • 缺陷的数量。
  • 交付速度,即团队交付可工作、已测试过并可以使用的代码的速率。
  • 每天提交到版本控制库的次数。
  • 每天构建的次数。
  • 每天构建失败的次数。
  • 每次构建所花的时间,包括自动化测试的时间。

如何呈献这些度量项是值得斟酌的。上面这些报告会产生很多数据,而如何解析这些数据就是一门艺术。比如程序经理可能想在一个项目健康报告中以非常简单的红黄绿交通信号灯方式看到已分析的聚合数据,而不是看到一页又一页的报告。相比之下,一个团队中资深的软件工程师会希望看到更详细的情况,但也不会乐意查看多页的报告。

每个团队的持续集成服务器在每次提交后都应该能够产生这样的报告和可视化效果,并将报告保存起来,以便今后对照某一数据库中的这些数据,对每个团队进行追踪分析。这些结果数据应该发布到一个内部网站上,用不同页面分别显示一个特定项目的数据信息。最后,把它们聚合在一起,这样就可以在整个开发过程,甚至整个组织的所有项目中追踪监控这些数据。

5.10 小结

部署流水线的目的是,让软件交付过程中的每个人都能够看到每个构建版本从提交到发布的整个过程。大家应该能够看到哪次修改破坏了应用程序,哪次修改可以作为候选发布版本进入到手工测试环节或发布环节。它应该能够支持人们执行到手工测试环境的一键式部署,并使大家能了解当前每个环境中运行的应用程序究竟是哪个版本,还能够支持一键式发布选定的某个版本,并清楚地标识出这一候选发布版本已成功通过整个流水线,并在类生产环境中经历了一连串的自动化测试和手工测试。

一旦有了部署流水线,发布流程中的低效环节就会显而易见。所有需要的信息都可从这个部署流水线上获取,比如一个候选发布版本需要多长时间能够通过各种手工测试阶段,从提交到发布的平均时间是多长,流程中每个阶段发现了多少缺陷。一旦掌握了这些信息,就可以优化软件的构建和发布流程了。

对于实现部署流水线这个复杂问题来说,没有万能钥匙一样的解决方案。关键还是在于创建一个记录系统,用来管理从提交到发布的任何变更,为你提供在流程中尽早发现问题所需要的信息。部署流水线可以帮助消除流程中的低效环节,这样可让反馈周期更短并更有效。这样做的途径有多种,比如添加更多的自动化验收测试,并行执行它们,或让测试环境与生产环境更相似,或者实现更好的配置管理流程等。

当然,部署流水线也依赖于一些基础设施,包括良好的配置管理,自动化的软件构建脚本和部署脚本,还要有自动化验收测试来验证软件会向用户交付价值。它还需要纪律性,比如确保只有通过了自动化构建、测试和部署的那些修改才能发布。我们会在第15章讨论这些前提条件和必要的纪律,其中有一个成熟度模型,用来评估持续集成、测试、数据管理等。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值