持续集成 Continuous Integration

发布于 2022-04-26 13:08:58 字数 11106 浏览 1038 评论 0

我还依稀记得我做的第一个大型项目。那是一个英国的大型电汽公司,我暑期在那里实习。当时我有个经理,他是 QA 组的。他给了a tour of a site and we entered a huge depressing warehouse stacked full with cubes. 他告诉我,这个项目已经开发了好几年,现在正在集成阶段。已经集成好几个月了。当时 my guide 告诉我,大家都不知道还要集成多久才能成功。那个时候起,我就知道了这个道理:集成是个很长且未知性很强的过程。

但其实集成并不一定得搞成这样。我在 ThoughtWorks 的同事,以及世界各地的开发者,他们做过的项目里,集成是个 non-event。任何项目成员的工作,都会在几个小时里 to a shared project state,并且集成到那个状态里(需要润色)只需要几分钟。集成发生了错误,马上就会被发现,并且他们能够快速地修复。

这种区别,并不是借由什么昂贵复杂的工具才能实现的。它的精华就藏在一些非常简单的实践里:团队中的每个人都要频繁地 - 通常是每天都要 - 把代码集成到一个中央的源代码仓库中去。

当我和人们分享这些实践的时候,通常我会听到两种回答:「(至少在我们这)没用的」,以及「做了也不会有任何改进的」。而当他们真正去做的时候会发现,其实并没有听上去那么难,但它对开发流程改进效果明显。因此,人们的第三种反应是:「我们当然在做——这些都没做你们是怎么开发的?」

「持续集成」这个词最初起源于极限编程,它是极限编程最初十二个开发流程的其中一个。我最初以咨询师的身份加入 ThoughtWorks 时,我鼓励当时所在的团队采用这个实践。是 Matthew Foemmel 把我模糊的提议细化成了可执行的 action。然后我们发现,项目从原来的没有集成、极端复杂的集成,渐渐变成我所说的那种 non-event 的状态(需要润色)。这篇文章是 Matthew 和我总结了该项目的经验以后写下来的,事实上它也成了我博客上访问量最大的一篇文章。

尽管持续集成这项实践并不需要特别的工具参与也可实施,但我们发现,有一台「持续集成服务器」的话会更有帮助。最有名的持续集成服务器是 CruiseControl,它最早是 ThoughtWorks 几位同事写的一款开源工具,后来交给专门的社区来维护。后面又有一些其他的 CI 服务器出现,都是开源的商业软件,比如 ThoughtWorks Studio 开发的 Cruise 等。

building a feature with continuous integration

要解释 CI 是什么,它怎么工作,我能想到最简单的方法莫过于以一个小型特性的开发为例子,快速为诸君展示下持续集成的工作流是什么样子。假设我们现在要对一个已有的软件做些更改,更改本身是什么并不重要,因此我们就先假设这是个小型的更改,几小时内即可完成。(后面我们会深入更大型的例子和其他相关问题)

我会这样开始我的工作:首先,先把当前已集成的最新代码先签出到我的本地。这可以通过一个源代码管理工具来完成,你从主干代码上签出一份完全一样的代码到你的本地机器上。

这对用过源代码管理系统的人来说可能很熟悉,但对于其他人来说可能很不明所以。如果你是后者,那我还是简单地介绍一下。源代码管理系统能替你将所有的源代码保存在一个仓库中。系统的当前状态通常被称为「主干」(mainline)。在任何时间点,开发者都可以将主干代码完整地拷贝到他自己的开发机,这个过程称为「签出」(checking out)。拷贝到开发机上的这份拷贝通常称为「working copy」(一般来说你的改动都在你从主干签出来的 working copy 上 - 实践中它们通常是一回事)。

然后,我可以在我自己的开发机上任意修改。我可能会改动产品代码,也可以会新增或修改原来的自动化测试。「持续集成」假定,a high degree of tests which are automated into the software:我称这些设施为 自测试的代码。这些代码一般都使用了流行的 XUnit 测试框架。

特性开发完成后(或在开发过程中的任意时间点),我会在我的部署机器上去触发一个自动化构建。它会拿到我当前的源代码去执行编译、链接,把它变成一个可执行文件,通常它还会执行自动化测试。只有当构建和测试都没有错误抛出,我们才认为整个构建成功了。

