持续集成理论和实践的新进展

最近雷镇同学将Martin Fowler先生的著名论文《持续集成》第二版翻译成中文并发布出来,掀起了国内对于持续集成理论和实践讨论的新的高潮。笔者在本文中将全面对比持续集成 论文前后两版的异同,分析并展示ThoughtWorks在持续集成领域的理论和实践方面的研究成果,以图对国内企业实施持续集成起到参考和借鉴作用。需 要说明的是,本文所介绍的内容毕竟限于笔者的水平,并且主要是ThoughtWorks内部开发和对外咨询实践的总结,所以未必对读者所遇到的情况是适用 的,请自行甄别。

《持续集成》第二版虽然是最近才翻译出来,但是实际上Martin Fowler先生完成此文是在5年前的事情。这五年恰好是ThoughtWorks中国公司快速成长的五年。在这五年内ThoughtWorks中国在持 续集成领域也有很多的发展,这包括:著名的持续集成工具Cruise主要是由中国公司负责开发1; 中国公司帮助国内很多大中型企业完成持续集成实施和相关的流程改进;2009年中国公司的很多同事对于持续集成的度量进行了深入的讨论并且最终由胡凯将其 实现为一款软件iAnalysis;2010年至2011年成功的交付了从需求提供方到多个技术服务提供商的持续集成方案,以及企业级自动化中心方案。所 以,本文主要包括两部分内容,一部分是通过对比第一版与第二版的异同介绍2000年到2006年之间持续集成领域的主要发展,另一部分则是介绍第二版发表 之后持续集成领域的新进展。读者如果之前没有阅读过《持续集成》论文的第二版,建议将本文第一部分一同阅读,因为本文并非对论文的重述,所以很多地方还需 要参考原文中的内容。

第一部分 《持续集成》第一版与第二版

《持续集成》第一版由ThoughtWorks首席科学家Martin Fowler先生和Matthew Foemmel共同完成2,第二版由Martin Fowler先生更新。

《持续集成》的第一版中并没有给出比较正式的定义,虽然作者在文中说是借鉴了XP实践中的术语,但是目前能看到的XP实践中对持续集成的定义实际上大多数都是指向了Martin的文章。那么我们还是来看看第二版中给出的定义。

持续集成是一种软件开发实践。在持续集成中,团队成员频繁集成他们的工作成果,一般每人每天至少集成一次,也可以多次。每次集成会经过自动构建(包 括自动测试)的验证,以尽快发现集成错误。许多团队发现这种方法可以显著减少集成引起的问题,并可以加快团队合作软件开发的速度。3

第二版相对于第一版增加了不少内容,其中最重要的几点包括:

  1. 详细介绍了使用持续集成进行软件开发的工作流程。
  2. 突出了配置管理在持续集成实践中的作用。
  3. 提出分阶段构建的概念。
  4. 增加了持续集成报告的内容。
  5. 增加了持续部署的内容。
  6. 给出了引入持续集成的建议。

持续集成的流程

在持续集成领域,我们经常会用到的一个术语就是“构建(Build)”。很多人认为“构建=编译+链接(Build=Compile+Link)”,Martin在第一版中指出一次成功构建包括:

  • 所有最新代码从配置管理工具中取出(check out或者update)。
  • 所有的代码从干净的状态开始编译。
  • 将编译结果链接并部署,以备执行。
  • 执行部署的应用并运行测试套。
  • 如果上述所有操作没有任何错误,没有人工干预,并通过了所有测试,我们认为这才是一次成功的构建。

实际上,目前很多团队对成功持续集成构建的定义基本上是符合上述定义的。这个定义的特点在于它是相对独立的,它是一个从干净状态的源代码最终获得可运行的通过验证的软件的过程。

