Table of Contents
- 1 持续交付和持续部署
- 2 自动化测试
- 2.1 功能测试
- 2.2 部署测试
- 2.3 性能测试
- 3 环境(environment)
- 3.1 自动化测试环境
- 3.2 自动化环境和生产环境的相似度
- 3.3 自动化构建过程的优化
- 3.4 环境的创建和维护
- 4 持续集成
- 4.1 单个产品的构建流水线
- 4.1.1 提交门限的概念
- 4.1.2 构建流水线的优化和变化
- 4.1 单个产品的构建流水线
1 持续交付和持续部署
持续交付 是目前的一个挺火的概念,它所描述的软件开发,是从原始需求识别到最终产品部署到生产环境这个过程中,需求以小批量形式在团队的各个角色间顺畅流动,能够以较短地周期完成需求的小粒度频繁交付。频繁的交付周期带来了更迅速的对软件的反馈,并且在这个过程中,需求分析、产品的用户体验和交互设计、开发、测试、运维等角色密切协作,相比于传统的瀑布式软件团队,更少浪费。
持续交付是经典的敏捷软件开发方法(例如XP,scrum)的自然延伸,以往的敏捷方法并没有过多关注开发测试前后的活动,例如前期的需求分析,产品的用户体验设计,产品的部署和运行维护等。随着伴随着敏捷的很多思想和原则在前后端领域的运用和升华,以及UX、DevOps等实践的逐渐兴起,我们在持续交付这个新的大概念下看到了敏捷方法和更多实践活动的结合和更大范围的应用。
举个flickr的例子,这个产品一周之内平均部署好几十次1, 几乎每个开发人员的每个修改就会导致一次部署。这对于产品来说,不仅仅意味着可以更快从用户那里得到关于产品的使用反馈,从而验证产品的想法,更可以迅速对产品进行改进,更好地适用用户的需求和市场的变化。
在这里我不想过多探讨这个端到端的过程,而是只想就整个软件交付过程中的一段进行探讨。假设现在需求已经明确,并且已经被划分为小的单位(例如用户故事user story),我们着重看一看从开发人员拿到用户故事,到这些用户故事被实际部署到生产环境上的这个过程。实际上这个过程当然是越短越好,特别是对于急需获得用户反馈的软件产品(例如很多互联网产品)。如果我们做的每一个用户故事,甚至是我们的每一次提交,都能够被自动地部署到生产环境中去,那么这种频繁近乎持续地部署,对于很多软件开发团队来说,就成了值得追求的目标。
持续部署和交付之间的另外一个区别在于,很多时候我们可以选择将功能部署,但是不让用户实际感觉到(dark launch2)。这样做可以有些功能需要部署到生产环境中,接受大量真实用户的非直接测试;或者有时候希望把一些小的功能组合起来一起展现给用户。因此,部署可以很频繁,然而实际交付给用户使用则可能根据计划,比部署的频率低。
当然持续部署并非没有投入和成本,产品的基础和特点不同,获得这种状态所需要的投入就越大。对于缺乏自动化测试覆盖的遗留系统,以及对安全性要求特别高的产品,它们要实现持续部署(甚至频繁部署)都会需要巨大的投入。但是如果产品所处的市场环境要求它必须及时相应变化,不断改进创新服务的话,这种持续部署的能力,就成了值得投入的目标。
持续部署,依赖于整个团队对所写代码的信心,这种放心,不仅是开发这段代码的人对自己写的代码的自信,也不是少数人的主观感觉,必须是团队或者组织的所有成员都抱有的基于客观事实的信心。因此,如何能够让任何新的修改都能够迅速地、有信心地被部署到生产环境,就成了一个值得解决的问题。后面我们仔细讨论,自动化测试是建立这种信心的根本保证。
在团队具备信心的基础上,要实现产品的持续部署,还需要有自动化构建流水线 (build pipeline)。以自动化生产线作比,自动化测试只是其中一道质量保证工序,而要将产品从原料(需求)转变为最终交付给客户的产品,自动化的生产线是中枢一般的存在。特别对于软件产品,多个产品往往要集成在一起才能为客户提供服务。多个产品的自动化构建流水线的设计也就成了一个很重要的问题。
产品在从需求到部署的过程中,会经历若干种不同的环境,例如QA环境、各种自动化测试运行环境、生产环境等。这些环境的搭建、配置、管理,产品在不同环境中的具体部署,都需要完善的工具支持。缺乏这些工具,生产流水线就不可能做到完全自动化和高效。
因此,持续部署靠自动化测试建立信心,以构建流水线贯穿始终,靠各种工具实现高效自动化和保持低成本。在下面我将详细谈论一下这几部分的内容。
2 自动化测试
如何能够保证我们写出来的代码既能准确实现我们的新功能,又能够不破坏既有的功能?唯有靠完善的测试。而当我们开始追求频繁地甚至是持续地部署的时候,自动化测试是唯一的能够让我们持续反复地验证软件的方法。如果一个产品具有完备的自动化测试用例,那么任何一次对软件的修改都能够得到自动化地回归验证,如果验证通过,我们就具备了将这些修改部署到生产环境中的信心。自动化测试的质量直接决定了我们能否具有持续部署的信心。
关于自动化测试,有很多著作详细地讲述了自动化测试的设计、实现技巧等,这里不再尝试重复这些内容。我想以一个web应用为例,举例分析一下到底需要些什么样的测试才能够让我们建立对这个产品的信心。
作为例子的web应用
这个简化的例子里包含了三个软件开发团队(A、B、C),这几个团队各自有自己的一些产品,最终他们的产品组合起来给用户提供完整的体验。A团队的产品包含了3个主要模块。其中fetcher集成了B和C的产品,定时从其中获取数据,经过分析后存入store;frontend则会从store中获取数据,以web界面与用户进行交互。 frontend同时还和其他一些组织外的第三方服务集成(例如twitter,weibo等)。
这里不妨再对A的产品所采用的技术进行进一步限定,以方便我们之后的有些讨论。我们不妨假定frontend和fetcher都基于java平台,都是标准的j2ee应用,store 采用mysql。frontend和fetcher都通过web api(可能是RESTful web api)于第三方应用集成。在生产环境中,frontend和fetcher都会部署在tomcat+apache服务器上。
我们现在可以以这个web应用为对象,考察一下如果我们在fetcher或者frontend 里修改了代码,诸如添加了新的功能,或者修复了bug,我们怎么样能使这些修改有信心地从开发人员的机器流入到产品环境中去。虽然我们以这个简化的web产品作为讨论对象,但是我们接下来讨论的大部分内容并不局限于web应用。
简单地讲,A产品的测试大概包含两个方面:
- 功能方面的测试。包含A产品自身功能的测试,A产品和B、C产品以及twitter 等外部应用的集成测试。
- 部署测试。将A产品顺利部署到各种环境中,需要大量的部署脚本和产品包等 的支持。部署测试验证这些脚本和产品包的正确性。
- 性能测试。在功能正确实现的基础上,产品的性能也必须满足预期。
2.1 功能测试
根据分层自动化测试的理念,功能方面的测试又可以分成如下几层。
最上层的是A和B、C以及外部应用的集成功能测试3。测试模拟真实用户和产品的交互,和真实的B、C以及外部应用通信,验证A系统功能的正确和完备。这种测试通常需要完整部署A以及相关的所有应用(如B、C),在真实的网络环境下执行。集成测试涉及的应用多,测试基础环境准备复杂,运行时间也通常较长,是最为昂贵的测试。
在A产品和其他产品接口确定的情况下,如果我们将A的外部依赖应用全部打桩 (stub),只关注于A产品自身的功能实现情况,这种测试我们估且称之为A的功能测试(有时候也叫A的验收测试)。以A为例,这意味着我们会将fetcher、db还有 frontend都部署起来,将所有外部应用如B、C、weibo都进行打桩。大家可能会觉得其上层的集成测试已经可以测到A的功能了,何必搞这么复杂又引入打桩?但是这一层测试相比于上层的集成测试有这么几个好处:
- 成本更低。单纯部署A比部署整个产品族的成本更低;而且因为A的外部依赖都 是stub,因此执行速度也会更快;而且因为打桩了外部依赖,不再需要考虑其 他产品的测试数据(fixture)准备,功能测试的测试数据准备的工作量相对也会 减少。大家可能认为打桩本身是个很高的成本,但是实际上有很多工具和库以 及让打桩变的很容易,例如对于web api,采用嵌入式web服务器可以很容易实 现这些api的模拟,这部分的成本很低。
- 更稳定。一般来讲,牵涉的应用越多,测试越不稳定。在B、C等都是真实应用 的情况下,任何应用中的问题都可能导致测试失败,甚至网络、部署上的问题 也可能导致测试失败。因此A的功能测试相对来讲更加稳定。
- 覆盖率高。因为成本更低,因此可以以同样成本编写和维护更多测试。以web应 用为例,目前有很多功能测试工具可以针对各种web交互进行测试。而且在外部 依赖打桩的情况,可以简单操纵stub模拟外部依赖接口的各种特殊情况,达到 对A在各种接口异常情况下功能的测试覆盖。
- 测试组织更良好。假设A、B、C都能够以这种方式对自身的功能进行完整验证, 那么A、B、C组成的整个系统的集成验证就可以只验证他们之间接口假设的正确 性,因此集成测试就可以只依靠贯穿3个产品功能的少量的测试,就可以保证 整个产品族的功能正确。
A产品的功能测试通常需要将A产品部署后才能进行。例如fetcher和frontend需要部署到tomcat里,store需要准备好mysql,还要将各自的配置文件写好,然后运行测试。非但如此,为了确信这个产品部署到生产环境能运行地和跑功能测试时一样,我们还要确保功能测试运行的环境和实际生产环境尽量保持一致,例如运行在同样的操作系统上,同样版本的tomcat、mysql服务器等。
这种分层测试的思想在整个自动化测试的设计和组织上都有体现。下层的测试相对于上层的测试,覆盖的范围更小,但是对功能的覆盖更全面。按照这个思路, A产品的功能测试下,又可能有fetcher和frontend两个组件自己的功能测试,而在fetcher内部,又可能有各层各模块的测试,再有针对每个类、函数或者方法的单元测试。
最外围的功能测试将A产品当作一个黑盒,这样的测试是浅层次的,不能完全覆盖所有场景,而且通常编写和维护成本高,运行时间长,并且受环境因素影响大4。如果一个产品的测试多数是这样的,很容易形成头重脚轻的冰激凌型结构5。
如果我们注意丰富底层的单元测试和小模块的功能测试,那么上层只需要较少的测试就可以达到较高的覆盖率,所有这些测试,以一个金字塔的形式,组合在一起确保A以及整个产品族的功能正确和完备,这样的测试组合稳定性和覆盖率高,而且开发成本较前一种冰激凌型低。如果这些测试都能够通过,那么团队就有信心将自己的代码修改部署到产品环境中去,这些自动化测试,就构成了产品的验证和功能防护网。
这里不得不提一下测试驱动开发(TDD)。前面提到了这么多的测试,如果系统在设计上对测试不友好,以致很难甚至无法写自动化测试,那么自然无法谈用自动化测试来保障功能。如果尝试先写功能后补测试,甚至希望另一个团队来写自动化测试,实践证明,想拥有完善、组织良好的测试用例也只是一个美好的愿望。测试驱动开发不仅能够很大程度上驱动出对测试友好的软件设计,也从一开始就保障了高测试覆盖率,以及组织良好、干净的测试代码。
2.2 部署测试
将产品部署到生产环境中与只是在开发环境下测试有很大的区别。我们都知道把 A产品部署到一群tomcat服务器上和把A在jetty或者IDE中跑起来有很大区别,仅仅保证后一点完全不能让我们有信心我们的产品能够在生产环境下成功部署运行。为了能够让我们的产品能够自动化地部署到生产环境中去,就需要有自动化的部署工具和脚本。
配置同样是部署过程中的一个重要环节。数据库等各种服务器的地址、账号密码,所有第三方依赖的地址(endpoint)、key文件等。这些配置可能会在不同的环境下有些许的变化。不同的网络环境例如DNS、防火墙也可能对产品的正确运行产生影响。确保这些配置在对应环境下能够正常工作是持续部署的关键。统一环 境而不是维护多种配置能够让产品在不同环境下的配置一致从而简化了部署脚本,但是仍然需要测试这些环境确实能够和统一的配置良好地工作。
如果我们用这些部署脚本将产品部署到环境中然后运行自动化测试,那么这些自动化测试实际上能够帮我们间接验证部署脚本的正确性。然而这也可能会导致产品的功能bug和部署脚本的问题的反馈夹杂在一起,让识别问题更加麻烦;同时也会让部署脚本的反馈周期变得更长。
无论部署测试的方法如何,部署工具和脚本的测试与产品的功能测试一样,都是确保产品能够持续部署的要素。后面我们专门讨论工具的时候,会详细讨论如何对环境、部署相关的工具和脚本进行自动化测试。
2.3 性能测试
性能测试也是产品交付之前的一道重要保障。在实际交付到用户手中之前,必须保证现有的系统能够有足够的容量支撑预期的用户量。性能测试的设计和实现同样已经有很多资源可以参考,这里也不再尝试重复已有内容。
性能测试和功能测试的最大不同在于,很大一部分性能测试是需要运行在和实际产品环境完全相同的环境,很多时候甚至直接用生产环境作为性能测试的环境。从准备这个环境以及自动化整个测试过程来讲,和功能测试并没有本质上的不同,而只有简单与复杂的区别,我们会在之后的工具和环境中详细讨论这些内容。
3 环境(environment)
环境是一个比较宽泛的概念。这里要说的环境,特指我们的应用所部署并运行的环境。一个环境包含了产品所涉及的从服务器(硬件或者虚拟机)、网络(DNS、 proxy、firewall etc.)到操作系统、应用软件等所有内容。
软件的开发到部署,所涉及到的环境至少有如下几种。
首先是开发环境,这里狭义地指开发者的单机开发环境。开发环境是任何应用首先运行的环境,任何代码都会首先在开发环境中首先得到一些手工或者自动的验证。自动化测试首先也会在开发环境上运行。开发环境未必和生产环境高度相似,例如A产品可能部署在linux平台上,而开发却用windows或者mac;生产环境中用的是tomcat,而开发环境中用jetty来作为j2ee容器。
然后是生产环境(production环境)。这是最为重要的环境,配备有最高级的硬件设备,部署着所有的应用,集成在一起为其客户提供服务。为了保证性能和稳定性,多半会运行load balance软硬件,拥有良好的安全配置。服务器们被安置在良好的物理环境中,并被时刻监控着运行状态。总之,这是最为复杂、重要的环境。
在开发环境和生产环境之间有很多环境,这些环境的复杂程度介于开发和生产环境之间。
A的环境
例如QA环境。顾名思义这是给大家进行功能测试的环境,大家未必只是QA们,而这里功能测试多半是手工。这个环境通常和产品环境具有一定的相似度,会部署一些真实的第三方应用。这个QA环境有时候也会兼用作演示(showcase)环境,抑或将演示环境独立出来。
3.1 自动化测试环境
除了这个QA环境,还有一系列用于自动化测试的环境。这些环境和自动化以及持续集成紧密关联。
比如运行A的功能测试时A所部署的环境(通常被称作staging环境)。这个环境和 A的生产环境极度类似,因为我们希望这些功能测试好像就是在测真实部署的A产品,这样一旦测试通过,我们就可以放心地将A部署。这种类似体现在:
- 相同的服务器、网络环境。两个环境下的服务器操作系统、服务器软件首先要 完全相同,用同样的软件包安装,系统的配置也要完全相同。可以说, staging环境中的机器要和生产环境中的机器几乎完全一样。网络环境也要相 似。相似度越高,因为环境不同而引起的潜在问题就越少。如果产品部署到云 计算环境中(例如amazon、heroku等),我们很容易建立任意个配置相同的机 器。
- 相似的拓扑结构。生产环境中为了提升系统性能和容量通常会采用负载均衡进 行水平扩展。例如我们可能部署多个frontend,store也可能是一个mysql集群。 staging环境不需要这么多服务器,但是A产品部署的基本拓扑结构应该保持相 同。
- 相同的部署方法。如果生产环境中会部署A的rpm包,那么staging环境中也必 须采用rpm包形式部署;反之如果采用脚本或者chef、puppet等工具,staging 环境也必须用同样的方法。否则部署方法不同,无法保证在生产环境中部署的 结果和staging环境中一样,也就增加了出问题的风险。
A的staging环境
staging环境之所以有这个称谓,就在于它和生产环境的相似。而这种相似,正是我们进行持续部署的信心所在。单个产品例如A、B的staging环境,可能只包含A产品自己的模块,而对它所依赖的B以及其他应用进行打桩,打桩的范围也可能根据所依赖应用的特点以及成本、效率等考虑而或多或少。
运行A、B、C的集成功能测试时A、B、C所部署的环境,和上面说到的A的staging 环境很相似,不过范围更大,部署了更多的产品,因此常常也叫端到端(end to end,e2e)测试环境。这个环境,也是和要尽量和生产环境类似,如果说A的 staging环境模拟的是生产环境中A的那部分,e2e环境就是模拟的整个组织的生产环境,可以看作是更大范围的,整个组织级别的staging环境。
生产环境和staging环境及e2e测试环境的最大区别可能在于容量上。通常生产环境需要有能力给大量的用户提供服务,因此通常会有很多服务器,而功能测试环境只是验证功能正确,并不需要同等数量的服务器来实现这一目的。
生产环境往往有复杂的安全规则设置,这些规则有时候会影响产品的功能(例如防火墙设置可能会影响多个应用之间的通信);生产环境中诸如数据服务器的密码等信息必须保密;生产环境中可能借助于代理才能访问互联网资源,等等。这些因素,在我们设计构造staging环境的时候,都必需纳入考虑。
最后还有持续集成(CI, continuous integration)环境。这是持续集成服务器用来运行它自己(包括它的agents)以及进行产品的自动化构建的环境。CI服务器就相当于一个开发人员,自动地监控代码库的变化,一旦有变化就自动运行自动化构建。CI服务器会在这个环境中运行自动化构建的所有内容,作为持续部署的中枢,像流水线一样贯穿整个开发、测试、部署过程。
3.2 自动化环境和生产环境的相似度
不难看出,自动化测试环境和生产环境的相似度影响我们对产品的信心。在越接近实际生产环境的环境中验证,我们越能够有信心将验证过的东西直接交付给用户;而验证环境的相似度越低,可信度越低。比如说我们如果只在开发环境下用 jetty和内存数据库来进行A的功能测试,我们肯定会对它是否能够在复杂的生产环境下部署产生怀疑。因为所有关于A的部署脚本、产品包都没有经过验证过。
然而理想和现实之间总要做出一些实际的取舍。成本和效率都允许的条件下,如果所有功能测试都在一个生产环境的副本下执行,那么我们可以在交付前验证所有的因素。然而现实是给所有团队创建完整的生产环境用作测试成本首先会相当高昂,生产环境往往有很多服务器集群,这些集群通常都已经是组织的巨大投入。
并且很多时候由于技术和其他方面的原因根本无法做到。例如如果生产环境中采用netscaler作为负载均衡器(load balancer),而团队采用amazon之类的云计算平台构建测试环境,目前技术上就很难将netscaler放到云中去运行。
从另一方面来讲,自动化环境和生产环境的高仿真度所来的好处呈边际效应递减。如果说staging环境相对于开发环境让我们能够有机会测试所有的部署脚本,并且能够测试产品在一个简化的生产环境中的实际运行情况,从而给了我们更多的信心,那么在staging环境中加入负载均衡器并且多用几台服务器给我带来的好处就远没有那么大了。
我们还可以考虑一下另一个类似的问题:staging环境是部署真的第三方依赖应用,还是应该将它们无一例外全部打桩呢?如果打桩的话,我们也许丧失了一些真实的反馈,漏掉了少数的测试用例,但是带来的好处却是测试稳定程度、执行速度以及对A产品自身测试覆盖率的提升。
另外,是否需要在测试环境中实现某些生产环境中的要素也取决于我们想测试的点究竟是什么。如果我们希望测试环境的安全性,或者我们希望测试负载均衡器的某些设置,那么我们可能就需要包含这些设备的环境来测试它们。
修改的频度也是其中一个考虑因素,如果防火墙、负载均衡器、缓存等的设置经常处于变动状态,那么可能在staging环境中复制这些内容就会有较大的价值。否则如果需要花很大的代价去频繁测试几乎不变动的内容,其价值相对来讲就会很小。
速度、成本、稳定等,都是我们在现实项目中可能考虑的因素,并非环境越和实际生产环境相似,效果就越好。在团队达成共识的前提下,选择当前合适自己情况的方案,是比较实际的做法。如果有少数的情况可能没有被测试覆盖到,也可以持续改进它。
3.3 自动化构建过程的优化
很多时候,如果我们必须在A的staging环境下开发和调试功能测试的话,在日常开发过程,尤其是TDD过程中,效率往往让开发人员无法忍受。开发阶段的反馈周期往往必须保持在数秒的级别,超过10分钟就让人无法忍受。例如junit单元测试,每个函数的编写过程中可能都要修改和运行n次,超过几秒就让人无法接受。而功能测试虽然天生就更复杂些,但是如果整套测试如果需要超过10分钟甚至更久,作为开发人员就不太会频繁地运行这部分测试。在这样的背景下,就产生了很多优化手段,它们的目的都是为了缩短自动化测试以及整个自动化构建过程的运行时间。
目前已经有很多优化手段6 。例如,不再将A的各个组件部署到staging环境中,而是部署到开发环境中,采用轻量级容器如jetty来代替tomcat,采用内存数据库代替mysql等。也可以采用诸如htmlunit的框架代替selenium来编写web功能测试。这些手段的最终目的都是希望在开发阶段能够以最小的成本、最快的速度来运行尽可能的自动化验证,以获得尽可能快的反馈。
在优化的环境中运行A的功能测试,固然不能让我们获得和在staging环境下运行测试相同的信心,但是实践中,在很多情况下,已经能够提供足够高的可信度,这种可信度对于某些非关键性产品来说,可能已经足够让他们放心将产品部署到生产环境中去了。与此同时,带来的是开发效率和质量的大幅度提升。
3.4 环境的创建和维护
大量环境的管理和维护,本身就构成了一个巨大的问题。在传统的基于物理机器的运维时代,这么多环境的安装、维护成本高昂,因此极易造成一套环境多用途、多团队共享的情况,无法保证环境的干净、可靠。但在云计算资源逐渐可能会低于电费的今天,软件团队将能够借助于虚拟机和云计算,以更低的成本去按需创建各种环境,甚至开发环境也可以用虚拟机代替。可以说,云计算是持续交付的基石。
4 持续集成
持续集成作为敏捷方法的一项核心实践,由来已久7。在持续交付中,持续集成服务器将从开发到部署过程中各个环节衔接起来,组成一个自动化的构建流水线(build pipeline),作为整个交付过程的中枢,发挥着至关重要的作用。
前面说过,我们希望我们对软件的修改能够快速、自动化地经过测试和验证,然后部署到生产环境中去。在自动化测试和环境都具备情况下,开发人员除了在本地运行自动化构建进行验证外,剩下的工作就主要由持续集成服务器来帮忙完成。
目前市面上有很多持续集成服务器软件,例如jenkins,go,bamboo, cruisecontrol,travis-ci等,这些软件有的支持构建流水线的概念,有的有构建流水线插件,持续集成服务器主要通过调用产品的自动化构建脚本8来执行你所配置的各阶段任务。
我们先以A产品的构建流水线为例,看看其中主要有什么样的内容。
4.1 单个产品的构建流水线
A的构建流水线
产品A的构建流水线自动化了从编译、静态检查、打包、在不同环境下进行部署并运行自动化测试、发布产品包以及完成最后部署整个过程。从开发人员提交修改到源代码库中那一刻开始,剩下的所有步骤都由构建流水线自动完成。
开发人员在开发过程中,首先会在开发环境中完成开发验证,自动化测试的编写、调试和修改,TDD,自动化构建脚本的编写,部署脚本的编写等,都在开发环境中完成。这里是所有修改的入口,所有验证的初始发生地。我们应该尽量做到所有的开发和验证都能够在开发环境中完成。
而当开发和验证完成,确认修改正确后,就可以将代码提交到源代码库中。持续集成服务器持续监视着代码库的修改情况,自动将最新的修改更新到持续集成环境中,开始从编译打包到部署的一系列自动化过程。
以A产品为例,这个自动化过程包含了若干个阶段,之所以分成若干个阶段,是为了更加直观地展现这个过程。后面我们会谈到,因为优化的关系,不同产品的构建流水线可能形态上会有些不同,但是它们都包含了如下几个重要阶段。
首先是打包阶段。打包是一个笼统的说法,其本质是将应用准备成能够在生产环境中部署的形式。capistrano部署rails应用直接将源代码checkout到生产环境, j2ee则规定了web应用必须以war包形式部署到容器中。这两种形式虽然都能够实现部署的目的,但是更好地是将产品以产品包的形式发布出去,例如对linux平台以rpm或者deb包的形式发布。
不论产品选择何种形式发布,有一个需求是共同的。所有产品都必需能够支持在安装后、服务启动前对配置文件进行修改。war包是不符合这个要求的,因为配置文件被包含在war包中,只有j2ee容器启动之后才能修改其中配置文件(现在有一些办法能够将war包中的配置文件从war中提取出来)。
在打包之前,还有必要对包的可用性进行尽可能充分的验证。静态检查、单元测试等任务可以在打包之前进行,如果功能测试成本不高,甚至也可以考虑放到打包之前运行。这样,我们可以对打出来的包的功能有一定的信心。这种信心对提升整个构建流水线的效率和正确率是很重要的,因为越往后的阶段成本相对来说越高,反馈越慢,因此前面的阶段验证越充分,后面的阶段成功的可能性越大,而失败之后的错误追踪也更加容易。
打包之后我们就可以将产品包部署到staging环境下进行功能测试。这个阶段的任务首先是要准备一个干净的staging环境。为此我们必须首先准备好必须的服务器以及网络环境,然后安装操作系统并作基本的系统配置(例如DNS等),然后利用我们的部署脚本和产品包将产品部署到环境中去。
这个过程中很重要的一条原则就是在staging环境中和生产环境中所采用的部署方法必须一样,是同一种方法,同一套脚本,同一组产品包。只有这样我们才能够有信心将经过验证的产品包放心地用这一套脚本部署到生产环境中去。这就类似于前面提到的环境相似度原则,用于staging环境的任何部署脚本、产品包如果有和生产环境不同的地方,都有可能在生产环境中导致问题,这些问题必然会成为我们持续部署的阻碍因素。
在环境部署好之后,就可以对环境中的产品运行功能测试。如果这些测试全部通过,那么我们就可以选择将部署脚本和产品包发布到仓库(repository)中去。这些交付物(artifact)会在之后的测试、部署中被用到,同时其他产品团队也会需要这些交付物去部署它们自己的环境,进行集成测试等。
接下来是在e2e环境中的测试。首先自然也是要准备好所需要的服务器等基础设施,然后将集成测试所涉及的所有产品都部署到该环境中去,再运行测试。
部署集成测试环境需要各产品都提供完善的部署手段,换句话说所有的产品都必须提供能够将自己部署到一个干净环境中去所需的包、脚本、工具等。
如果集成测试也通过,那么我们就可以选择将产品包部署到实际生产环境中去了。这一过程所包含的具体内容,视不同产品的复杂程度、生产环境的特点、组织的策略等,可能会有很大的不同。
构建流水线的各个阶段之间的触发方式,通常是自动的,上一个阶段成功之后,下一个阶段就会被自动触发执行。但是在某些情况下,有些阶段的触发可能是手动的。例如publish和deploy两个阶段,在很多情况下可能是手动的。deploy阶段的触发,因为涉及到生产环境的安全性,还往往可能需要触发的时候进行身份验证。
4.1.1 提交门限的概念
从构建流水线图中我们可以看到,持续集成服务器是在不断监视着源代码库的变化,一旦有人提交就会触发构建过程,如果其中发生问题,则会将结果反馈给团队。整个构建过程通常会需要一段时间,我们希望整个构建过程的成功率尽可能高,或者更准确点讲,尽量反映开发人员在本地开发环境中无法验证或者发现不了的问题。
因此我们希望开发人员在将修改提交到代码库之前,能够在自己的开发环境中进行充分的验证,至于不同开发人员之间的修改的集成、更复杂环境下产品的验证这些在本地环境中较难低成本验证的东西,构建流水线会帮助我们提供反馈。但是如果本地验证不够充分,甚至不作本地验证就随意将代码提交,构建流水线就可能会被大量低级错误所充斥,大部分时间处于失败状态,最后就像被DDoS了攻击一样,失去了给团队提供更有价值反馈的能力。
所以,团队内的开发人员应该首先在本地进行充分验证,然后再提交。多充分算充分呢?这基于所验证内容的成本和团队的共识。如果所有构建内容能够在本地 10分钟之内执行完成,我们就可以约定提交之前必须在本地执行所有构建内容;反之如果整个构建过程耗时超过30分钟,每次提交前都执行全部构建就会严重拖慢开发进程,打乱开发节奏,这种情况下团队可以约定将一部分内容作为提交前必须执行的内容。
这种整个团队为了提高构建流水线的成功率,约定的在提交前必须执行并保证通过的构建内容,就成为构建门限,或者成为本地构建。很明显,本地构建占整个构建过程的比重越大,团队就能越早在本地就得到尽可能多的反馈,整个持续交付过程就更加流畅。
然而本地构建的一个重要要求就是要耗时短。有时候可以通过构建的优化来减少构建时间,不同产品的特点不同,构建复杂度以及时间也会有区别。
4.1.2 构建流水线的优化和变化
我么以A产品为例讨论了它的构建流水线(见图),然而我们给出了构建流水线设计并非是唯一的方案。构建流水线的目标,那就是能够给团队以持续部署的信心。在满足这个目标的前提下,流水线的具体实现形式可能会有不同程度的变化。
整个构建流水线各阶段的执行时间是影响其设计的一个重要因素。如果package 和staging两个阶段可以在5分钟内完成,也许我们不需要把它们分成两阶段来获得反馈。如果A没有和其他任何产品的集成,那么e2e测试阶段也可以去掉。对于更加简单的应用,也许只要一个阶段就可以包含所有的构建内容。
另一个因素是整个过程的组织形式和视觉呈现要求。将不同的构建内容显式分成不同的阶段可以对各阶段的反馈有更明确的了解。特别是在某些阶段需要手动触发时,这种阶段的分隔就更加有价值了。
不管怎么优化,都必需遵循构建流水线的基本目标原则,如果优化的结果过度偏离了它的目标,就不再是优化的问题,而是能否起作用的问题。
Footnotes:
1http://code.flickr.com/, 见页面最下方的部署统计
2 facebook适用dark launch测试他们的新功能:https://www.facebook.com/note.php?note_id=96390263919
3 目前关于测试的术语很多。在这里我们沿用c2.com对于单元测试的定义,即对单个对象的方法或者函数的测试,功能测试则是对产品的一部分或者整个产品的功能的测试,把多个产品的组合的功能的测试成为集成(功能)测试
4 参见此文关于测试深度的讨论:http://fabiopereira.me/blog/2012/03/18/introducing-depth-of-test-dot/
5 此图片以及下面的金字塔结构图片引自:http://watirmelon.com/2012/01/31/introducing-the-software-testing-ice-cream-cone/
6 参见这篇文章:http://dan.bodar.com/2012/02/28/crazy-fast-build-times-or-when-10-seconds-starts-to-make-you-nervous/
7 参见Martin Fowler的文章:http://martinfowler.com/articles/continuousIntegration.html
8 不同的语言有很多工具用来编写自动化构建脚本,例如ant、maven或者rake
转发自:http://exceedhl.thoughtworkers.org/cd/cd.html