构建完成后,我才会考虑将代码提交到源代码仓库中。我更改的文件,当然也可能已经同时被其他人所修改,并且提交到了主干上。因此我必须先把他们的最新代码签出来,与我的本地代码合并,并重新执行一次构建。如果签出的代码发生了冲突,它肯定会把编译或者测试给挂掉。这时我则有责任修复构建错误,并重复签出-执行构建这个操作,直到我的本地代码可以和主干代码同时存在为止。

把我的 working copy 与主干代码同步完成并成功通过构建后,我终于可以将我的代码提交到主干上。这会更新源代码仓库。

不过,提交完成并不是就完事了。此时,在持续集成服务器上,它会拉下新的主干代码,重新执行一次构建。只有当这次构建成功了,才能说我的更改完成了。我完全有可能忘记提交某些文件,导致构建只有在我自己的机器上才能成功。因此,只有集成服务器上的构建成功了,才能认为我的这次更改完成了。这次集成构建可以由我手动触发,也可以由 Cruise 自动触发。

如果两个开发者提交的 代码会有冲突,通常是在第二位开发者签出代码以合并 working copy 的时候被发现。否则的话,在他签出代码的时候集成构建肯定就已经挂了。不过无论如何,错误都会很快被揪出来。这个时候,最重要的任务就是修复失败的构建,让构建重新变绿。在一个持续集成的环境中,你永远不能让失败的构建状态持续太久。好的团队一天会有多次成功的构建。失败的构建可以出现,但应该被尽快修复。

这样做的结果就是,你有了一个能稳定工作的软件,并且它只有很少的 bug。每个人都从这个稳定的代码库上签出代码,开始工作,并且永远不会离这种稳定状态太远,他们随时都可以把代码集成回去。花在找 bug 上的时间将会大大减少,因为它们会被很快发现。

持续集成实践

上面的故事就是 CI 的一个概览,也是我们日常工作的缩影。让所有这些能流畅运作起来,显然不仅只有上面提到的这些。下面我会讲讲 CI 的一些关键实践,它们让 CI 高效运作。

维护单一的源代码仓库

软件项目的构建需要数以千计的文件。维护这些文件需要巨大的开销,特别是有多人在同一份代码上工作时。因此,这些年来业界有人开发出了一些工具来管理这些代码和文件。这些工具有很多叫法,比如源代码管理工具、配置管理工具、版本控制系统、仓库工具等。它们已成为项目开发中不可或缺的一部分。令人失望和惊讶的是,它们还不是所有项目的标配。有些项目就还在用本地的或共享硬盘来管理代码。这很少见,但我确实见过这样的项目。

因此,确保你的项目至少有一个源代码管理系统。这是必须的配置。不用担心价格问题,因为有很多开源工具是免费的。当下,Subversion 就是一个不错的选择,它是开源的。(CVS 也有很多用户,虽远胜于无,但现在一般都会选择 Subversion)。有趣的是,据我了解,大部分商业性的源代码管理工具并不如 Subversion 受到青睐。我一直听人说,唯一值得付钱的工具只有 Perforce

有了源代码管理系统之后,确保大家都知道都通过它来获取源代码。不应再听见有人问「那个 foo-whiffle 文件在哪儿?」仓库里应该都找到一切。

另外,尽管很多团队都在用代码仓库,但我还是注意到有个问题:他们没把所有必要的东西放到仓库里。他们会把代码提交到仓库里,但其实所有构建所必须的东西也应该提交到仓库中,包括测试脚本、配置文件、数据库 schema、安装脚本、三方库等。就我所知还有团队将编译器也提交到了仓库中(早期 C++ 编译器版本兼容问题多时显得尤为重要)。一般的准则是,你应该能够在一台干净的机器上,签出代码后就可以完整地构建出整个系统。构建机上只需要安装最少量需要的软件 - 通常是那些很大、安装复杂又稳定的文件,比如操作系统、Java 开发环境、或者数据库系统等。

构建所需要的所有东西都必须提交到源代码管理系统中。此外,日常工作所需的其他东西也可以提交进去。比如 IDE 配置就很值得提交进源代码管理系统,因为这样团队可以使用一份同样的 IDE 配置。

版本管理系统的另一特性是,它允许你创建多分支,以应对不同特性的开发。这个特性很有用,但也经常被人们过度使用,进而带来了不少麻烦。建议只保持最小量的分支开发。更具体地讲,有一条单一的开发主干就够了,它包含了项目当前的所有代码。大多数情况下,团队成员都应该在这条主干上工作(允许使用分支的情形包括:修生产环境下发现的 bug、短期技术探索)。