Martin在第二版中则在成功构建的基础上给出了成功集成的定义。成功集成关注的不是一次“编译+链接+部署+验证”的过程,而是从开发流程的角度介绍一次完整的在持续集成约束下的代码提交过程4

  • 将已集成的源代码复制一份到本地计算机。
  • 修改产品代码和添加修改自动化测试。
  • 在自己的计算机上启动一个自动化构建。
  • 构建成功后,把别人的修改更新到我的工作拷贝中。
  • 再重新做构建。
  • 把修改提交到源码仓库。
  • 在集成计算机上并基于主线的代码再做一次构建。
  • 只有这次构建成功了,才说明改动被成功的集成了。

下图展示了Martin对成功集成的定义:

当然在第一版的“代码提交”这一节,Martin也提到了本地构建的概念,只是不如第二版这么明确。

配置管理

Martin在第一版中有两处提及配置管理,分别是:单一代码源(Single Source Point)和代码提交(Checking In)。第二版中则包括:通过持续集成构建特性(Building a Feature with Continuous Integration)、只维护一个代码仓库(Maintain a Single Source Repository)、每人每天都要向主线提交代码(Everyone Commits To The Mainline Every day)、每次提交都应在集成计算机上重新构建主线(Every Commit Should Build the Mainline on an Integration Machine)。不仅条目数量上增加明显,作者提出的很多实践都是基于配置管理来讲的。

工具

配置管理是持续集成的输入。在第一版中作者所推荐的配置管理工具是CVS,到第二版中作者推荐的配置管理工具已经换成了SVN5(参见第二部分中的配置管理工具部分)。

分支策略

实现进度与质量的平衡是配置管理的重要目的。Martin在第二版中对滥用分支给出了警告:

尽量减少分支数量。典型的情况是保持一条主线,……,每个人都从这条主线开始自己的工作。(对之前发布版本进行Bug修正或者临时性的实验都是创建分支的正当理由。)

但是这里给出的建议对于大型团队来说并不十分合适。我们将在第二部分对于配置管理的分支策略进行详细描述。

内容

Martin在第一版中给出的原则是:

任何人都可以找到一台干净的机器,连上网,通过一个命令就可以取得要构建所开发的系统需要的所有源文件。

第二版中的原则增加了对构建的支持6

任何人都可以找到一台干净的机器,做一次取出(checkout)动作,然后对系统执行一次完整的构建。

分阶段构建(Staged Build)

分阶段构建是Cruise(已经更名为Go)引入的重要概念。其主要的意义在于:

  • 分离关注度不同的验证阶段,比如Commit Build和Regression Tests,团队会对不同的验证阶段采取不同的策略
  • 构建流程可视化
  • 通过分阶段并发构建来缩短反馈周期

当构建的时间过长时,我们通常会要求开发人员只运行速度较快的价值较高的构建阶段就可以继续自己的开发任务,而不必等待漫长的次级构建完成。这里作者提到ThoughtWorks不同的团队有很多有趣的实践,我们将在第二部分向读者介绍其中的一部分。

报告

作者在第二版中专门拿出一节“每个人都能看到进度(Everyone Can See What’s Happening)”来介绍有关持续集成报告的内容。因为:

持续集成的目的是为了沟通。

这是第二版相对于第一版来说一个非常明显的变化。在第一版中通知的手段还主要是电子邮件,实际上在作者撰写第二版的时候,ThoughtWorks已经不赞成将电子邮件作为主要的持续集成通知工具了。更好的沟通工具包括音乐、熔岩灯、显示器等。

对于沟通的重视从工具的角度也可以体现出来。Cruise Control最主要做的事情是任务调度,在报告部分做的相对来说非常粗糙,比较有价值的报告大部分是从Cruise移植过去的。Cruise在从一开始 就非常重视这一点,通过Cruise你可以非常清晰地知道,代码发生了什么变化、正在进行的构建的状态和历史构建的状态。网页的形式对于分布式团队来说具 有不可替代的优势。

正如我们前面所说的,音乐、熔岩灯等物理手段,具有更强的信息辐射能力。站起来往周围看一看就知道哪个团队的构建成功了,哪个失败了。

持续部署

