持续集成(Continuous-Integration)
原文是 Martin Flower 于 2000 年 9 月 10 日写的《Continuous Integration》。
持续集成(Continous Integration)是一种软件开发实践,团队成员频繁地集成他们的工作,通常每人每天至少集成一次,从而每天有多次集成。每次集成使用自动化构建(含测试)来最快速度地查找错误。许多团队发现持续集成显著地降低了集成错误,使团队更加迅速地开发内聚的软件。这篇文章快速对持续集成进行了整体介绍,以及它的相关技巧和应用情况。
我依然清晰记得我第一次接触大型软件项目那天。我在一个大型的电子公司进行暑期实习。我的上司——测试部门的一员,带我参观了一个网站,然后我们走进了一个堆满立方体的仓库。我被告知这个项目已经开发了若干年,目前正在集成,而且已经集成了好几个月了。我的向导告诉我没有人知道这个项目还要多久才能完成集成。从这件事情上我学到一个软件项目的共同故事:集成是一个漫长而且难以预测的过程。
但是其实不需要这样。多数我Thoughtworks的同事完成的项目,和世界上很多其他团队,集成对他们来说不需要这么复杂。 每个人的工作其实就只有几个小时领先共享项目状态,并且只需要几分钟就可以集成回去。所有集成错误都可以被迅速发现并且修复。
这样反差并不是因为使用了昂贵复杂的工具。这个结果的本质是一个简单的实践——团的的每个成员都频繁地向托管代码仓库集成,通常是每天。
当我向人们描述这个实践的时候,我通常得到两种反馈。“这不管用”和“这样做不会有什么差别”。但当人们开始尝试会发现比想象容易,而且会让开发有巨大的差别。于是第三种常见的反馈是:“是的,我们做持续集成——没有它我们怎么活? ”
持续集成这个词,起源于极限编程,是当时原始的12种实践之一。当我开始在Thoughtworks当顾问,我鼓励我所在的项目使用这种技术。Matthew Foemmel把我模糊的布道词转化成了行动,我们最后看到项目的集成从稀少复杂的过程变成了不重要的事情。Matthew和我一起在这篇文章的最初版本中写了我们的经验,并成为了我个人网站中最火的文章之一。
尽管持续集成是一个不需要特别工具去部署的实践,我们发现使用集成服务器是非常有用的。我知道的最好的服务器是CruiseControl(小编:作者当时写这篇文章的时候CruseControl还是非常先进的工具,我们现在github上大家有很多非常出色的集成工具了),一个有Thoughtworks几个员工写的开源工具,后来由社区维护。 由此开始很多集成工具都出现了,也包括Thoughtworks的Cruise和Thoughtworks Studio。
通过持续集成构建一个特性
对我来说最容易讲清楚“什么是持续集成?持续集成如何工作?”的方法是举一个例子告诉大家持续集成是如何同开发一个小的特性一起工作的。假设我们要开发一小部分程序,至于任务是什么并不重要,我们假设它很小,可以在几个小时内完成。(我们将在后面讲更长的任务,和其他的问题)
我首先把目前集成好的代码克隆到本地。我通过一个代码管理工具实现这一步,我从主分支中检出一个副本。
上述的方法对那些使用版本控制软件的人来说是成立的,对于那些不用版本控制的人来说简直是胡说八道。所以让我在这里简单地解释一下。一个代码版本控制软件将所有的代码集中管理在一个仓库里。仓库当前的状态通常被成为主线(主分支)。任何时候一个开发者可以从主分支中拷贝一个工作副本到本地,这个操作叫做检出。开发者机器上的代码叫做开发副本。(多数时间你可以把工作副本更新成主线——实际上和检出是一样的)
这是我拿着我的工作副本完成我的工作。这可能包括修改代码和编写(或修改)自动化测试脚本。持续集成假设软件中的测试是高度自动化的:我称作自我测试的代码。这些过程通常使用XUnit这种普遍的测试框架(小编:目前前端用mocha/jest等等比较多)。
当我完成之后(通常在不同的点),我执行在我的开发机器上执行一次自动化构建,它使用工作副本的代码,编译并且链接它们成为可执行文件,然后执行自动化测试。只有当整个编译和测试用例都没有错误,整个构建过程才算正确。
对一次正确的构建,我可以把变更提交到仓库。问题是,其他的人可能在我提交之前把变更提交到了主线。所以我要用他们的版本更新自己的工作副本,然后重新构建。如果他们的变更和我的产生了冲突,那么在编译和测试的过程都会产生错误提示。而修复这些错误直到我的工作副本可以构建通过直到与主线同步是我的责任。
当我把我的工作副本正确地同步并最终将变动提交到了主线,代码仓库也就更新了。
然而提交并不意味着我工作的结束。这时我们会在集成服务器上再次构建主线代码(小编:之前是本地构建)。只有这次构建成功,我们才说变更被完成了。这里经常会出现我从我本地少提交了一些代码,于是仓库没有被正确更新。所以只有在我提交的变更在集成服务器上构建成功了,我的工作才完成了。这个集成过程可以由我手动完成,也可以由Cruise自动完成。
如果在两个开发者之间产生了冲突,通常是第二个开发者提交代码的时候会捕获到。如果不是集成构建失败的话,错误也不会被马上反馈出来。这时最重要的事情就是修复错误,然后让构建再次正常工作。在一个持续集成环境,你不应该让任何一次集成停留在失败状态过长。优秀的团队每天都会有好几次通过的集成测试。而错误的构建经常会出现,但应该马山被修复。
这样做的结果就是总是存在一个只有少量bug的稳定的版本。所有的迭代都从这个版本开始,又不会离这个版本太远,否则会需要大量的工作再集成回去。这样,更少的时间用在了查找bug上,因为bug总是很快就暴露了出来。
持续集成的实践
上面的故事是关于持续集成的概述和它如何在日常工作中被运用。当然,让一切顺利工作需要更多的知识。接下来我会聚焦在让持续集成高效工作的关键实践上。
维护一个单一源的仓库
编译一个项目,有一大堆文件需要被组织工作在一起。跟踪每一个文件的变化需要很大的努力,特别是有很多人一起开发的时候。因此,多年来,软件开发团队创造了工具去处理这件事情。这样的工具——被称为源代码管理工具、配置管理和版本控制系统、仓库或其他名称——他们是多数项目不可分割的一部分。悲剧是,他们不是所有项目的一部分。虽然非常稀少,我还是遇到过少量项目没有使用版本控制工具而是用了一些杂乱的共享盘。
因为作为一个简单的基础,请确保你已经拥有了一个像样的源代码管理系统。成本不是问题,因为有好用的开源工具。选择当前开源的存储仓库是svn(小编:当时作者写文章的年代较早,估计GIT还没有普及)。有趣的是,当我和程序员聊天的时候,发现大多数商业源代码管理工具还不如SVN。唯一一个我听到过有用的商业工具是Perforce。
当你拥有了源代码管理工具,确保每个人可以从固定的地方获取到源代码。没有人会问:“XXX文件在哪里?“所有的文件都在仓库里。
很多团队的共同错误是他们没有把所有的东西都放到代码仓库里。如果人们使用仓库,把一个文件放进去,那么编译项目的所有脚本也应该被放进去,包括:测试脚本、属性文件、数据库概要、安装脚本和第三方库。我知道一些项目会把编译器也放到仓库里(在早期C++编译器各种奇怪的时代)。最基本的经验法则是你可以在一台新机器上,将代码从仓库中检出,然后对整个系统进行构建。新机器上通常只有最小的配置,通常是一些大的、复杂不易安装的东西——比如操作系统、java环境、数据库系统等等。
你必须把所有用来构建源代码的东西都放到版本控制系统里,当然你也可以把大家常用的东西也放到版本控制里。IDE的配置文件很适合放到版本控制里,这样大家就可以很方便的分享相同的配置。
版本控制工具的一个特性是它允许你创建多个分支去处理不同的开发流。这是一个有用而且必要的特征——但它通常也被过度使用,让人陷入麻烦。尽量保证你利用最少的分支。通常会有一个主线:目前正在开发项目的分支。大多数时候每个人都在这条主线上工作。(比较合理的分支包括之前产品的BUG修复、临时实验等。) (小编:现在的GIT FLOW其实是每个Feature都会有自己的分支,这个可能略微领先于作者写这篇文章的时代)
通常你应该把需要构建的所有东西都放到版本控制里,而不包括任何构建的产出(中间产物等)。有些人会把构建产出放到版本控制里,但我感觉这是一种信号(smell)——表明一个深层次的问题,就是没法可靠的重新构建。
自动化构建
把源代码变成可以运行的系统通常是一个非常复杂的过程,包括编译、移动文件、加载配置到数据库等等。然而,就像和软件开发中的大多数项目一样可以被自动化——作为结果也应该被自动化。让人们敲奇怪的指令,或者点击一个一个的对话框,会滋生错误。
自动化构建环境是系统共同特征。Unix系使用make作为工具;Java社区使用ant(小编:目前是maven和gradle);.Net系使用MSBuild和Nant。确认你可以使用一条命令使用这些脚本编译和运行你的系统。
一个常见的错误是没有把所有的东西都引入自动化构建。构建应当包括从仓库中获取数据库结构并在运行环境执行。我将阐述我早期的经验法则:任何一个人都可以使用一台新机器,从代码仓库中检出代码,发出一条指令,然后整个系统都运行起来了。
构建脚本通常以各种形式出现,通常是针对特别的平台或社区的,但是其实不用这样。尽管我们大部分的java项目使用ant,部分项目使用ruby构建(Ruby Rake是一个出色的构建系统)。我们还通过使用ant构建微软的COM项目获得了很大的价值。
一个大型的构建通常比较耗费时间,如果你只进行了少量的更改,你不希望把其中每一步都完成。所以一个好的构建工具会分析整个过程中哪些是需要被变更的。一个通用的过程是检查源代码和类文件的日期,然后只更新其中日期晚于上次构建的部分。依赖关系这时会变得棘手:如果一个对象发生了变化,那么依赖它的对象也需要被重新构建。编译器可能会处理这类问题,但也可能不处理。
根据你的需求,你可能需要不同的构建方法。你可能会构建一个系统有(或没有)测试代码。一些组件可能会独立构建。构建脚本允许你为不同的情况构建不同的目标。
我们通常会使用IDE,IDE中通常会自带一种构建管理过程。然而这些文件通常是IDE的一部分,而且很脆弱。此外他们需要IDE才能工作。IDE用户创建他们的项目并完成他们个人的开发当然没有问题。但是,必须要有一个服务器上可用的主构建,而且可以通过其他脚本执行。所以对于一个java项目我们可以让开发人员使用IDE构建,但是主构建需要使用ant确保它可以在开发服务器上执行。
构建自测
传统上一次构建意味着编译、链接……以及需要所有让程序跑起来上其他的材料。 一个程序跑起来了,但并不不意味着它做了正确的事情。现代的静态语言可以过滤很多错误,但更多错误从这张网中溜了过去。
在构建过程中执行自动化测试一种快速有效捕获异常的方式。测试并非完美,但它可以捕获大量异常,已经足够好用了。特别是极限编程和测试驱动开发已经完成了非常棒的自我推广——让人们看到了测试代码的价值。
我的普通读者知道我是一个极限编程和测试驱动开发的分粉丝——然而我需要强调对从自测代码中获益而言,这两种方式都不是必须的。这两种方法都要求在写代码之前先写针对代码的测试——在这种模式下测试更多是在探索整个系统的设计,同时也是在捕获bug。这是个好东西,但对于持续集成的目的来说,我们对自测代码的需求是较低的。(尽管测试驱动开发是我喜欢的一种方式)
对于自测代码,你需要有一个测试套件检查代码仓库中的大部分代码。测试需要可以从一个简单的命令开始,然后进行自我检查。如果测试没有通过,需要能反馈测试结果。对一个自测的构建,如果测试没有通过,构建也会失败。
在最近的几年中,越来越受欢迎的测试驱动开发,让开源工具XUnit家族变得流行,成为了构建自测的典范。XUnit在Thoughtworks中被证明非常有用,而且我总是向大家建议去用它。这些工具,由Kent Beck(小编:极限编程的创立者)开创,让你非常方便地创造一整套测试环境。
XUnit工具当然是写自测代码的起点。你还需要去注意其他端到端的测试,目前有一系列这样的工具包括FIT,Selenium,Sahi,Watir,FITness等,还有很多其他的我没有列出。
当然你不应该希望测试可以找到所有的问题。大家通常说:测试不代表没有BUG。无论如何,完美不是你在自测构建中获得的唯一回报。不完美的测试,频繁地被执行,比完美的测试从未被执行强。
每个人每天都提交代码到主线
集成主要是要去沟通。集成让开发者告诉其他开发者自己改了什么。频繁地沟通让人们快速了解变化和发展。
开发者提交代码到主线的前提是他们可以正确的构建代码。这当然包括通过自动化测试。和所有的开发周期一样,开发工作者首先更新他们的工作副本,将主线代码同步,解决冲突,然后在本地机器上构建。如果构建成功,他们可以将代码同步到主线。
通过频繁地进行这项操作,开发人员会找到他们的冲突。这是修复问题和快速定位问题的关键。当开发者们每个小时都在提交代码,冲突可以在它发生的几个小时内被发现,在这时它刚刚发生所以还是比较容易被修复的。如果冲突在数周都没有被发现,那是很难被修复的。
在你更新本地副本时构建意味着你同时检测编译冲突和文本冲突。因为构建是自测的,你同时检测到运行代码的冲突。如果后者的冲突长时间没有被检测到,会更让人尴尬。因为在每次提交之间还有数小时,这里有太多的空间让这种错误去隐藏。此外因为变更量不大,你可以使用差异调试(diff-debug)法去查找Bug。
通常我的实践原则是每个开发人员必须每天提交代码。在实践中如果开发人员更加频繁地提交会非常有用。你提交得越频繁,给冲突和错误留的空间越小,你修复错误的速度就越快。
频繁提交让开发者们将他们的工作分解成只有数小时的小块。这样有利于跟踪进度和提供对进度的感知。通常一开始人们会感觉到很难再几小时内做一些有意义的事情,但我们发现指导和实践能够有效帮助他们学习。
每次提交应该在集成服务器上构建
使用每天的提交,团队得到频繁通过测试的构建。这意味着主线在健康的状态。然而在实践中,事情仍然会有问题。一个引起问题的原因是纪律,人们在提交之前不做更新和构建。另一个原因是开发环境和线上环境的差别。
因此,你应该去确保常规构建发生在集成服务器,而且只有在集成服务器上的构架成功了才认为提交被完成了。这件事情,谁提交代码是谁的责任,所以提交代码的程序员需要监控监控主线构建的结果,然后在发生中断时修复问题。这样做的一个必然结果就是在主线构建还没有通过的时候,你不应该回家。
这里有两种方式确保这个问题:使用手工构建或者使用集成机。
手工构建很好解释。本质上它和在本地构建,然后提交仓库是相似的。就是开发人员登录到构建机上,从代码仓库检出主线(里面包含了他最后一次提交),然后开始集成构建。开发人员盯着构建过程,如果成功了,他的提交就成功了(也可以参考James Shore的Continuous Integration on a Dollar a Day)
一个持续集成服务器充当着代码仓库的监控角色。任何一次向代码仓库的提交,都伴随自动化地从代码仓库检出代码到集成服务器,初始化一次构建,然后通知提交者最后结果。提交者没有获得通知之前(通常是一封邮件)就不算完成。(小编:目前大多数公司其实都有完善的系统在通知,邮件仍然在被使用)
在Thoughtworks我们是持续集成服务器的忠实粉丝,实际上,我们领导了CruiseControl和CruiseControl.NET的原始开发——后来被广泛使用的持续集成服务器。从那时开始我们也做出了商业版的持续集成服务器Curise CI。
不是每个人都喜欢持续集成服务器。Jim Shore(小编:之前提到的James Shore,《敏捷开发艺术》的作者)给了一个极具说服力的例子(Continuous Integration is an Attitude, Not a Tool)为什么他喜欢手动集成。我同意他说的持续集成不仅仅是装几个工具,而是一整套的实践方案。但同样许多做持续集成的团队会觉得工具非常好用。
许多组织的构建建立在时间规划上,比如每天晚上。这和持续构建不是一回事,也不足以说是持续集成。持续集成的整体意义在于迅速发现问题。每天晚上构建,意味着在人们发现之前,Bug们存在了一天。一旦他们在系统中存在够久,那么发现就变得困难了。
马上修复中断的构建
持续集成的一个关键点是一旦构建失败,需要马上被修复。持续集成的整体意义在于开发人员总是在一个稳定的版本上开发。主线中断了并不是什么坏事,如果总是中断那需要去建议开发人员小心一点,在提交前先在自己本地构建。如果主线构建真的中断了,那么需要马上修复它。
我记得Kent Beck说过”没有任何一个任务比修复构建更重要“。这不意味着团队中的每个人需要停下他们正在做的去修复构建,而是说需要几个人把工作恢复正常。也就是说需要有一种意识,认为修复构建是一件高优先级的事情。
通常最快的修复方式是回滚到构建之前的版本,让系统返回上一个通过的状态。当然,一个团队应该尽量尝试不要在中断的构建上进行调试。除非造成中断的原因非常明显,否则应该去回滚版本然后在本地工作站去修复bug。
为了避免中断主线,可以使用一种叫做挂起头的技术(Pending head : bliki: PendingHead)
当团队引入持续集成,这通常是最难处理的事情之一。早期的团队很难养成主线构建的习惯,如果他们正在使用代码库。耐心和稳定的应用程序似乎会起作用,所以不要气馁。
让构建变快
持续集成的整体意义在于快速的获得反馈。没有什么比构建需要很长时间更加吸血的了。当然我得承认一个古怪的老家伙可能把长时间构建看成乐趣。多数我的同事认为需要一个小时的构建不可以被接受。我能记起团队梦想着很快完成构建——但偶尔我们会有时候我们达不到那样的速度。
对于大多数项目,10分钟构建是非常合理的。多数我们现代的项目都可以达到这个标准。这值得去投入经历,因为构建时间每减少一分钟,每一个程序在他提交代码的时候就节省了1分钟。因为这是频繁提交时间的叠加,所以它节省了很多时间。
如果你一开始就需要1个小时构建,那么前景是堪忧的。甚至新项目会让人望而生畏,然后考虑如何让构建更快。对于企业级应用,我们发现主要的瓶颈是测试——特别是测试中包含远程服务如比如数据库时。
也许最关键的步骤是在一个部署管道上开始工作。部署管道(也叫构建管道)背后的思想是,多次构建串行执行。提交代码到主线触发了第一次构建——这称为提交构建。提交构建是在有人提交代码到主线时需要的。提交构建需要执行得足够快,通常他会走一些捷径降低查找错误的能力。关键是要平衡查找错误的能力和执行速度,以便让提交构建足够的稳定,整个团队可以在这个基础上工作。
如果提交构建执行的很顺利,那么其他人就可以对当前工作保持自信。 当然,你还可以做进一步、更慢的测试。可以用额外的机器执行进一步、更加耗时的测试程序。
这种方式的一个简单例子就是双阶段管道。第一个阶段通常是执行本地的单元测试,伴随着完全可以离线的数据库。这样的测试通常非常快, 保持着10分钟的指导时间。然而,在大量交互,特备是需要真实数据才能测试的bug不会被测出来。第二阶段的构建执行不同的测试套件确实能够命中真实数据,以及更多端对端的行为。这样的套件可能需要几个小时去完成。
在这样的场景下,人们使用第一个阶段作为提交构建,并把它加入持续集成的循环。当系统可以从上次成功的提交构建中找到执行文件进行进一步的测试时,会进行第二阶段的构建。如果第二阶段的测试失败,它不具有“停止一切”的属性,但团队还是保持提交构建运行的同时尽快修复问题。
如果在第二阶段的测试中发现了一个BUG,这意味着你可以在提交构件时可以多做一个测试。你应该尽可能多的保证后面阶段中的错误会在第一阶段中形成测试,这样BUG会在第一阶段修复。用这种方法只要有错误漏过第一阶段,测试就会被加强。有的BUG无法用快速构建的测试找到,所以你会决定只在第二阶段中测试。幸运的是,多数情况你可以在提交构建阶段增加合适的测试。
这个例子是双阶段管道,但是这个基本原理可以被扩展成为任何数量的阶段。在提交构建后的其他构建也可以并发执行,所以当你需要两个小时跑第二阶段的测试,你可以用两台机器并发执行来提升响应性。通过使用并发的第二阶段构建,你可以引入包括性能测试在内的,任何的自动化测试到常规测试当中。
在生产环境克隆版中测试
测试的重点是在可控的条件下批量找到所有可能在生产环境中发生的问题。为了做到这点,生产的环境很重要。如果你在不同的环境测试,每一个不同的结果都可能在生产环节不会发生。
所以你希望你的测试环境对真实环境的模仿约真实越好。使用相同的数据库,相同的操作系统版本。把所有在生产环境的库都安装到测试环境,即便系统不使用它们。使用相同的IP和端口,并使用相同硬件。
当然,在实现过程中会有限制。如果你在开发桌面程序,在云端克隆所有的桌面、以及不同用户装的所有第三方软件是不现实的。同理一些生产环境是非常难复制的(当然我经常遇到假装为了节省成本没有复制适当难度的环境)。尽管这些束缚存在,你还是应该尽量更多的克隆生产环境,然后了你应该去了解每多妥协一个测试和生产之间的差异的风险。
如果你拥有一个简单的配置,而没有太多的笨拙的通信,你可以在一套模拟的环境执行你的提交构建。然而,在多数情况下,系统会响应很慢或者不能马上响应,你需要用到测试替身(小编:英文是Test double,就是类似那些dummy/fake/stubs/mocks的技术,让流程先通过测试)。所以通常为了开发速度考虑,会有一个非常人工的环境给提交测试,然后使用一个生产环境的克隆作为第二阶段的测试。
我发现大家越来越喜欢使用虚拟化技术,把测试环境结合在一起。虚拟机可以和所有必须的组成部分一起保存。这样安装最后一次构建和执行测试就变得更简单了。更有意义的是,它允许你在一台机器上并发执行多个测试,或者在一台机器上模拟网络中的多台机器。随着虚拟机性能问题逐渐被解决,这个选择变得越来越有意义。
让大家可以方便的得到最新版本的可执行文件
软件开发中非常困难的部分之一是确保你构建了正确的软件。我们发现很难指定你预先想知道什么,并且是正确的;人们跟容易看到一些不太正确的问题,然后提出如何去改变它。敏捷开发过程明确的希望利用这部分的人类行为。
为了实现上述过程,项目中的任何一个人都要可以拿到最后一个版本的可执行文件并能够执行它:去演示、探索性测试,或者简单看看本周都有哪些变更。
这个其实很容易实现:就是有个固定的地方让人们可以拿到可执行文件。把多个可执行文件放到这个仓库是非常有用的。对每一个最新的版本应该使用最近一次通过提交测试的可执行文件——这样的可执行文件会比较稳定,可以提供的提交套件比较强大。
如果你在遵循一个良好的迭代过程,那么在迭代结束设置一个构建环节是明智的。 演示通常需要演示者熟悉软件特性,所以最值得牺牲的是演示者知道如何操作的东西。
每个人都知道正在发生什么
持续集成都是关于沟通的问题,所以你需要让确保每个人都可以方便地看到系统当前的状态和发生的变更。
最需要沟通的事项之一,就是主线版本的状态。如果你使用Cruise,会有一个网页告诉你当前构建进行的状态和最后最后一次主线构建的结果。许多系统会把持续的显示和构建系统关联起来——通常成功的时候信号灯变成绿色,失败时红色。通常的一个设计是红绿的熔岩灯——不仅仅是展示状态,更是某种状态持续的时间。红色灯上的气泡表明这次构建已经在失败的状态停留太久。每个团队在构造传感器上都有自己的选择——当然你也可以选择一些好玩的(我最近看到有人用了一直跳舞的兔子)
如果你使用手动的持续集成,可视化依然很重要。对物理构建机进行监控可以显示主线的构建状态。通常构建的人可以在桌面上放一个构建记号(再一次,像橡胶鸡那样的傻事是一个不错的选择)。人们喜欢在构建成功的时候制造一点声音,比如说铃声。
当然,持续集成的web页面可以携带更多的信息。Cruise不仅仅展示谁在构建,还展示了他们改变了什么。Cruise提供变更的历史,让团队成员了解当前项目的变动。据我了解,项目领导喜欢使用它来了解人们正在做什么,和当前系统有怎样的变化。
另一个使用web站点的优势是那些在异地的团队可以了解项目进度。总的来说,我希望团队的每个人都坐在一起工作,但保不齐有一些外围人员喜欢关心具体事物。这还有助于将多个项目的信息汇总——提供一个简单自动化地状态。
优秀的信息展示不仅仅是在电脑屏幕上。我最喜欢的信息展示发生在一个正在使用持续集成的项目上,它有很长一段时间无法稳定构建,我们在墙上挂了一本日历,我们用方框标记一年中的每一天。每天测试人员用绿色的方框表示当天的构建稳定了,用红色的表示构建有问题。直到有一天日历展示了一种持续稳定的进步,后来绿色的方框占了绝大多数,最后日历就从墙上消失了——目的已成。
自动化部署
为了完成持续集成你需要多个环境,一个用来做提交测试,另一个用来做第二阶段测试。因为你每天需要把执行文件从一个环境到另一个环境多次,你希望自动化完成这一切。因此你需要脚本让你可以把应用发布到任何一个环境。
这样做的一个自然结果是,你还会有一个相似的脚本让你方便地把程序部署到生产环境中去。也许你不会每天都发布(尽管有的项目真的这样),但是自动化部署帮助你加快速度并避免错误。这也是一个便宜的选择,因为你知识用了部署测试环境的相同功能。
如果你在生产环境自动化部署,一个额外需要考虑的功能是自动化回滚。坏事总是发生,如果有棕色的物质撞击旋转的金属,那就赶快回滚到最后一个好的状态。可以自动化回滚也可以降低部署的紧张程度,鼓励人们多去部署,让新特性上线更加迅速。(RUBY社区开发了一个叫做Capistrano的工具帮助完成这类型的事情)
在集群环境我们会发现滚动部署将新版本一次更新到一个节点,然后在几个小时内慢慢将整个应用替换掉。
我尝试过的一个有意思的变种是把一个尝试发布部署给一个用户子集。然后团队根据这次发布的效果决定要不要对整个用户群上线。这允许你在做最终决定前,测试你的特性和用户界面。捆绑良好持续集成思想的自动化部署是完成这项工作的关键。
持续集成的好处
总的来说,我认为持续集成最大的和广泛的收益就是降低了风险。我的思想仍然回到了我在第一段中提到的那个软件项目。他们在那里有一个很长的项目,但是不知道还有多久才能完成。
延迟的集成问题是,很难预测需要多久去集成,也很难预测还需要多久。结果就是,你把自己置身于最紧张的一个项目盲点——即便你之前有如期完成的例子。
持续集成完全解决了这个问题。这里没有长时间的集成,你完全消灭了盲点。你总是知道你在哪里,那些在工作,哪些不工作了,还有那些系统中突出的bug。
BUG们——这些令人讨厌的东西,破坏了你的信息,打乱你的事件,毁灭你的声誉。BUG让用户对你生气。工作中出现bug让你很难进行后续的开发工作。
持续集成并不是避免了BUG,而是让他们更容易被发现和清除。这方面,很像自测代码。如果你写了一个BUG,并很快发现,那摆脱它就相对容易。因为你只改变了系统的一小部分,你不需要去看很远。因为产生BUG的部分,恰恰是你刚刚在开发的那小部分,它还存在于你的记忆里——让你更容易找到BUG。你也可以使用差异调试法——比较当前的版本和之前没有BUG的版本。
BUG也是积累的。你积累越多的BUG,你解决每一个需要花的事件就更长。这个可能的一个原因是你让BUG们产生了互动,当构建失败显示由多个错误造成——让每一个都很难被定位。这也有心理因素,就是当bug太多就没有动力去修改——一个被实用程序员称为“破窗综合症”的现象。
使用持续集成开发项目,结果就在生产环境和开发过程,bug显著减少。我需要强调这种受益程度取决于测试套件的好坏。你会发现构建一个显著的测试并不难。通常一个团队需要较长时间才能达到他们所能达到的低BUG水平。达到这个水平意味着你需要持续努力改善你的测试。
如果你使用持续集成,就移除了频繁发布的最大障碍。持续发布非常有价值,因为这让用户可以更快的获得新特征,然后更快的获取用户的反馈,这让整个研发循环的协作变得更强。 它打破客户和开发之间的障碍——我认为是软件开发中阻止成功的最大壁垒。
引入持续集成
当你想要引入持续集成时——从哪里开始呢? 使用全套我列出的实践可以给你完整的收益——但你不需要全部都开始。
但这里没有固定的食谱——更多依赖于你的配置和团队。但这里有一些我们做事的教训。
可选的一个第一步是让构建自动化。把所有你需要的东西都放入版本控制系统,这样你可以使用一条指令就构建整个系统。对于多数项目这不是一件小事——这是后续很多事情的基础。一开始你只是偶尔进行构建,或者在晚间做一个自动化的构建。当还不是持续集成的时候,自动化地每晚构建是一个不错的开始。
然后可以在你的构建中引入自动化测试。识别出问题的主要领域,然后让自动化测试覆盖这些错误。特别是在一个已经成熟的项目很难让测试套件马上很好地工作——构建测试是需要时间的。尽管如此,你必须从某地方开始,毕竟罗马不是一天建成的。
接下来,尝试提高提交构建的速度。需要几个小时的持续集成比没有强,但是可以做到10分钟内更好。这通常需要你认真地对代码做手术,因为你需要打破系统在执行较慢处的依赖关系。
如果你创建一个新的项目,就从一开始用持续集成。从你的系统构建时间超出10分钟原则后就开始持续关注。更早的行动,让重构发生在代码仓库变得非常大而成为你主要痛点之前。
上述有一定用处,然后找到一个之前做过持续集成的人来帮助你。就像大多数技术一样,如果你不知道最后的结果,是很难上手的。找一个导师会花钱,但是如果你不这么做,也会丧失时间和生产力在这上面。(免责声明/广告:我们Thoughtworks在这个领域提供顾问服务。毕竟我们犯过之前的大部分错误)
最后的思考
在我和Matt写了原来的文章的这几年,在软件开发中持续集成已经变成了一个主流技术。现在Thoughtworks所有的项目都离不开它——然后我们看全世界其他团队使用持续集成。我从来没有对这项技术悲观过——不像对某些有争议的极限编程实践。
如果你还没有用过持续集成,我强烈建议你尝试一下。如果你已经使用了,希望这篇文章中的一些点可以帮助你提高效率。这几年,我们从持续集成中学习了很多,我希望这里有更多需要学习和提高的。
进一步的阅读
像这样的一篇文章只能覆盖这么多地方,但这是一个非常重要的话题。所以在我的网站上我创建了一个向导页面,给你更多的信息:Delivery Guide。
想要更加详细地探索持续集成,我推荐Paul Duvall冠名的图书Continuous Integration(获得了震撼大奖)。想要了解更加广泛的的过程,请看 Jez Humble 和 Dave Farley的书籍 ——这本书也在震撼大奖上打败了我。