每日构筑,持续性构筑的重要性

作者: Richard Sun (版权所有,未经许可严禁转载)

我现在实在难以想像一个开发团队如何能够面对长期不对整个项目进行构筑这个现象。如果这样的团队一星期甚至一个月才进行一次构筑,这样的团队多久才对整个系统进行测试啊?我无法想像这种现象如何能够持续下去。在Intuit,全美个人财务管理,个人税表软件提供商,我所在的开发组,持续性的开发和集成是通过持续性构筑来实现的。每次开发和QA用Perforce提交代码,CruiseControl就会被提醒说新的代码被提交到源码库里,可以进行一次新的构筑来保证源码可以通过构筑,通过单元测试。我们利用Maven来构筑Java源码和C#源码。在我们的组里开发者,QA基本上能做到完全保证自己的提交不造成构筑失败(源码通过编译,所有的单元测试和接受性测试都能在构筑后顺利运行)。在晚上,我们进行每日构筑,在第二工作日开始前,保证前一天的代码提交没有异常。这样第二天当大家进入公司后能够将所有的源码根据源码库进行刷新,开始新的开发和持续性集成。

进行每日构筑,持续性构筑的目的是什么?
程序开发的圣杯(The Holy Grail)是在最短的时间内把软件产品完成,推入市场换取利润。开发流程越容易适应内部外部出现的变化,开发周期就不会因为变化而过度延伸,造成产品无法按期进入市场。这个道理很容易理解,在开发过程中,很多小小的变化可以被开发团队在出现的时候所忽略。但是这些各种各样的变化最后聚集在一起,最后可能会变成一个可怕的错误,开发团队可能为了修改这个错误而花费两三个月来做。什么样的微小变化会造成这样的错误?比如,一个大三学生来到公司实习,他的任务是为现有软件增加几个功能,没有人给他任何指导,他也不知道自己不该复制代码,结果,他在多处复制代码。这也不是他的错,这些地方都需要比较相似但是都有一定不同点的代码,这个大学生没有经验,眼光比较窄,就觉得自己做的也没有错(资深设计者有时都会犯这样的错误)。后来这个学生走后,测试发现他的设计有点问题。接手的开发者找到错误,然后发现同样的错误在很多地方被复制,修改需要至少一周时间。再来一个例子是,一个功能在设计阶段没有考虑防护性问题,到了该出货的时候,测试发现黑客可以利用这个功能来攻击整个软件系统。修改这个功能的设计,需要两个月。所以任何微小的变化必须及早发现,改好,甚至有时要加强措施来避免同样的问题发生。这和每日构筑,持续性构筑有什么关系?想像一个团队有20个开发者,有些在开发独立的模块,有些合作进行集成。他们每天都在提交代码,但是整个团队不进行每日构筑,这些代码不停地变化,一个星期前的代码可能和今天的代码对比可能是完全不同,这就是我说的变化,这种变化几乎是随时随刻的,一星期积攒下的变化往往是一天积累下的变化好几倍,好几十倍,甚至上百倍。想像一天五个代码变提交甚至集成为产品一部分,在当天的构筑中发现两个变化的集成会造成不合理,这个问题最早当天就能解决,最迟第二天就能解决。要是等到一个星期后,才进行产品构筑,一个星期里可以有几百个变化,编译甚至测试可能会出现十几个问题,这样可能会耗费团队一天的时间来修补出现的错误。我们再看看团队等到一个月以后才进行构筑,这时可能会出现数十个,有时可能会出现近百个错误,这可能会花费团队两到三天时间进行修补,构筑越不频繁,变化的堆积也就越多,团队应对这些变化的时间花费也就越长。

每日构筑,持续性构筑的目的就是让开发者频繁地将自己的程序设计变化集成于产品,尽早让问题显示出来,让开发者在变化很小的状况下进行相应的调整。接受性测试,单元测试在构筑后运行能够自动化测测试其实就是进行接受性测试和逆行测试(Regression Testing),对整个产品的集成质量和单个部件的质量进行认证。每日构筑,持续性构筑的目的就是让整个开发团队能够以很小量的集成来评估自己的进度与质量。

