重温经典:持续集成(上篇)丨中国DevOps社区
Martin Fowler
摘要
引言
使用持续集成构建功能
持续集成的实践
维护单一的源代码存储库
构建自动化
如何构建自动化测试
每人每天都向主干提交代码
每次提交都应该在集成机上构建主线
立即修复失败的构建
保持快速构建
在生产环境的克隆中测试
任何人都能轻松获得最新的可执行文件
每个人都可以看到正在发生什么
自动化部署
持续集成的好处
引入持续集成
最后的思考
延伸阅读
持续集成是一种软件开发实践,团队成员频繁地将他们的工作成果集成在一起,通常每人每天至少提交一次,这样每天就会有多次集成。每次集成都通过自动构建(包括测试)进行验证,以便尽可能快地检测集成错误。许多团队发现这种方法可以显著减少集成问题,并允许团队更快地开发内聚软件。本文简要介绍了持续集成技术及其应用现状。
我清楚地记得我第一次看到一个大型软件项目。那时我在英国一家大型电子公司做暑期实习。我的经理,QA小组的一员,带我参观了一个地方,我们进入了一个令人沮丧的大仓库,里面堆满了立方体。我被告知这个项目已经开发了几年,目前正在集成,并且已经集成了几个月。他告诉我,没有人真正知道完成集成需要多长时间。从中我学到了软件项目的一个共同的故事:集成是一个漫长而不可预测的过程。
其实不需要这样。我在ThoughtWorks的同事和世界上许多其他人所做的大多数项目都不把集成当成一个很严重的事儿。任何单个开发人员的工作都离共享的项目状态只有几个小时,并且可以在几分钟内集成回去。任何集成错误都能被快速发现并得到迅速修正。
这种鲜明的对比并不是昂贵复杂工具的结果。它的本质在于一个简单的实践,也就是团队里的每个人都在频繁的集成,通常是每天对于一个受控的源代码存储库进行集成。
当我向人们描述这一做法时,我通常会发现两种反应:“这里不行”和“这样做不会有多大区别”。人们在尝试的过程中发现,这比听起来容易得多,而且对开发有着巨大的影响。因此,第三种常见的反应是“是的,我们这样做了——没有它你怎么活?”
术语“持续集成”起源于Kent Beck的极限编程开发过程,是最初的12个实践之一。当我开始在ThoughtWorks工作时,作为一名顾问,我鼓励在我工作的项目中使用这种技术。Matthew Foemmel把我模糊的建议变成了实际行动,我们看到了这个项目从罕见而复杂的集成,变为我描述的不是那么严重的事情。Matthew和我在这篇论文的原始版本中写下了我们的经验,这篇论文一直是我网站上最受欢迎的论文之一。
尽管持续集成是一种不需要特殊工具来部署的实践,但我们发现使用持续集成服务器是很有用的。最著名的此类服务器是CruiseControl,这是一个开源工具,最初由ThoughtWorks的几个人构建,现在由一个很大的社区维护。从那时起,出现了其他一些CI服务器,有开源的,也有商用的——包括ThoughtWorks工作室的Cruise。
对于我来说,解释什么是CI以及它是如何工作的最简单的方法是展示一个快速的例子,说明它如何与一个小特性的开发一起工作。假设我必须对一个软件添加一点功能,任务是什么并不重要,因为现在我假设它很小,可以在几个小时内完成。(稍后我们将探讨更长的任务和其他问题。)
首先,我将当前集成源代码的副本复制到本地开发机器上。我通过使用源代码管理系统,从主干签出一个工作副本来实现这一点。
上面那段话对于使用源代码控制系统的人来说是有意义的,但是对于不使用源代码控制系统的人来说是胡言乱语。所以让我快速地为后者解释一下。源代码控制系统将项目的所有源代码保存在存储库中。系统的当前状态通常称为“主干”。开发人员可以随时在自己的机器上生成主干的受控副本,这称为“签出”。开发人员机器上的副本称为“工作副本”。(大多数情况下,你实际上是把你的工作副本更新到主干上——实际上和签出也是一样的。)
现在我拿着我的工作副本,做任何我需要做的事情来完成我的任务。这将包括修改产品代码,以及添加或更改自动化测试。持续集成假定软件中有高度自动化的测试:我称之为自测试代码的工具。它们通常使用流行的XUnit测试框架的一个版本。
当我完成之后(通常在我工作的不同阶段),我就在我的开发机器上执行一个自动化的构建。这将获取工作副本中的源代码,将其编译并链接到可执行文件中,然后运行自动测试。只有在所有的构建和测试都没有错误的情况下,整个构建才被认为是正确的。
有了正确的构建,我就可以考虑将更改提交到存储库中。当然,问题是,在我有机会提交我的更改之前,其他人可能,而且通常已经对主干进行了更改。因此,首先我用他们的更改来更新我的工作副本,并重新构建。如果他们的更改与我的更改冲突,在编译或测试中将显示为失败。在这种情况下,我的责任是修复这个问题,并重复构建,直到我可以建立一个与主干正确同步的工作副本。
一旦我自己构建了一个正确同步的工作副本,最终我就可以将我的更改提交到主干中,之后会更新存储库。
然而,我的提交并没有完成我的工作。此时,我们再次构建,但这次是在基于主干代码的集成服务器上。只有当这个构建成功时,我们才能说我的更改已经完成。因为总有万一,我可能会遗漏了我的机器上的东西,存储库没有得到适当的更新。只有当我提交的更改在集成服务器上成功构建时,我的工作才能完成。这个集成构建可以由我手动执行,也可以由Cruise自动完成。
如果两个开发人员之间发生冲突,通常会在第二个提交的开发人员构建其更新的工作副本时捕获冲突。否则,集成构建将失败。无论哪种方式,错误都会被快速检测到。此时,最重要的任务是修复它,并使构建重新正常工作。在持续集成环境中,不应该让失败的集成构建保持在失败状态太久。一个好的团队一天应该有很多正确的构建。不好的构建时有发生,但应该迅速被修复。
这样做的结果是,有一个稳定的软件,工作正常,包含很少的错误。每个人都是从这个共享的稳定的基础上开发的,从来没有离开这个基础太远,以至于需要很长时间才能集成回来。寻找错误会花更少的时间,因为错误很快就会显现出来。
上面的故事是关于CI的概述,以及它在日常生活中是如何工作的。显然,让所有这些工作顺利进行并不仅仅是这些。我现在将重点介绍构成有效CI的关键实践。
软件项目涉及许多需要组合在一起才能构建产品的文件。跟踪所有这些文件,是一项重要的工作,特别是当有多人参与时。因此,我们毫不意外的看到,多年来,软件开发团队已经构建了管理所有这些文件的工具。这些工具称为源代码管理工具、配置管理、版本控制系统、存储库或各种其他名称,是大多数开发项目不可分割的一部分。令人悲哀和惊讶的是,它们并不是所有项目的一部分。尽管很少见,但我确实遇到不使用这样的系统的项目,项目使用一些混乱的本地和共享存储器的组合。
因此,作为一个简单的基础,确保你要有一个像样的源代码管理系统。成本不是问题,因为有高质量的开源工具。当前选择的开源存储库是Subversion。(较老的开源工具CVS仍然被广泛使用,虽然比什么都没有要好得多,但是Subversion是更时髦的选择。)有趣的是,当我与开发人员交谈时,我了解到大多数商业源代码管理工具比Subversion更受欢迎。我一直听到人们说唯一值得花钱的工具就是Perforce。(译者注:本文写于2006年,时至今日,Git更为流行)
一旦你得到一个源代码管理系统,确保它位于众所周知的地方,每个人都去获取源代码。没有人会问“foo-whiffle文件在哪里?”所有的东西都应该在存储库里。
尽管许多团队都会使用存储库,但我发现一个常见的错误是,他们没有将所有内容都放在存储库中。如果人们使用它,他们会把代码放在那里,但你的构建需要做的一切都应该在那里,包括:测试脚本,属性文件,数据库架构,安装脚本和第三方库。我知道一些项目,将编译器检入到存储库(对于早期的大量的C++编译器很重要)。基本的经验法则是,你应该能够用一台空白的机器开始项目,做一个签出,并且能够完整的构建系统。只有少量的东西应该放在空白的机器上,通常是大的、安装复杂的和稳定的东西。操作系统、Java开发环境或基础数据库系统是典型的例子。
你必须将构建所需的所有内容都放在源代码管理系统中,但是你也可以将人们通常使用的其他内容放在其中。IDE配置很适合放在那里,因为这样人们就可以很容易地共享相同的IDE设置。
版本控制系统的一个特点是,它们允许你能创建多个分支,以处理不同的开发流。这是一个有用的,但不必要的功能,但它经常被过度使用,并使人们陷入麻烦。尽量少用分支。特别是在有一条主干的情况:目前正在开发的项目的唯一分支。几乎每个人大部分时间都应该在这条主干上工作。(合理的分支是修复先前生产版本的错误和临时的实验。)
一般来说,你应该在源代码管理中存储构建所需的所有内容,但不存储实际构建出的内容。有些人确实将构建的产品放在源代码管理中,但我认为这是一种坏味道——这意味着更深层次的问题,通常是无法可靠地重新创建构建。
将源代码转换为可以运行的系统,通常是一个复杂的过程,它包括编译、移动文件、把数据库模式加载到数据库等等。然而,与软件开发中的大多数任务一样,它是可以被自动化的。它也应该是自动化的。让人们输入奇怪的命令或点击对话框是浪费时间,也最容易产生错误。
构建自动化环境是系统中一个共同的特性。Unix世界使用make作为工具已经几十年了,Java社区发展了ANT(译者注:后来JAVA的构建工具发展为Maven和Gradle),并且. net社区已经有了Nant,现在又有了MSBuild。确保你可以使用单个命令使用这些脚本构建和运行启统。
一个常见的错误是没有在自动化构建中包含所有内容。构建应该包括从存储库中获取数据库模式,并在执行环境中启动它。我将详细阐述我先前的经验法则:任何人都应该能够引入一台空白机器,签出存储库中的源代码,发出一个命令,之后在自己的机器上就拥有了一个正在运行的系统。
构建脚本通常有不同的风格,通常是特定于平台或社区的,但他们大可不必如此。尽管我们的大多数Java项目都使用Ant,有一些在使用Ruby(Ruby Rake是一个非常好用的构建脚本的工具)。我们通过使用Ant自动化在早期的微软 COM项目中获得了某些价值。
一个大的构建通常需要花费很多精力,如果你仅仅做了一个小小的更改,那么你不会想要执行所有的步骤。所以,一个好的构建工具会分析在流程中需要更改的内容。通常的做法是检查源文件和目标文件的日期,只有在源文件的日期较晚时才进行编译。依赖关系会变得更加棘手:如果一个对象文件改变了那些依赖于它的对象文件,那么这些对象文件可能也需要重新构建。编译器能够处理这类事情,也可能不处理。
根据你的需要,你可以构建不同类型的东西。你可以通过是否使用测试代码或者使用不同的测试集来构建系统。有些组件可以独立构建。构建脚本应该允许你为不同的情况构建可选目标。
我们普遍使用IDE,而且在使用IDE时,大多数的公司内部都有一些构建管理的过程。然而,这些文件总是IDE专有的,而且它们非常脆弱。不过,他们这些公司需要通过IDE进行工作。用户通过IDE设置自己的项目文件并将其用于单独的开发是完全没有问题的。然而,有一个在服务器上可用并且可以从其他脚本运行的主干是非常重要的。所以在Java项目中,我们可以让研发人员在IDE中构建,但是主干需要使用Ant来保证它可以在开发服务器上运行。
从传统意义上来讲,构建意味着编译,链接以及执行程序所需的所有其他过程。一个项目可能会运行,但是,这并不意味着它在做正确的事情。现代静态类型语言可以发现许多bug,但是更多的bug会成为“漏网之鱼”。
在构建过程中包含自动化测试是更快、更有效地发现bug的比较好的方法。当然,测试并不是完美的,但它能够发现很多Bug,这就足够有用了。特别是极限编程(XP)和测试驱动开发(TDD)的兴起为自动化测试的普及做了大量工作,因此许多人已经看到了这种技术的价值。
经常阅读我作品的读者会发现我是XP和TDD的忠实粉丝。然而,我想要强调的是,这两种方式都不是构建自动化测试的最佳途径。这两种方式都强调在使测试通过之前你要先编写测试——在这种模式下,测试不仅能够用于发现错误,而且还涉及探索系统的设计。这是一件好事情,但是,对持续集成而言,这并不是必需的。因为我们通常对自动化测试代码的需求比较少。(尽管TDD是我进行自动化测试的首选)
对于自测试代码而言,你需要一套自动化测试体系它可以检查大部分代码库中的Bug。测试可以从一个简单的指令中启动并进行自动检测。运行测试套件的结果应该可以指出是否有任何测试失败。对于具备自测试的构建,测试的失败应该会导致构建失败。
在过去的几年时间里,TDD的兴起普及了XUnit开源工具家族,这些工具对于这类测试非常理想。Xunit 工具对我们 ThoughtWorks 来说非常有价值,我也总是建议人们使用这些工具。这些由Kent Beck首创的工具,能够帮你非常容易的构建一个自动化测试环境。
XUnit工具当然是让代码进行自动化测试的起点。你也应该寻找其他专注于更多端到端测试的工具。目前有很多这样的工具,包括FIT、Selenium、Sahi、Watir、FITnesse,还有很多其他工具,我在这里不打算都列出来。
当然,你不能指望测试能发现一切错误。正如人们常说的那样:测试并不能证明没有缺陷。虽然你从自动化测试的构建中得到的反馈并不一定是完美的,经常运行的不完美的测试也比根本不写的完美测试要好得多。
集成主要是关于沟通的。集成允许开发人员将他们所做的更改告知其他开发人员。频繁的交流能让人们在变化发生时迅速了解情况。
开发人员遵守主干的一个先决条件是,他们可以正确地构建自己的代码。当然,这包括通过构建测试。与任何提交周期一样,开发人员首先更新其工作分支以匹配主干,解决与主干的任何冲突,然后在其本地上构建。如果构建通过,那么他们可以自由地提交到主干上。
通过经常这样做,开发人员可以快速发现两个开发人员之间是否存在冲突。快速修复问题的关键是快速找到它们。由于开发人员每隔几小时就提交一次冲突,所以在冲突生后的几小时内就可以检测到,此刻没有发生太多代码修改,所以容易解决。持续数周不被发现的冲突,可能很难解决。
在更新工作分支时进行构建,这一事实意味着可以同时检测到编译冲突和文本冲突。由于构建是自测试的,所以你还可以检测代码运行中的冲突,如果后一种Bug在代码中存在了很长时间而没有被发现,那么它们是特别难以发现的错误。由于两次提交之间只有几个小时的更改,所以问题隐藏的地方也就只有那么多了。此外,由于没有太大的变化,你可以使用差异调试来帮助你找到错误。
我的一般经验法则是,每个开发人员每天都应该提交到代码库。在实践中,如果开发人员更频繁地提交,通常是有用的。提交的频率越高,寻找冲突错误的地方就越少,解决冲突的速度也就越快。
频繁的提交会鼓励开发人员将他们的工作分解成几个小时的小块。这有助于跟踪进度,并提供一种进度感。通常人们一开始觉得他们不能在几个小时内做一些有意义的事情,但是我们发现指导和练习可以帮助他们学习。
未完待续……
- End -
往期文章
想成为译者?
社区译者团
活动预告
中国DevOps社区
广州第九届Meetup(线下):7月5日
敬请期待