通常,任何类型的构建所需要的所有东西,你都应该提交到源代码管理中。同时,所有的构建物都不应该提交。确实有人会把构建物提交到源代码管理系统中,但我觉得这是个坏味道——它通常暗示着其他深层问题的存在,通常是因为,团队不具备重复执行构建的能力。

将构建自动化

将源代码变成一个可运行的系统,通常涉及一系列复杂的流程,如编译、文件移动、将 schema 加载到数据库中等。不过,就如其他大部分的任务一样,这个过程也是可以被自动化的——既是可以,也就应该被自动化。依靠人工来键入奇怪的命令、在对话框中点击按钮等,纯属浪费时间,也很容易发生错误。

automated environments for builds are a common feature of systems. Unix 世界有 make 已经几十年了,Java 社区开发了 Ant,.NET 社区有 Nant,后来又出现了 MSBuild。你需要利用这些工具,确保只需要一行命令,就可以构建起你的整个系统并运行之。

这里有一个常见的误区,即没有自动化所有的构建步骤。构建过程应该从仓库中拿到数据库的 schema 文件,并把它在执行环境中真正启动起来。我之前讲过的准则是:任何人都能够仅用一台纯净的机器、从仓库中签出源文件、运行一行命令,然后在他们的机器上获得一套可用的系统。

构建脚本可能有不同的写法,并且通常与平台或社区相关,但这不是必需的。尽管大部分的 Java 项目使用 Ant 来进行构建,但也有人选择了 Ruby 来充当构建工具(Ruby 的 Rake 系统是个很好的构建工具)。我们把早期的一个使用 Microsoft COM 的项目改用 Ant 来进行构建,效果良好。

一次庞大的构建通常非常耗时。如果你仅做了一些细小的修改,通常不想每次都跑所有的构建步骤。因此,一个好的构建工具还要能分析变更的部分,这必须是构建过程的一部分。较常用的方法是检查源文件、obj 文件的修改日期,仅编译那些改动过的文件。但这样的话,依赖关系也没那么容易处理:一个文件改动了,所有依赖它的文件也必须被重新构建一次。编译器可能会处理这种情形,但也可能不回。

depending on what you need, 你可能需要构建一些不同类型的东西。你可以选择是否把测试代码包含进系统构建中,或可以选择部分的测试集来进行构建。(You can build a system with or without test code, or with different sets of tests) 一些组件可能可被单独构建。构建脚本应能支持不同情形的构建需求。

现在大家都用 IDE 了,并且现代的 IDE 都不同程度地支持一些构建过程。不过,构建出来的文件通常与 IDE 强相关,不稳定程度较高。并且它们需要放到 IDE 里才能工作。作为 IDE 用户,配置自己的项目文件并用于日常开发是没问题的。但是,拥有一个可以既在服务器上运行、 and runnable from other scripts 的构建也是很重要的。因此,假设我们有个 Java 项目,开发者们能在自己的 IDE 上构建是没问题,但主构建必须使用 Ant 这样的构建脚本,以确保在开发服务器上,构建也能被执行和触发。

让你的构建能够自测试

传统意义上讲,一个构建包含了编译、链接,以及其他程序执行所必须的所有过程。程序可以运行不代表其运行结果是正确的。现代的静态强类型语言可以捕捉很多类型上的错误,但还有更多的 bug 从这张法网中溜走了。

另一个发现 bug 更快更有效的方式,是在构建过程包含自动化的测试。测试不是完美的,这我不否认,但测试能帮助发现许多 bug——足够多了。随着极限编程(XP)和测试驱动开发(TDD)的兴起,极大地推广了自测试代码,让越来越多的人看到了这些实践的价值。

熟悉我的读者都知道,我是 TDD 和 XP 的忠实粉丝,但我依然得补充一下,对于自测试的代码来说这两项实践都不是必须的。这两个实践强调的是测试先行,在写实际的产品代码前先写一个会挂掉的测试——在这种工作方式下,测试除了帮忙发现 bug,更多的是一种对当前的系统设计的探索。这当然是好的,但对于持续集成来说,它并非一个必要条件,因在持续集成中,我们对自测试代码的要求没有那么严格(虽然我个人还是更喜欢通过 TDD 的方式来产出自测试代码)。

要拥有自测试的代码,你需要一系列自动化测试套件,用它们来大范围地覆盖你的代码库,检查 bug 的存在。测试需要能仅通过一条命令来触发,并且能自动化完成其测试。测试套件的运行结果需要显示出是否有任何挂掉的测试。对于一个能自测试的构建来说,任何失败的测试都应该导致构建失败。