持续集成实践有一个基本的思想就是:越是痛苦的事情,越要经常做。集成之后更令人心惊胆颤的事情就是——部署。部署到生产环境的流程通常要严格得多,然而所有的工作必须经历了生产环境的验证才算是成功的,所以——持续部署才是王道。Martin在第二版中建议:

你应该有一个脚本帮助你很容易地将系统部署到生产环境中去。……同时要特别考虑的是要能够自动回滚。

引入持续集成的建议

作者在第二版中特别给出了逐步引入持续集成的建议。包括:

  1. 引入版本控制。
  2. 实现自动化构建。
  3. 添加自动化测试。
  4. 加快提交构建。
  5. 寻找帮助。(比如ThoughtWorks)

第二部分 持续集成领域的新进展

正如前文所说,ThoughtWorks中国公司在过去的几年里面对于持续集成实践和帮助客户实施持续集成都积累了很多的经验,同时在理论体系方面也更加丰富完整。这也使ThoughtWorks在这个领域继续保持了行业领先的位置。

正如我们在第一部分讲到的,持续集成不应该只作为一个孤立的实践来应用。我们的经验表明如果只把持续集成作为一个孤立的实践应用很难从持续集成长期 受益。持续集成往往进入“长红”或者“长绿”的不正常的状态。长红意味着系统长期无法集成;长绿则往往意味着缺少足够的验证。为了术语上的澄清,我们明确 地将持续集成的定义区分为狭义的持续集成和广义的持续集成。

狭义的持续集成:基于某种或者某些变化对系统进行的经常性的构建活动。

广义的持续集成:软件开发团队在上述活动的约束下所采用的开发流程。

狭义的持续集成

一般来说,狭义的持续集成包括如下几个方面:持续检查、持续编译(链接)、持续验证、持续部署、持续基础设施、持续报告等6个方面。

持续检查

持续检查的目的是保证代码风格一致,主要关注于代码的静态质量和内部质量,比如变量命名方式、大括号位置等等。大部分的现代集成开发环境(IDE) 都具备实时检查代码质量的功能。为了保证主线上的代码质量能够达到一致的标准,应当在持续集成脚本中加入静态检查阶段。比如,Java的PMD、 FindBugs等等。

持续编译

持续编译是一个很朴素的想法,就是保证主线上的代码始终处于可编译的状态。但是这对于很多大中型团队来说却并非想当然的简单。这是因为很多团队并未 采用集体代码所有权策略,导致存在依赖的团队的代码无法编译。针对这样的问题,一方面我们建议采用集体代码所有权;另一方面,对于确实因为安全原因需要隔 离的代码应该边界、明确接口,很少存在大部分代码需要对大部分人保密的情况。

持续检查和持续编译是持续集成中最基本的验证手段。

持续验证

持续验证的目的是检查主线上的代码是否能够实现所要求的功能,或者已有的功能是否被破坏。在大部分的构建中,验证部分是耗时最长、成本最高的部分, 但同时也是收益最大的部分。在这个阶段,我们看到的主要问题是验证不充分和验证时间过长。针对这样的问题,我们通常采用分层分级的持续集成策略。后面有详 细的描述。

持续部署

对于大型软件应用来说,部署往往是一个费时费力又容易出错的步骤,因为这里面涉及到数据迁移、版本兼容等各种棘手的问题。持续部署的思想是将这些工作标准化自动化,使其能够可靠地、自动地、快速地运行。持续部署是当前DevOps运动中的热门话题之一。

持续基础设施集成

现代大型软件开发,尤其是互联网应用开发中经常依赖于一些常见的基础设施——比如Spring、Tomcat、Database等等。这些基础设施发生变化的时候,我们应当及时地触发持续集成,以确保我们的系统是能够与新的基础设施一起工作的。

持续报告

报告是将持续集成的状态以适当的形式展现给干系人的基本手段。报告是持续集成的晴雨表,所以它必须直观、易懂,而且对关注点不同的角色展现不同的内 容和粒度。比如,开发人员可能更关心错误的日志;项目经理可能更关心测试覆盖率;产品经理可能更关心持续集成通过率的趋势等等。