进行每日构筑,持续性构筑是否造成团队气氛紧张?
这个问题提出的主要原因是当构筑失败时,团队成员之间会不会因此而相互攻击,甚至被管理层利用来克扣薪金,制造团队内部的不和谐。我对国内社会的现状不太了解,我只想从我自己现在的团队氛围来讲。INTUIT,我所在的组在去年7月份实现持续性构筑,在第一个月不少人在代码提交时造成构筑失败,当时我们组在圣地亚戈(San Diego),还有另外一个组在山景城(Mountian View)地区工作,也在我们的源码树(Source Tree)里提交代码,两个组其实是合作一起开发同一个部件。当我们开始进行持续性构筑开发时,他们还没有开始为我们贡献源码,于是在第一个月,我们出现了至少二十次的构筑失败,山景城小组的成员就开始在电子邮件里埋怨我们为什么老造成构筑失败,一个月后我们建立了伙伴构筑(Buddy Build),我们圣地亚戈小组造成的构筑失败开始减少,最后基本上成为两个月我们才会造成一次构筑失败。而我们完善了我们的提交代码机制后,山景城小组开始向我们的源码树里提交代码,他们开始每天都造成构筑失败。两个小组写作改善了伙伴构筑,但是问题一直都没有解决,这种每天都要出现构筑失败的现象到现在还一直持续。多数时间,山景城小组是造成构筑失败的元凶。虽然我们有源码提交的机制,我们有伙伴构筑,有些问题就是无法解决的。我们的构筑需要两小时,不少部件必须和Oracle,MySQL,甚至SQL Anywhere互动。任何必须依赖数据库的自动化测试,都是复杂,易出现问题的测试。我们圣地亚戈小组的开发都是简单的内存常驻部件的开发,相对简单,所有测试都以单元测试的框架进行。所以两个小组在代码提交,保证持续性构筑的成功的努力上是不一样的。简单地指责一个小组不断地造成构筑失败,甚至以此评价这个小组的敬业精神不够是不合理的。一开始,我经常心里暗暗感觉这个小组的初期开发构架很不合理导致问题不断出现,后来还觉得这个小组的敬业精神不够。后来,我开始接触了他们的测试框架,看到他们设计的测试用例,我还是觉得他们的构架很不好,但是我已经改变了对这个小组的敬业精神的怀疑。这就是“你根本无法了解这个人,除非你穿着他的鞋子走上一英里路。”

我对整个问题的理解是,构筑失败并不是世界末日。构筑失败从某些角度反应的是我们在做事上有些问题,这个问题的根源是什么?我们如何帮助这个组员,或是这个小组避免同样问题的出现?这些才是正确的问题思考,故障排除的思维方式。当然,一个组员,一个小组不断地重复同样的错误说明了这个成员,这个小组在敬业精神上有问题。让我在更进一步地说说这个问题,是什么鼓励着这个成员,这个小组在不停地重复同样的错误?我对这个新问题的回答是几个追加的问题,这个成员,这个小组有没有防范重复相同错误的机制,比如防止构筑失败可以用伙伴构筑来寻找可能发生的问题?这个成员,这个小组是否知道源码提交前必须对自己的代码进行检阅?如果这些问题的答案是没有,不知道。那么我敢肯定有人会像这个程序或小组提建议说,你/你们要建立相应机制来防止同样的事情发生,你/你们要经常检阅源码。总之一个同样的错误在一个公众监督的环境下,不可能持久地重复自己。要是这个组员/小组因为自己所开发的产品过于复杂,他们已经尽最大努力避免同样的问题发生,但是因为面对的问题过于复杂,重复发生的问题防不胜防,我觉得容忍也并非坏事,但是容忍是一把双刃剑,容忍导致纵容重复性行为的出现,容忍有时会造成一个成员/小组对同样行为的轻视,甚至导致更大隐患的潜伏,在今后的开发中造成重大损失。最后说说,要是一个组员/小组在机制限制下还能重复自己的错误行为,可能他们就是不在乎,可能他们在管理层有靠山,这样的成员小组只是在公众面前不停地出自己的丑而已。这样的“毛驴”(jackass)往往是公司里让人感到不耻的人。每日构筑,持续性构筑就好像群众监督,任何错误都暴露在公众的眼光之下,这样的公众注视会鼓励大家为自己的错误感到羞耻,督促大家迅速改正自己造成的错误。这就是流程改进。

对于他人出现的代码提交错误,什么样的反应才是正确的反应?我觉得指责是错误的反应,伸手帮忙才是正确的反应。帮助造成错误的人/小组排除故障,提供支持和鼓励,而不是责备其实比责备的影响更大,造成错误的人/小组在接受帮助的时候,感觉是愧疚,和感激。愧疚来自感受他人将自己的工作时间花费在排除自己的造成的错误上,感激也来自同样的东西。除非这个人/小组的心理不正常,对这些帮助无动于衷,不停地重复同样的错误,甚至经常地需要其他人/小组牺牲自己的时间来帮助解决问题,这个小组要么很快就被公司开除,要么以后出了问题没有人会来帮忙,间接地出局。