在过去的几年里,TDD 的兴起使得 [XUnit 家族] 的开源测试工具广为人知,它们非常适合用来进行这种类型的测试。XUnit 工具已经在 ThoughtWorks 证明了其价值,我总会推荐周围的人们去使用它。是 Kent Beck 开创的这些工具,它让你能非常容易地建立起一个完全可以自测试的环境。

使用 XUnit 工具绝对是你迈向自测试代码的第一步。除此之外,你的眼光也要多放在一些端到端测试的工具上。现在市面上有不少做端到端测试的工具,比如 FITSeleniumSahiWatirFITnesse等,以及许多我不能详列于此的工具。

当然,你不可能指望测试能帮你找出所有的 bug。正如我们经常听到的:测试通过不能证明就完全没有 bug。但是,一个自测试的构建,并不是因为绝对的完美才有价值。不完美但频繁运行的测试,远比完美但永远写不出来的测试好得多。

每个人每天都要向主干提交代码

集成问题主要是沟通问题。开发人员通过一次集成,让他人了解自己所做的更改。频繁的沟通让人们能够尽早地知道代码库发生的变化。

开发者能向主干提交代码的一个前置条件是,他们能够正确地构建这次提交的代码。这当然也包含了让测试通过的过程。正常的提交流程是这样的:开发者会拉取主干最新的代码,与自己的 working copy 合并,如果有冲突则修复之;然后在自己的机器上执行构建。如果构建成功了,则可以随时将代码提交到主干上。

频繁提交的结果就是,开发者可以迅速地发现自己的代码是否与其他人的冲突了。快速修复问题的关键,就是先能快速发现问题。如果开发者能够几小时内就频繁进行提交,可能发生的冲突就可以在几个小时内被发现,这个时候发生的改动还没有那么多,解决起来也很容易。但如果是积累了几周的冲突,发现时就已经很难修复了。

更新了 working copy 即构建,意味着同时也是在发现编译冲突和文本冲突等。因为构建是自测试的,在执行构建时也会检测合并是否有冲突。合并错误通常是非常隐秘的 bug, if they sit for a long time undetected in the code. 因为发生冲突的提交只包含了最近几小时内的更改,可能出现问题的地方屈指可数。并且因为此时改动的地方还不是很多,你可以利用 二分法进行调试,找出 bug。

我的经验是,每个开发人员每天都应该提交代码仓库。在实践中,如果能做到更频繁的提交会更好。你提交得越频繁,发现冲突时要检查的地方就更少,冲突修复起来也越快。

频繁提交会鼓励开发人员将手头的工作分解成更小的 chunks,每个 chunk 大概是几个小时的工作时间。这对于进度跟踪也是有帮助的,能给你带来对进度的感知。人们经常会觉得,几个小时没法作出什么有意义的事,但我们觉得,mentoring 和练习会让自己快速成长。

每次提交都应在集成服务器上触发一次主干代码的构建

有了每日提交后,团队就可以频繁地进行自测试的构建。这应该能说明,主干代码维持在一个健康状态了。但在实践中,还是会有出错的可能。一个可能的原因是,纪律性。比如有人没有在提交前同步主干代码,或者提交前没有执行一次构建等。另一个可能的原因是,开发者之间的机器上存在环境差异。

因此,你必须确保在一台集成服务器上,能够规律地执行日常的构建。并且仅有当这次集成构建成功时,才认为这次提交完成了。因为这次提交的开发者需要对此次构建负责,他就需要时时关注该次主干代码的构建,并在构建失败时进行修复。这样,任何人若在一天行将结束之时提交了代码,都必须确保主干代码的构建通过以后才能回家。

要做到以上所说,我见过两种实施方式:手工触发构建,或使用一台持续集成服务器。