广义的持续集成及持续集成策略

当要把持续集成实践应用到团队的时候,有很多额外的技术或者非技术因素需要考虑。

组织结构

持续集成是一个重要的沟通工具,而开发过程中两个最需要紧密沟通的角色就是开发和测试。在我们常见的组织结构中开发和测试往往隶属于不同的部门,甚 至这些部门隶属于不同的高级经理。这往往会给持续集成的推广带来很大的阻力。这是因为持续集成从环境搭建到运行维护都需要两种角色的通力合作。我们的经验 是这类涉及到人力资源的事情除非某一级“共同的大老板”出面,否则是很难协调的。“借调”这样的方式往往不能保证效果。

流程

放到团队的角度看待流程应当更加关注于各个成员之间的配合。每个开发人员提交代码之前应当确保是经过本地构建的;开发人员在提交之前应该确认主线上的代码是通过了持续集成的;测试人员测试的版本应该是通过了某次持续集成的,并且有相应的具体版本信息。

为了保障流程的顺利执行,我们还经常采用持续集成看板、提交令牌等辅助手段。

环境

环境是指持续集成运行时所依赖的软件和硬件的集合。我们经常遇到的一个问题是,软件在一台机器上能够通过持续集成的验证,而在另一台机器上则不能通 过。这通常是因为我们对持续集成环境的定义不明确造成的。所以在搭建持续集成和在组织内推广持续集成的时候,我们需要特别注意持续集成环境的标准化,明确 指出持续集成运行时依赖哪些第三方库,机器配置如何,端口和网络状况如何等等。

我们经常采用将持续集成环境加入配置管理的方式来解决环境标准化的问题。

分层

在大型团队(超过100人)中,扁平的开发组织结构是运行起来是比较困难的。常见的做法是按照特性,将团队划分为10人左右的小团队。一般来说,如 果团队数量超过10个,还会再增加一层架构。这时候,配置管理的策略也应当做出调整。常见的做法是为每个团队拉出一个分支,设置一个集成分支用于将各个特 性分支的内容整合在一起。需要注意的是,这里每个分支都应该具备所有代码的访问权限,也就是所有分支是同根的、等价的。

图片来自:http://damonpoole.blogspot.com/2008/01/multi-stage-continuous-integration-part_05.html

测试分级

在实施持续集成的时对于测试的类型应该有比较明确的定义。一般来说,我们经常把测试分为三级——单元测试、集成测试和系统测试。这是一个很大的话 题,这里只是说明此处的单元测试并不是指针对函数的测试。虽然单元测试主要是函数基本的测试,但是每个单元测试应该针对的是特性或者对应代码在实现该特性 上所发挥的作用。

单元测试的运行速度通常非常快,应该在数秒到数分钟。单元测试应该覆盖绝大部分的特性需求。集成测试单个测试的运行速度通常会比单元测试慢一个数量 级,比如存在文件读写或者其他的IO和网络操作。集成测试主要用于保证系统的各个组件之间的调用是工作的。系统测试的运行速度一般会比集成测试慢,通常需 要将整个系统运行起来,比如Web开发中的selenium测试。系统测试主要用于测试系统的正确路径(Happy Path)可以工作。有的团队会开发很多基于整个系统的回归测试,也属于系统测试。就场景的覆盖而言,单元测试>集成测试>系统测试,从运行 时间来看则相反。

持续集成的成熟度评估

在帮助客户实施和推广持续集成的过程中我们逐渐总结出一些原则,帮助客户评估现状,分析和设计未来的目标。该评估方法借鉴了ThoughtWorks敏捷成熟度模型中有关配置管理、测试、构建等内容,并做了适当的简化。

构建:

级别  描述 
3+:对外防御的  团队能够根据自己的需要,协调其他团队的持续集成,当依赖的其他团队的代码和组件或者第三方库和基础设施发生改变时自动进行构建。团队对于外部依赖的可靠性有信心。 
3:对内防御的  构建是自动的。由测试和检查来保证团队内部的开发质量。持续集成的修复具有最高的优先级。团队对持续集成的结果有信心。 
2:频繁的  构建是自动的,而且构建的速度较快。构建的触发条件是明确的,通常每次代码提交都会触发构建。团队中的每个人都会触发构建,并且了解构建的状态。 
1:重复执行  构建是自动的,但是执行的不够频繁。构建的触发是随机的或者频率是非常低的(低于每天一次)。构建的速度通常非常慢,比如一次构建超过半个小时。 
0:可重复的  主要依赖于手动的方式构建软件,但是每次构建的方式都是相同的或者相似的。通常有相关的文档的指导。经常团队指定某个人负责构建软件,虽然大部分人都能够做这件事情。 
-1:手动的  主要依赖于手动的方式集成软件。每次集成的方式可能不一样。经常团队中只有个别人能够将软件集成起来。 

测试:

级别  描述 
3+:全面集成的  全团队对测试负责。测试驱动整个开发过程。测试与构建完全集成。 
3:测试驱动的  业务分析人员和开发人员均参与测试。测试在构建过程中自动执行。开发人员实践测试驱动开发。 
2:集成的  开发人员参与测试。部分测试集成在构建过程中执行。大部分测试在软件开发过程中执行。 
1:共享的  开发人员参与测试。测试并未集成在构建过程中。部分测试在软件开发过程中执行,大部分测试在软件开发结束后执行。 
0:审查的  测试由专门的测试人员负责。有部分测试是在软件开发过程中执行。但大部分测试在软件开发结束后执行。 
-1:独立的  测试由专门的测试人员负责。仅在软件开发结束后执行。 

配置管理:

级别  描述 
3+:企业级的  企业有统一的配置管理策略。 
3:跨项目的  配置管理在多个项目之间协调。 
2:自动的  配置管理策略与持续集成策略紧密结合,团队成员有频繁提交的意识。一般采用乐观锁策略,原子提交。 
1:集成的  版本管理下的内容是受控的。通常在版本管理中的代码是可以编译的。开发人员能够访问到自己工作所需要的代码。开发人员按照一定的规则访问配置管理工具和提交代码。一般采用悲观锁策略。版本管理工具和构建过程集成在一起的。 
0:基本的  有基本的版本管理。但版本管理下的内容不全面,或者不足以支撑团队的开发。 
-1:无配置管理  没有配置管理。或者使用方式完全错误。 

常用实践和工具

持续集成看板7

问题:

我们需要让整个团队能够方便快捷的了解持续集成的状态。

上下文:

在完成了基本的构建基础设施的搭建之后,我们需要让团队成员及时获得持续集成的状态信息。传统的邮件方式可能会使人厌烦,或者错过重要的构建信息。

解决方案和实现:

安装一个显示器,将构建的状态信息以简明的方式展示在显示器上。将显示器放置在团队所有成员都能够很容易看到的地方。

个人构建中心

问题:

在某些情况下,构建环境的成本很高,而我们需要每一个开发人员提交之前完成一次个人构建。

上下文:

有些测试是依赖于设备的,而这些设备非常昂贵,并且配置复杂,所以无法给每个开发/测试人员一套构建环境。我们发现根据本地构建的理论模型,每个人的提交应该是串行的,这样我们实际上可以做到让这些人共享一套环境,但是从逻辑上就像是每个人有一套自己的环境一样。

解决方案:

在运行个人构建的时候将本地的代码同步到一台共享的机器上执行构建,构建完成后结果反馈给提交这次个人构建的人。

实现:

个人构建中心的实现有很多种方案。这些方案的区别主要在于如何将代码同步到个人构建中心服务器上。两种常见的方式:一个是使用rsync或者类似方式同步;另一个是使用分布式配置管理工具如git/hg同步。其拓扑结构如下:

第一种方式相对独立灵活;第二种方式稳定、高效,但是对于配置管理工具有依赖。

后果:

个人构建中心节省了大量的计算资源,同时也容易使得中心服务器成为单点失败的源头。一旦中心服务器出现问题,可能会导致团队的流程受到较大影响。