我觉得有每日构筑,持续性构筑的机制,会提高团队一日接一日的开发质量控制。它能提高大家对自己的代码的质量控制,做事会更小心,同时能够提高团队的合作精神。我不觉得每日构筑,持续性构筑的机制会造成团队的气氛紧张。但是不同的社会意识形态会影响人与人之间的协作态度,我说的好处在一个团队协作精神意识高的环境里是很容易见到的。但是在一个相互拆台,相互抵制,相互恶意竞争的环境里,我不知道每日构筑,持续性构筑会如何被恶意运用,造成团队内部不和谐。

如何正确地进行每日构筑,持续性构筑来建立良好的开发流程
首先说说构筑时间长度,象微软这样的公司,Windows Vista的源码构筑需要8到12小时的构筑时间,这样的产品不适合持续性构筑。这种构筑最多一天两次,有时,一天一次已经是很勉强的事。要做到持续构筑,象这样庞大的工程,只有把项目的部件分拆后才能缩短构筑时间达到一天数次的持续性构筑。我所见过的项目,构筑时间长度有10分钟,2小时和8到12小时。对于构筑需要10分钟的项目,持续性构筑基本上是每次的代码提交就能导致构筑开始,然后是测试。这样的项目敏捷性是很高的。这种项目在作坊型企业为客户进行外包小项目中经常出现。对于构筑需要2小时的项目,这样的项目可能有50000以上的单元测试和接受性测试,编译时间可能要20分钟,其他时间都是用于运行测试。这种项目还是可以进行持续性测试,但是它已经显示出整个项目要分拆成好几层,进行分层持续性构筑。对于构筑需要12小时的项目,这种工程只能进行一天一次的每日构筑,有时可能还要两天一次的构筑。这里最大的挑战就是,如何对现有构架的伸缩(Infrastructure Scalability)。