手工触发构建的方式最容易描述。它与触发一个本地构建基本类似,就像每位开发者在提交代码进仓库之前都会做的一样。开发者登进集成服务器,签出最新的主干代码(就是他自己最新的一次提交),然后触发一次新的构建。他需要关注构建的状态,构建通过后,他的这次提交即告成功。(Jim Shore 也描述过这个过程

持续集成服务器则扮演着一个仓库监控者的角色。只要代码仓库一接收到新的提交,持续集成服务器会自动把新提交的代码签出到本地,并触发一次构建。构建完成后,服务器会将构建结果以通知提交者。提交者只有在收到通知 —— 通常是一封邮件 ——后,才算此次提交完成。

在 ThoughtWorks,我们更倾向于使用持续集成服务器来触发自动化的构建。我们主导了广为使用的开源 CI 服务器 CruiseControlCruiseControl.NET 最早的开发。之后我们还开发了商业版本的 Cruise CI 服务器。几乎每个项目上我们都会使用 CI 服务器,而且迄今为止效果都很令人满意。

并非所有人都倾向于使用 CI 服务器。Jim Shore 发表了一些值得注意的论据,强调为什么他更倾向于手工触发构建。他提到,CI 并不只是安装一些软件这样简单,这点我同意。为了做出高质量的持续集成,这里提到的实践都是必须的。当然,那些持续集成做得好的团队,多也觉得 CI 服务器是很有用的工具。

许多组织会安排一个时间表,规律性地执行构建,比如每天晚上触发构建。这与我们所说的持续构建不是一回事,对于持续集成来说也还不够好。持续集成的灵魂就在于尽快地发现问题。每日构建意味着,bug 会在代码库中潜藏一整天,没人能在夜晚来临之前发现它们。如果 bug 在系统中潜藏如此之久,定位、修复起来将耗时甚多。

立即修复挂掉的构建

持续构建一个重要的部分,即是当主干代码的构建挂掉时,必须马上将它修复。The whole point of working with CI ,是保证你永远都是在一个稳定的代码库上工作。主干构建失败了并不是大问题。当然,如果构建经常失败,则可能说明团队在提交前没有认真同步代码或执行构建。当主干代码确实构建失败时,要让它马上就回复正常。

我记得 Kent Beck 的说法是,「修复失败的构建是团队优先级最高的事」。这倒也不是说,构建一旦失败,团队所有人就都必须停下来修复这次构建。通常,让一两个人来修就足够了。不过重点是,必须把「修复构建」这项任务排到紧急、高优先级的象限中去。

通常而言,修复失败构建的最快方式就是回滚上一次主干代码的提交,这样可以把系统恢复到上一次已知的稳定状态中去。团队不应该在调试代码上花费太多精力,除非导致构建失败的原因显而易见。否则,推荐的做法是回滚上次主干代码的提交,再下来在个人的开发环境上调试问题。

为了从根本上避免挂掉主干代码的构建,你可以考虑使用 pending head 技术。

当一个团队在尝试引入持续集成时,通常这是最难完成的一项时间。【这段后面再翻】

keep the builds fast

持续集成的全部意义在于提供快速的反馈。因此,没有什么比耗时的构建更伤害 CI 本身的了。Here I must admit a certain crotchety old guy amusement at what's considered to be a long build. 我大部分同事都觉得,如果构建超过一个小时,那是完全不能接受的。我知道很多团队都梦想能够有极速的构建,但无法避免地,我们还是会遇到一些情况,我们没法把构建速度提升到那么快。

不过,对于大多数项目来说,极限编程所建议的十分钟通常都是合理的。现代的项目一般都能达到这个目标。投入全力来达到这个目标是有价值的,构建所省出来的每一分钟,都让所有开发者提交时能少等待的一分钟。

如果你现在的项目构建时间就需要一小时,着手于缩短构建时间前总难免有所畏惧。甚至即使是全新的项目,思考如何让构建保持快速也是令人却步的。但至少对于企业级应用来说,我们发现一般的瓶颈是在测试这块——特别是那些与外部服务(如数据库)交互的测试。

也许最重要的一步,是开始配置另一条用于部署的流水线。部署流水线(也称构建流水线staged 流水线)这个概念背后的支撑是,在整个流程中确实发生了多次构建。提交到主干上的一次提交首先会触发第一次构建——我称这次构建为 提交时构建。任何人向主干上提交代码,都需要执行一次提交时构建。这次提交时构建需要能快速完成,相对地,也削弱了它用于发现 bug 的能力。我们需要在「发现 bug」和「构建速度」上有所权衡,让一次提交时构建能足够稳定——至少别人可以在此基础上继续工作——就可以了。

原文:https://martinfowler.com/articles/continuousIntegration.html

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

JSmiles

生命进入颠沛而奔忙的本质状态,并将以不断告别和相遇的陈旧方式继续下去。

0 文章
0 评论
84961 人气
更多

推荐作者

醉城メ夜风

文章 0 评论 0

远昼

文章 0 评论 0

平生欢

文章 0 评论 0

微凉

文章 0 评论 0

Honwey

文章 0 评论 0

qq_ikhFfg

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文