持续集成是一个程序开发的原则,它要求开发小组的每个成员频繁的集成他们的工作成果,这个频度通常是至少每天一次,有时甚至每天多次。每次的集成通过一个包含测试的build去尽快的探测潜在的错误。很多团队都发现这种原则能有效地减少集成问题,并能让他们更快的开发出cohesive(粘连性)的软件。这篇文章对持续集成的技术和用法做了一个快速的总结。
我清楚的记得我参与第一个大的软件项目的情形:当时我在英国一家大型电子公司做暑假实习生,我的经理,QA组的一员,给了我一个tour的职位,我和他一起进入了堆满立方体的仓库,我被告知这个项目已经开发了几年了,现在正在集成,而且已经集成了几个月了,我的向导告诉我没人知道集成完成还需要多久。从这件事情上,我学到了软件项目的一个通用的道理:集成是一个漫长且不可预测的过程。
但是好像不是那么回事。我在ThoughtWorks的同事们做的大多数项目(还有世界各地的人参与)根本就不把集成当成一回事。每个独立的开发者的工作可能每次就几个小时,对于一个共享的项目来讲,几分钟内就能把他们的工作集成进来,每次集成的错误很快就能发现并被修复。
这个相反的事实并不是使用一个昂贵而且复杂的工具的结果,它的本质依赖于在开发组的每个组员都遵循了一个简单的原则:在有代码管理的仓库中频繁的进行集成。
当我把这个原则刚开始讲给人们听时,反馈一般是:“你那套在我这没用”,“这样做也没多大用”。但当人们发现持续集成比听起来简单,而且给开发带来的巨大变化以后,不同的反馈就来了:“当然,我们当然这么做了,不敢相信你离开它还能活下去吗?”。
术语“持续集成”起源于极限编程开发,是它的12个基本原则之一。当我作为一个顾问在ThoughtWorks开始工作时,我就鼓励我工作的项目组使用这项技术。Matthew把我的建议彻底的贯彻到了实践中,我们的项目随着持续的进行,逐渐进入了我前面描述的把集成不当一回事的境界了。Matthew和我把我们的经验写下来形成了这边文章的初始版本,它已经成了我的网站里面最受欢迎的文章了。
虽然持续集成只是一项不需要特殊工具去实施的一个原则,但是我们发现使用持续集成的服务器以后会非常有帮助。最出名的服务器是CruiseControl,由ThoughtWorks的几个人构建的一个开源工具,现在在社区里广泛流传。最初的CruiseControl是JAVA写成的,现在为Microsoft的平台开发的CruiseControl.NET也开始使用了。
1 持续集成(CI)的特性
解释CI是什么,怎样去使用它,最好的方式是:给大家展示一个使用持续集成的原则进行开发的例子。假设我必须得为软件的一部分做些事情,不论任务是什么,同时假设任务很小,几个小时内就能完成(我们稍后会研究更大的任务和其它主题)。
我先获取了一份当前集成的代码到我本地的开发机器上,我使用了代码管理工具的检出就办到了。
我假设大家都用过代码管理工具,不然你就对上面的一段就不理解了。代码管理系统把一个项目所有的源代码都保存在一个仓库里。系统的当前状态始终是最新提交的代码,我们叫它主线(mainline),任何时候开发者都能把这个主线上的代码更新到自己机器上,我们叫它检出。开发者机器上的代码叫做工作拷贝(working copy)。
现在我获得了我的工作拷贝并在这上面来完成我的任务:改变产品代码,添加和更改自动测试。持续集成假设了一个软件中的高度自动化测试机制:我叫它自测代码。他们通常使用一个Xunit测试框架的版本。
一旦我完成(或在工作过程中的任何一点),我就在我的开发机器上执行一个自动化的构建(build):它能把我的代码编译,链接成可执行,并运行自动化测试。只有所有的编译和测试都没有错误,我们就认为是成功的构建了。
构建成功后,我就可以考虑把我做的工作提交代码仓库了,但是同时有可能其他人也会把工作提交到主线,所以,我得先更新我的工作拷贝然后再重新构建。如果我的工作和主线的代码有冲突的话,那么在编译或测试的时候就会报错。在这种情况下,我就得去修复这个问题,直到我能将我的工作拷贝同步的构建成功。
一旦我的工作拷贝构建成功我就可以提交更新仓库的。
但是我的提交并不意味着我完成了我的工作,我还得需要把主线的代码进行构建,因为我有可能把有些工作遗忘到我的机器上,而仓库没有得到正确完整的更新。只有当我的工作提交后的代码能够成功的构建,我的工作才算完成。集成构建的工作可以是我手工的执行,也可以是用CruiseControl来自动执行。
如果二个开发者之间的工作冲突,通常会在第二个开发者提交工作拷贝时发现。在这个时候最重要的事情就是去修复这个冲突,直到集成构建成功。你绝对不能把这种冲突存放很久,一个好的团队会每天都确保自己的集成构建正确,即使有失败发生,也会很快修复。
2 持续集成的原则
上面讲的只是CI的简单描述,要想能很顺利的工作还需要注意更多。我们下面主要集中来说说有效的CI的几个关键原则。
2.1 维护唯一的代码仓库
一个软件会包括很多文件,需要把他们像音符一样编制在一起才能生成产品。要想把握全局需要花费很大的气力,特别是当这个项目有很多人参与时。所以那些开发时间较长的项目团队都会有构建工具去管理项目。这些工具有代码管理工具,配置管理,版本控制,仓库,还有各种各样的名字,它们已经是项目的一个完整的组成部分。
所以一个基本的原则是确保你有一个最近版本的代码管理系统。成本不再需要考虑,因为开源工具已经唾手可得。当前一个可选的开源仓库是Subversion。(老版本的是CVS,虽然还广泛使用,但Subversion更摩登一些),商业的代码管理工具我听得多一些的是Perforce。
一旦你有了代码管理工具,你就要告诉大家怎么通过它去获取代码。不要出现有人问某某文件在哪儿的问题,什么东西都应该在代码仓库里。
虽然很多项目组都使用代码仓库,但一个常见的错误是他们没有把什么都放在仓库里。如果大家使用仓库,就应该把用于构建的东西都放到里面去:测试脚本,属性文件,数据库,安装脚本和第三方库。我还知道有人把编译器都检入(check in)仓库的。基本的原则是,对于一个处女机,只要对仓库执行检出,就能构建一个完整的开发平台。(处女机上只需一些较大的,复杂的软件如操作系统,JAVA开发环境,数据库等)
你最好把构建所必须的东西都放进资源控制系统里,你也可以放些其他开发者经常会用到的东西,IDE setups也可以放进去,这样大家可以共享同一个IDE了。
版本控制系统的一个特性是能让你创建多个分支去出来不同的开发流。这是个十分有用的特性,但是过分使用的话会把大家带入困境。尽量让分支小。特别的是要有一个主线:一个唯一的程序分支,每个人都从这个主线开始工作(分支往往会是先前一个已发布版本的bug修复版或临时的试验版)。
2.2 自动构建
让现有代码形成一个运行的系统通常是个很复杂的过程:编译,移动文件,装载表到数据库等等。但是大部分的工作都可以自动化,也需要自动化。要人们去敲陌生的指令,或是点击对话框是个费时又易出错的事情。
自动化的构建环境是通用系统的一个特性,Unix世界已经使用make数十年了,JAVA社区开发的ANT,.NET社区有Nant,现在又有了MSBuild。确保你能通过一个简单的命令调用这些脚本来构建和启动你的系统。
一个常见的错误是没有把所有的事情都包括到自动化构建中去。构建应该包括把数据库表从仓库中取出然后更新到执行环境中。我又要引用我之前的规则:每个人在处女机上只要通过一个简单的命令就能检出代码并获得一个可运行的系统。
构建脚本有许多种,根据不同的平台,社区会有不同,虽然我们JAVA项目通常用ANT,但是也有人用Ruby(Ruby Rake系统是个很好的构建脚本工具)。
一个大的构建通常十分耗时,但是如果你只是做了一个小的改动,你不必全部重新构建。所有好的构建系统能分析什么需要去改变。通常的方式是检查代码或目标对象的日期,只编译那些日期更新过的。这依赖于他们的机制,如果一个目标文件改变了,那么那些与之相关(或依赖它)的对象也要重新构建。有的编译器能处理这种情况,有的则不能。
根据你想做的事情,你可以构建一个系统带或者不带测试代码,或者有不同的测试集。一些组件能单独的构建,一个构建脚本应该能根据不同情况提供可选的目标。
我们有很多人都使用IDE,大多数IDE都有一些代码构建的工具内嵌其中。然而这些工具总是依赖于IDE而且很脆弱,如果那些没有IDE的开发者就可能没办法进行了。最好还是用一个服务器来使用ANT来确保构建的进行。
2.3 让你的构建能自测
构建,传统意义上意味着编译,链接和一些使程序能执行的工作。一个程序能运行,但不意味着它能做正确的事情。
获取bug的好的方式是在build过程中包含自动测试。测试不可能完美,但是它确实能捕获bug,这就够了。实际上,极限编程和测试驱动开发已经为自测代码的推广树立了好的榜样,越来越多的人看到了这项技术的价值。
经常看我文章的人都知道,我是极限编程和测试驱动开发的忠实fans,然而我要强调的是:不是所有这些方法都能获得自测代码的好处。这些方法都强调在你编码之前写测试,然后让你的编码能通过这些测试,在这种模式下,测试工作跟设计系统所做的工作一样多。这是个好事情,但是对于持续集成的目的来说不是必要的,我们没有那么多的自测代码需求(尽管测试驱动开发是我生成自测代码的偏爱的方式)。
对于自测代码,你需要一套自动测试来检查大段代码中的bug,这些测试需要能被从简单的命令中剔除和能够自测。测试的结果能够指示错误的所在。自测的失败会造成build的失败。
这些年来,测试驱动开发的广泛使用让开源工具XUnit family在测试中的使用也普及开来,Xunit工具在ThoughtWorks的工作实践中已经证明非常有用,我总是建议人们去使用它们。就因为这些工具(Kent Beck是我们的先导)使我们能很轻易的构建一个完整的自测环境。
Xunit工具肯定是你进行代码自测的开始点。你也应该看看其它的一些更注重end-to-end测试的工具:FIT, Selenium, Sahi, Watir, FITnesse等等。
当然你别指望测试能找到一切。有句话叫做:测试不能证明BUGS不存在,完美的测试总是不能写出来,而不完美的测试却可以执行多遍帮你去找到错误。
2.4 每个人每天都提交代码
集成主要是沟通,它能让开发者知道彼此所作的改动。
开发者提交代码的前提是他们能把自己的代码正确的build,当然也包括build中的测试。提交的过程是:先把自己的工作拷贝与主线同步,然后找到并解决其中的可能的冲突,然后在自己本地机器上build,如果能够通过的话,就可以自由的提交代码到主线了。
通过频繁的这样做,开发者能很快的发现二个开发者间的冲突。解决这个问题的关键就是尽快的发现冲突。当开发者几小时提交一次,那么冲突就能在几小时内发现,这个时候开发者可能还没走远,问题就容易解决了。而且这几个小时内变动的地方有限,bug的藏身之处也就容易发现了。你可以用diff-debugging去帮你找到bug。
我的原则是每个开发者每天都得提交他们的代码,当然如果大家提交的比这更频繁,那么冲突也就更容易发现了。但通常人们发现自己无法在几小时内就完成一项有意义的变动。
2.5 每次提交都在主线集成的机器上做build
使用每日提交,团队会频繁的进行测试build。如果大家在提交前没有更新就做了build或者开发者的机器环境不一样的话,大家build的结果会不一样。所以你得在集成了主线的代码的机器上去做build,这样你就需要一个持续集成的服务器。
持续集成的服务器就像个数据仓库的监视器,每次的代码提交都会自动的触发checkout代码到集成服务器上,初始化build,并将结果通知提交者。提交工作直到收到邮件通知才算结束。
持续集成的引入会给大家的工作习惯带来冲击,大家会不习惯去总是留意主线的build状态。
2.6 让build快点
持续集成的要点是提供快速的反馈,没有什么比build耗时更能吸CI活动的血了。但是对于什么是耗时的build,这个概念很难界定,根据极限编程的原则,10分钟应该是个好的分界线了。大多数好的项目都不会超过这个时间,因为此时每节约1分钟就为全体开发人员每人都节约了一份钟。既然集成build要经常的做,那么累积起来的时间就更多了。
大多数的build都是时序的进行的,提交触发一个主线的build,然后由它来依次的调用其他的build。其实这其中耗时最多的还是测试,但是如果减少测试的话,虽然能提高build速度,但探测bug的能力就下降了。
一个好的方法是进行一个二步的build,第一步编译并在本地进行单元测试,这样的测试一般很快,不会超过10分钟的原则。但是那些参杂在内部交互中的bug就很难发现了。第二步是运行一个不同的测试集(针对真实的数据库)的build,这个可能比较费时了。
这种情况下,大家可以把第一步作为提交build,把这作为主CI循环。第二步build是个次要的build,它能把最新的好的提交build检出来进行测试。如果第二步的build失败,并不停止一切,而是让开发小组尽快的修复这个失败,同时保持其他的提交build继续运行。关键的问题是这二步的同步。
这只是个二步build的例子,其他多步build的例子道理是一样的,只要遵循同步和迅速修复错误的原则,完全可以实现多步build的同步。
2.7 克隆一个产品环境进行测试
因为最终的程序会部署到产品环境中去,因此在不同的环境下进行测试的话,会存在一些因环境变化造成失败的风险。
这样你就得找个同产品环境装了一样的操作系统,同样版本数据库,同样版本的第三方类库(即使你不会真的用到它)的环境,使用同样的ip地址和端口,运行在同样的硬件配置上。
也许有时这种产品环境的复制很是花费,或者根本不可能完全做到,但你应该尽可能的去使用这条原则。
2.8 让每个人都能容易的获取最新版的可执行程序
软件开发一个很重要的部分是你要确信你build了正确的软件。人们往往很难提前知道他需要什么,什么需要被改正,但是往往问题出现时,他才知道改变是多么需要。敏捷的开发过程详尽的利用了人们的这种行为。
为了帮助工作,每个项目组的人都应该能够去获取一个最新版的可执行程序而且确保能运行:演示,探测测试,或是仅仅看看这周有什么变动。
找个大家都知道而且能获取最新版的项目的地方就是很明显的事情了。也许把几个版本的可执行程序放同一个地方会有很有用。这样就可以方便比较新开发的特性和提交不同的代码版本了。
2.9 让每个人都知道正在发生什么
持续集成所做的一切都是为了沟通,所以你要确保所有人都知道系统现在的状态,目前已经做了那些变动。
沟通中最重要的一件事是主线的build状态。如果你正在使用CruiseControl,这上面有个build,它会告诉你build的状态和上次主线build的状态。许多小组喜欢把这些消息更明显化,build成功的话就是绿灯,失败就出现红灯。(lava lamps)
如果你做一个手工的CI过程,这种可视化仍然是本质。CruiseControl提供了一个网站来指示:谁在build,他做了哪些改动。同时它也提供了项目小组系统最近变更的历史记录。小组的领导就喜欢拿这些东西来看是哪些人做了哪些事。
使用网站的另一个好处是:那些不在一起工作的人也能看到目前项目的状态。我一般是喜欢所有围绕一个项目工作的人都坐在一起,但是全球所有人有时都会把目光放在一些事情身上。
好的信息显示不仅仅是在计算机屏幕上,你完全可以有其他的途径。我们组曾经有个项目1个月都没有1次成功的build了,我将一个大的日历张贴起来,每次build通过就划一个绿灯,失败的话就划个红灯。这样大家每天就很清楚今天该做的事情了。
2.10 自动部署
要做持续集成,你需要多个环境,一个运行提交测试,其它的运行次要测试。如果你需要每天把代码在这些环境间搬来搬去,你就要把它自动化。拥有能让你把应用部署到这些环境的脚本是十分重要的。
接下来的事情就是你也要有脚本能让你迅速的把这些应用部署到产品环境中。你可能不必每天都升级产品环境,但是自动化的部署能让你的工作加速而且减少错误。而且这也是一劳永逸的事情。
有可能的话你尽量让你的自动部署支持部署回滚,这样就可以鼓励人们去进行更频繁的部署,同时能加强系统的安全系数。Ruby on rails社区开发了一个工具叫Capistrano就是个好的例子。
回滚中最困难的事情是数据库迁移,数据库改变是件很恐怖的事情,你无法保证你的数据能正确的迁移。Evolutionary Database Design这篇文章详细的描述了自动重构和数据库迁移的技术,在它里面它试着去获取在Pramod and Scott Amblers的书里介绍的重构数据库的详细信息。
自动部署还一个好处是能让你在正式部署前做些测试试验,让你的用户去体验新的特性和新的用户界面,由此来给你提交最好的决定做参考。自动化部署是CI的一个好的原则。
3 持续集成的好处
我认为使用持续集成的最大的好处是减少风险。当我的思绪回到我前面提到的那个项目,他们的工作是快结束了,但谁也不知道他们什么时候能真正结束。
不连续,不及时的集成的最大的问题是,很难预测项目要花多长时间,最糟的是,你不知道你究竟还要走多远。结果就是,你想个盲人一样置身于项目的大森林里,虽然你是个很少迷路的人。
持续集成完全能解决这些问题,没有漫长的集成工作,所有的盲点尽在掌握。无论什么时候你都知道你在那儿,你的工作是什么,问题是什么,那些bug就跟虱子矗立在光头一样那么明显。
持续集成不能完全摆脱bug,但它能让你尽快找到并消灭它们。就因为这个原因我钟爱自测代码。
Bug是累积的,bug越多你越难消灭它们。有时候bug是相互交叉的,多个bug在一起的时候,往往消灭一个,会带出更多(拔出萝卜带出泥)。有时候这也是心理问题,人们有时会对存在大量bug的代码产生恐惧心理,这就是所谓的Broken Windows 综合症(Broken Windows syndrome)。
持续集成的效果与你的测试集的质量是成正相关的,要找出那些很深层次的bug,意味着你得不断提高你测试集的质量。
如果你使用持续集成,它就能帮你跨越频繁的部署的障碍。频繁的部署是很有意义的,它能让你的用户很快的发现系统新的特性,然后给你针对这些特性迅速的反馈,这样就能让你的开发循环有更好的协同工作。这就能让你的用户和你们的开发能更好的沟通——我认为这是个成功的软件开发中最大的障碍。
4 持续集成入门
也许你现在特别想试验一下持续集成,那么应该从那儿开始呢?我上面所列的是持续集成的所有好处,你不必一开始就都将它们使用进来。这儿没有标准答案,更多的是依赖于你们的项目和开发小组。但是这儿有些事情我们可以学着去试试。
第一步是让build自动化。把你所要的东西都放到代码管理器中,这样你就能用一个简单的命令build整个系统。对于项目来讲这不是一个镜像,而是其他所有工作的本源。刚开始你可以偶尔的进行build,或者做自动的晚间build。也许一个晚间自动build是你实施持续集成的最好开始。
在你的build中引入自动化测试。试着标识出那些容易出错的主要区域,然后把自动测试加入其中来检测错误。特别的是在一个已经存在的项目当中很难快速的获得一个测试用例,这很花费时间,但你能从一些小地方开始。
试着加速你的提交build,一个要花费数小时的持续集成build比没有做持续集成好,但是能够坐下来想办法将build时间压缩到10分钟的话,那就再好不过了。这通常就需要你对你系统中较慢的地方所依赖的代码做些漂亮的外科手术了。
如果你开始一个项目,那么最好一开始就进行持续集成。留意你的build时间,迅速采取行动让它尽量不要超过10分钟。只有这样才不至于让你的系统变得庞大以后再来做减肥的工作。
找到那些曾经做过持续集成的人来给予你帮助。一项新技术是很难去解释清楚的,你无法想象结果是什么,尽管找个顾问会花费很多,但是如果你不这样做的话你就会损失你的时间和生产效率。
5 最后的想法
自从Matt和我在这个网站上写了那份原始版本的文章,持续集成已经成为了软件开发的主流技术。现在ThoughtWorks的项目没有了它很难进行。我也看到世界各地的很多人都在使用CI。我很少听到这种方法的副作用,至少不像极限编程的其他原则那么多争议。
如果你还没有使用持续集成,那么我强烈的建议你试一试。如果你已经在使用,那么上面的一些观点也许能帮助你将集成更有效的执行。在过去的几年里,我学到了很多持续集成的知识,我希望将来能有更大的提高。