进行分层构筑的方法是这样,整个工程由许多小的项目构成(在Visual Studio里csproj,vbproj,和vcproj就是代表不同的项目;在Eclipse里,.project,.classpath和java源码测试都代表不同的项目;还有在maven里,project.xml和project.properties和java源码测试,甚至相关联的C#工程都包括成一个项目。最后这个例子里,maven项目会包括几个java和其他语言的项目)。项目之间会有一定的相互依赖性,这种依赖性不是相互循环的,总有一些项目是独立的,而一些项目是依赖独立的项目。,还有一些不仅依赖这些独立的项目,更依赖那些依赖独立性的项目。也就是说,各个项目间的依赖性更像一个巨大的树。树根是那些独立的项目,树干里是那些和独立的项目有简单依赖性的项目,我们就称这些项目为依赖性微复杂的项目,树叶部分则是依赖性复杂的项目。根据依赖性进行分类,我们可以把整个工程根据依赖性切成不同的层次,比较独立的项目就是最底层,这一层的项目构筑后都是重复使用的库,比较容易被更改然后进行构筑。上一层是专门依赖底层独立项目的,这些项目还是比较容易进行构筑,只要把底层的变化稀释到这一层,根据这些最新变化进行开法,这里开发者同时进行对底层复用库进行集成性测试。对于依赖性更复杂的项目,这些项目应该依靠定期的底层和中间层库的发布来进行。比如我的项目对很多底层库和中间层库的依赖,我发现它们的变化过于频繁,我的设计总得不到稳定化,我可以将我的项目依赖三天前成功构筑的底层库和中间层库。我的集成和这些底层库中间层库的集成有三天的差别。对于需要8到12小时构筑的项目,我从来没有得到过机会进行构筑改进,所以我无法给出什么很好的建议。 设计自己的构筑系统有好处也有坏处,我觉得最大的好处是自己的东西自己可以随意改动,自己设计如何构筑和自动化测试,生成构筑後的报告。特别是我加入Intuit以后这种感受特别深。我的老板经常跑到我这里问我的项目中,单元测试有多少,功能测试有多少,代码覆盖率是多少。我们的系统是用各种第三方软件集成的,有时各种各样的信息散布在各个网页和记录文件里,于是乎我要这里copy & paste,那里copy & paste,组成一个报告给他,我经常在这种费力没有成果的工作中消耗很多时间,这算不算的上是与敏捷背道而驰(Anti-agile)?是的,这是与敏捷背道而驰,只是他的上司需要这些数据,而这个上司的上司也需要这些数据,所以,这些事情不得不做。而且并不是每个人都能接受敏捷,XP等等。这些事情做多了,人也就烦了,所以我最希望自己设计这些系统,自动化处理这些必要的数据。现实中,这种机会基本没有,我所接触的都是公司使用现有的第三方技术来实现这些,于是数据永远都是散布在各个角落里。
【每日构筑,持续性构筑的重要性】其实以现在的科技,设计自己的构筑系统并非难事,使用任何脚本语言,比如我最喜欢用的Perl(或是大家各自的选择,wscript,python,ruby等等),再加上现有的构筑引擎,如Ant,Maven,微软的msbuild,甚至Visual Studio .Net本身,这些都能用于迅速地搭建一个可以适合小的团队的每日构筑系统。我个人喜欢用脚本语言来进行设计因为脚本语言的逻辑表达力很强,你可以通过阅读脚本语言的逻辑来迅速了解其构造。maven,Ant,和msbuild都倾向于使用XML来设计项目文件。这些构筑系统都很不错,只是不合我胃口,特别是XML格式的项目文件,还有自身的死板性,所以我个人喜欢自己按照自己的做事风格为自己定做构筑系统。其实这也没有什么不好的,微软自己的构筑系统就是用Perl,和DOS batch设计而成的,参见DDK里的构筑系统。这只是开发过程中很好玩的一件事,同时也是帮助自己提高自己的工作效率的一种流程改进。当一种工具无法满足你的需要,要么继续改进现有的工具,要么自己设计新的工具来适应形势的变化。
伙伴构筑,这是帮助整个团队获得构筑成功的关键,伙伴构筑在90%的情况下都能抓住构筑失败根本原因。伙伴构筑的原理其实很简单,就是在一个开发机器上同时建立两个源码空间,开发者在一个空间中进行项目设计,在提交前,先把所有最新更新搞到另一个源码空间里,然后把所有改过的代码拷贝到此源码空间里,最后构筑看看会不会出问题,这叫自我伙伴构筑,也就是自己和自己进行伙伴构筑。还有一种是请人帮忙,首先是叫伙伴把自己的源码库进行更新,然后把所有变化过的源码交给伙伴叫他构筑一次,看看会不会出问题。出了问题,好啦,你要把问题找出修好,没出问题,你可以进行提交,但是,你要在提交后等你的变化通过构筑后才能回家。这个规则可以保证你的提交不会出现问题。但是问题有时还是防不胜防,比如,你的设计是多线程有关,有时你的设计是错误的,因为你在自己的机器上设计,而且怎么测试,可能设计不会出问题,等到了同样的设计到了一台不同的机器上,问题开始出现了。所以我说,人无完人,在仔细的人也要犯错误。所以一年有几次因为自己不小心而造成构筑失败是正常的。只要不把构筑失败白饭吃就好了。
最后总结一下每日构筑,持续性构筑的规则:

  1. 每天都要更新的源码空间,保证其是和整个代码管理库中的内容是相差很小的。
  2. 必须建立正确的每日构筑,持续性构筑的规范,比如提交前要进行伙伴构筑,要经常地更新源码空间,尽力保证自己的代码不会造成问题。
  3. 提交代码前要进行伙伴构建,要么自己来,要么找个人帮助你;
  4. 如果是持续性构筑,提交代码后要等构筑成功後才能回家。如果你没有时间等,就等到第二天再提交。
  5. 如果是每日构筑,提交代码前要做好检查,提交后就回家等吧。
  6. 每天下午5点後就不要提交,否则没人能够帮你。你要是提交了,就等持续性构筑成功后再走。
我觉得尽力让自己不造成构筑失败最能培养耐性和细心,大家都用一个标准来对待自己的代码提交质量,才能保证每日构筑,持续性构筑能够工作。如果一个小组把这个问题提升到谁造成构筑失败谁要罚钱的地步,我觉得这已经是无法进行每日构筑,持续性构筑。保持每日构筑,持续性构筑的成功是每个人的义务,不需要什么硬性制度来强制实现。要是强制实现这种工作行为,那么这个开发团队面临的是更大的质量危机,每日构筑,持续性构筑根本无法解决这样的问题。每个人都会犯错误,所以大家只要能够尽力做到保证每日构筑,持续性构筑成功,不必自责太深。
Special Thanks to Stanly Xu (http://blog.csdn.net/stanley_xu)。

    推荐阅读