提交令牌

问题:

在实施本地构建的时候,向目标分支的提交应该是串行的,以避免构建被破坏后难以定位问题来源。但是团队往往缺乏一种有效的机制来保证这种串行。

上下文:

有些团队试图通过技术的手段来解决这个问题,比如通过配置管理上的锁机制,这种方式和乐观锁模式有较大冲突。有些团队通过团队内部沟通的方式解决, 比如谁提 交之前都会通知别人,或者通过持续集成监视器来了解当前的构建状态,以决定自己是否可以提交。这些方式各自有各自的适用情形,较容易理解。

解决方案和实现:

使用一个实物作为令牌,准备提交的代码的人首先取得令牌,当代码提交完成(包括相应的提交构建)之后,将令牌交还。令牌要醒目,并且移动方便。小型奖杯、毛绒玩具、较大的头饰(如下图)都是不错的令牌。

分阶段构建

问题:

在某些团队中完整构建所花费的时间可能很长,如果每次提交都运行完整的构建会浪费很多时间。

上下文:

随着持续集成的日益完善,我们往往会发现验证所花费的时间越来越长,而大部分验证趋于稳定,失败的情况很少见。通过技术手段缩短构建时间是解决问题的根本办法,但是缩短构建时间是一个耗时耗力的工作,很难短期内见效。

解决方案和实现:

将构建分为几个阶段执行,在本地构建中仅执行速度比较快、可信度比较高、出错概率比较大的验证。利用晚上或者其他合适的时间执行全面的验证——我们这次构建称为全量构建。需要注意的是,这种情况下仍然要保证提交构建和本地构建的一致性。

iAnalysis

iAnalysis是一款轻量级的持续集成报告工具。该工具的核心思想是将持续集成构建过程中产生的数据以趋势和对比的方式展示出来。正如前文所 说,我们在2009年的ThoughtWorks Away-Day上讨论了敏捷度量的话题,大家最后一致认为,数据有两种最基本的用法——横向对比和纵向对比。横向对比就是不同的人、不同的团队之间对 比;纵向对比就是现在和过去对比。iAnalysis正是这种思想的体现。

关于作者

肖鹏,ThoughtWorks资深咨询师,程序员,敏捷过程教练。拥有7年以上软件开发实践经验,多次担任大中型企业敏捷流程改进咨询和培训,服 务对象涉及通信设备制造、通信运营、互联网行业等。他关注于设计模式、架构模式、敏捷软件开发等领域,并致力于软件开发最佳实践的推广和应用。他曾参与翻 译《Visual Studio 2005技术大全》,主持翻译《面向模式的软件架构》第四卷和第五卷等图书。


1 目前Cruise的开发任务已经不在中国公司了。

2 实际上这篇文章介绍的是Matthew所在团队在2000年早些时候已经在使用的实践,ThoughtWorks中国公司的总经理郭晓先生当时也在这个团 队。

3 为了与ThoughtWorks常用的术语保持一致,部分术语与雷镇和熊节同学所译略有差别,下同。

4 笔者对其格式略作处理,与原文稍有出入。

5 Martin最近在自己的一篇博客上对几种流行的配置管理工具做了对比。

6 注意,本文并非为指出第一版的缺陷,只是通过对比来说明作者论文中重点的变化。

7 这里只是借用“看板”这个词的字面含义,与精益中的看板有区别。


感谢张凯峰对本文的审校。

给InfoQ中文站投稿或者参与内容翻译工作,请邮件至editors@cn.infoq.com。也欢迎大家加入到InfoQ中文站用户讨论组中与我们的编辑和其他读者朋友交流。

This entry was posted in Agile, Best Practices. Bookmark the permalink.

发表评论

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / 更改 )

Twitter picture

You are commenting using your Twitter account. Log Out / 更改 )

Facebook photo

You are commenting using your Facebook account. Log Out / 更改 )

Google+ photo

You are commenting using your Google+ account. Log Out / 更改 )

Connecting to %s