Martin Flower谈持续集成

53 阅读1小时+

​我清楚地记得第一份工作初次接触到大型软件项目时的情景,依然记的当时是在一家大型英国电子公司暑期实习。我刚入职就被质量团队的测试经理带着参观公司,他带着我进入了一个巨大的、令人沮丧的、没有窗户的仓库,里面的小隔间挤满正在工作的人,经理告诉我这些程序员同事已经为项目(一个大型软件产品)持续开发了几年的代码, 当他们完成编程时,他们各自的单元(功能)模块正在被集成在一起,而且已经集成了几个月。经理告诉我,没有人真正知道这需要多长时间才能完成集成。通过这件事让我了解到集成多个开发人员的工作是一个漫长而不可预测的过程。

我已经很多年没有听说过一个团队陷入如此漫长的集成环节,但这并不意味着集成是一个不让人痛苦的过程。一个开发人员可能为研发新功能已经投入好几天,定期从主分支拉取最新代码到她的功能分支。而就在她准备pull request功能代码到主分支上时候,此时主分支上发生了一个大的变更,这个变更改变了一些她正在开发的代码。她必须将开发新功能转变为弄清楚如何将她的代码与这个变更集成起来,这对她的同事来说还好,但对她来说却不是什么好事,为了挖掘隐藏缺陷,迫使她调试自己不熟悉的代码。

随着时间的推移,这个团队可能会了解到对核心代码进行重大变更会导致上述问题,因此必须停止这样做。但是,由于阻止了常规重构,最终导致代码库中出现了大量cruft(屎山)。遇到cruft代码库的人想知道它是如何变成这样的,而答案通常在于集成过程中存在太多摩擦,以至于人们不愿删除cruft。

但是这样的问题是可以解决的。我在Thoughtworks的同事们所做的大多数项目都把集成当作一件无关紧要的事情。任何一个开发人员可以在短短几分钟内完成集成,任何集成缺陷都可以迅速地找到并快速地修复。

这个结果并不是昂贵复杂的工具的原因。其本质在于团队中每个人对源代码库进行频繁(至少每天一次)集成的简单实践,这种实践被称为“持续集成”(也可以被称为“基于主干的开发”)。

我写这篇文章有两个原因。首先,总会有新人进入这个行业,我想向给他们建议如何避免令人沮丧的代码仓库。但第二个原因是这个话题需要明确,因为持续集成是一个被误解的概念,有很多人说他们正在做持续集成,一旦他们描述了他们的工作流,很明显他们搞错了持续集成的核心,对持续集成的清晰理解有助于团队间的沟通,所以当我们描述我们的工作方法时,我们知道会发生什么。它还帮助人们意识到还有更多的事情可以做来改善他们的经验。

01/ 使用持续集成构建新功能

对我来说,解释持续集成是什么以及它是如何工作的最简单的方法是通过一个示例来快速展示它是如何工作的。我目前正与一家药剂制造商合作,我们正在帮助扩展他们的质量系统,这个系统用以计算药剂的效果将持续多久。目前系统已经支持了十二种药剂,我们需要扩展飞行药剂(一种新药剂)的逻辑,此药剂引入了一些需要注意的新feature。

我首先将最新的产品源代码拷贝到本地开发环境中。

源代码在我的开发环境中,我可以执行一个命令来构建产品。这个命令检查我的开发环境是否正确设置,将源代码编译成可执行产品,启动产品,并对它运行系统测试。这大概只需要几分钟,然后我开始查看代码,以决定如何开始添加新功能。这个构建几乎从未失败过,但我这样做只是为了以防万一,因为如果它确实失败了,我想在开始进行代码变更之前就知道。如果我在失败的构建上进行变更,我会感到困惑,认为是我的代码变更导致了构建失败。

现在我可以拿着代码副本做任何我需要做的事情。这包括变更产品代码,以及添加或变更一些自动化测试。在这段时间里,我频繁地运行自动化构建和测试。

我现在准备将我的变更推到代码库。我做的第一步是再次拉取,因为有可能,实际上很可能,我的同事在我工作的时候已经将变更推入主干分支。确实有一些这样的变更,我将它们git pull到我的工作副本。我将我的变更合并到它们之上,并再次运行构建。这样做也许会让你感觉是多余的,但这次测试就是构建失败了。测试为我提供了构建失败的线索,但我发现查看我拉取的变更代码比看报错更有效。似乎有人对一个函数进行了调整,将它的一些逻辑移到它的调用程序中。他们修复了主干代码中的所有调用程序,但我在我的变更中添加了一个新的调用,当然,他们在提交代码时还看不到我新增的调用代码。我做了同样的调整并重新运行构建,这次通过了。

因为我花了几分钟时间来整理,我再次拉取,再次有一个新的提交。然而,这个构建工作正常,所以我能够 git push 我的变更到代码库。

然而,git push并不意味着我完成了新feature的研发。一旦我把代码推送到主干,持续集成服务就会注意到我的提交,将变更后的代码签出到 CI 代理上,并在 CI 代理上构建它。由于在我的开发环境中构建运行良好,所以我并不期望它在 CI 服务上失败, 但是“在我的机器上运行”是程序员圈中一个众所周知的短语是有原因的。很少有东西会导致 CI 服务构建失败,但是很少并不等于从不。

在CI服务器的构建并不需要很长时间,但是对于一个急切的开发人员来说,这段时间已经足够长,而我是一个老码农,所以我喜欢花几分钟时间来伸展一下腿脚,阅读一封电子邮件。很快我从 CI 服务那里得到了一个通知,构建一切正常,因此我再次开始这个过程,以进行新功能的进一步开发工作。

02/ 持续集成实践

上面的故事是持续集成的一个例子,希望能让你了解一个普通程序员的工作是什么样的。但是,与任何事情一样,在日常工作中,有很多事情需要解决,所以现在我们将讨论继续集成的关键实践。

把所有东西都放在版本控制的主干上

现在几乎每个软件团队都将他们的源代码保存在一个版本控制系统中,这样每个开发人员不仅可以轻松找到产品的当前状态,还可以找到对产品所做的所有变更。版本控制工具允许系统回滚到开发中的任何点,这对于理解系统的历史非常有帮助,使用差异调试来找到缺陷。在我写这篇文章的时候,占主导地位的版本控制系统是git。

但是,虽然版本控制是司空见惯的,一些团队却不能充分利用版本控制。我对完全版本控制的测试是,我应该能够在一个配置非常少的环境中行走——比如一台只安装了Vanilla OS 的笔记本电脑——并且能够在克隆存储库后轻松地构建和运行产品。这意味着存储库应该可靠地返回产品源代码、测试、数据库模式、测试数据、配置文件、IDE配置、安装脚本、第三方库以及构建产品所需的任何工具软件。

你可能注意到我说过存储库应该包含所有这些元素,这与存储它们是不一样的。我们不需要在存储库中存储编译器,但我们需要能够获得正确的编译器。如果我检出去年的产品源代码,我可能需要能够使用去年使用的编译器来构建它们,而不是我现在使用的版本。存储库可以通过存储一个链接到不可变的资产存储来实现这一点——不可变的意义是,一旦一个资产被存储在一个id中,我总是能够准确地获得该资产。

类似的资产存储方案可以用于任何太大的东西, 例如视频。克隆一个存储库通常意味着抓取所有东西, 即使它不是必需的。通过使用对资产存储的引用,构建脚本可以选择只下载特定构建所需的东西。

一般来说,我们应该在源代码控制中存储我们构建代码所依赖的所有配置。有些人确实在源代码控制中保存构建产品,但我认为这是一个迹象——一个更深层次问题的迹象,通常是无法可靠地重新创建构建。缓存构建产品可能有用,但它们应该始终被视为可处置的,通常最好确保它们被及时删除,以便人们在不应该依赖它们的时候就忽略它们。

这个原则的第二个要素是,它应该很容易找到给定工作的代码。这其中的一部分是明确的名称和URL方案,无论是在存储库中还是在更广泛的企业中。这也意味着不必花时间弄清楚要使用版本控制系统中的哪个分支。持续集成依赖于有一个清晰的主干——一个单一的、共享的、充当产品当前状态的分支,这是将部署到生产的下一个版本。

使用git的团队大多使用“main”作为主干分支,但我们有时也会看到“trunk”或旧的默认值“master”。主干是中央仓库上的分支, 因此,要向名为 main 的主干上添加一个提交,我需要首先向我的本地副本 main 提交,然后将该提交推送到代码库。跟踪分支(称为 origin/main )是我本地机器上主干的副本。然而,它可能过时,因为在持续集成环境中,每天都会有许多提交推送到主干上。

我们应该尽可能地使用文本文件来定义产品及其环境。我之所以这样说,是因为尽管版本控制系统可以存储和跟踪非文本文件,但它们通常不提供任何方便的工具来查看版本之间的差异。这使得理解发生了什么变更变得更加困难。在将来,我们可能会看到更多的存储格式 具有创建有意义的差异的工具,但目前,清晰的差异几乎完全是为文本格式保留的。即使在这种情况下,我们也需要使用文本格式来产生可理解的差异。

自动构建

将源代码转换为运行中的系统通常是一个复杂的过程,涉及编译、移动文件、将模式加载到数据库中等等。然而,像软件开发中大部分任务一样,它可以自动化,因此应该自动化。要求人们输入陌生的命令或点击对话框是浪费时间和缺陷的温床。

计算机被设计用来执行简单的重复性任务。一旦你让人类代替计算机做重复性任务,所有的计算机就会在深夜聚在一起嘲笑你。——尼尔·福特

大多数现代编程环境都包含自动化构建的工具,而且这些工具已经存在很长时间了。我第一次使用它们是通过 make 工具,它是最早的 Unix 工具之一。

任何构建指令都需要存储在存储库中, 在实践中这意味着我们必须使用文本表示。这样我们可以轻松地检查它们,以了解它们是如何工作的,最重要的是,当它们变更时,可以看到差异。因此,使用持续集成的团队避免了需要在 UI 中点击来执行构建或配置环境的工具。

使用常规的编程语言来自动化构建是可能的,实际上简单的构建通常被捕获为shell脚本。但随着构建变得越来越复杂,最好使用一个专门为构建自动化设计的工具。部分原因是这样的工具将拥有用于常见构建任务的内置函数。但主要原因是构建工具以一种特定的方法组织逻辑时工作得最好——一种替代计算模型,我称之为依赖网络。依赖网络将逻辑组织成任务,这些任务被结构化为依赖图。

一个非常简单的依赖网络可能会说“测试”任务依赖于“编译”任务。如果我调用测试任务,它会查看是否需要运行编译任务,如果是,它会首先调用编译任务。如果编译任务本身有依赖关系,网络会查看是否需要首先调用它们,依此类推,沿着依赖链倒推。像这样的依赖网络对于构建脚本很有用,因为任务通常需要很长时间,如果它们不需要,那么时间就是浪费的。如果自从我上次运行测试以来没有人变更任何源文件,那么我可以节省做一个潜在的长时间编译。

为了判断是否需要运行某个任务,最常见且最直接的方法是查看文件的修改时间。如果编译的输入文件中的任何一个修改时间晚于输出文件,那么我们就知道如果调用该任务,则需要执行编译。

一个常见的缺陷是没有在自动构建中包括所有内容。构建应该包括从存储库中获取数据库模式并在执行环境中启动它。我将详细说明我之前的经验法则:任何人都应该能够带入一台干净的机器,从存储库中检查源代码,发出一个命令,并在他们自己的环境中有一个运行中的系统。

虽然一个简单的程序可能只需要一两行脚本文件来构建,但复杂的系统通常有一个大的依赖关系图,经过细微的调整以尽量减少构建所需的时间。例如,这个网站有超过一千个网页。我的构建系统知道,如果我变更了这个页面的源代码,我只需要构建这个页面。但是如果我变更了发布工具链中的一个核心文件,那么它需要重新构建它们。无论哪种方法,我都在编辑器中调用相同的命令,构建系统会计算出需要做多少工作。

根据我们的需求,我们可能需要构建不同类型的东西。我们可以构建一个带或不带测试代码的系统,或者使用不同的测试集。一些组件可以独立构建,构建脚本应该允许我们为不同的情况构建替代目标。

让构建进行自测

传统上,构建意味着编译、连接和所有需要让程序执行的附加内容。程序可以运行,但这并不意味着它就能正确地执行业务逻辑。现代静态类型语言可以捕捉到许多 bug,但更多的缺陷会逃逸。如果我们想按照持续集成的要求频繁地集成,这就是一个需要解决的关键问题。如果 bug 被引入产品,那么我们就面临着在快速变化的代码库上做 bug 修复的艰巨任务。而手动测试太慢,无法满足集成的频率。

面对这种情况,我们需要确保在第一时间内缺陷不会引入产品。实现这一目标的主要技术是测试套件,在每次集成之前运行,尽可能多地清除缺陷。当然,测试可能并不完美,但它可以捕捉到很多缺陷。我使用过的早期计算机在启动时进行可见内存的自测,这让我将其称为自测代码。

编写自测试代码会影响程序员的工作流程。任何编程任务都既包含修改程序的功能,也包含扩展测试套件以验证这种改变的行为。程序员的工作不仅仅包含研发新功能,而且还要有自动化测试来证明这一功能是被正确地实现。

自从这篇文章第一版后的二十年里,我看到编程环境越来越需要为程序员提供构建此类测试套件的工具。这方面最大的推动力是JUnit,最初由Kent Beck和Erich Gamma编写,在20世纪90年代末对Java社区产生了显著的影响。这激发了其他语言的类似测试框架,通常称为Xunit框架。这些强调轻量级、程序员友好的机制,允许程序员轻松地与产品代码一起构建测试。这些工具通常具有某种图形化进度条,如果测试通过,则为绿色,但如果任何测试用例执行失败,则变为红色 - 这引申出了类似“绿色构建”或“红色进度条”的短语。

测试套件就是我们开发代码的信心保证,如果测试结果是绿色的,那么产品中就没有重大的 bug。我喜欢想象一个顽皮的小精灵,它能够对产品代码进行简单的修改,比如注释行,或者反转条件,但却不能改变测试。一个健全的测试套件永远不会允许小精灵在没有测试结果变成红色的情况下做任何破坏。任何测试失败都足以让构建失败,99.9%的绿色仍然是红色。

自测代码对于持续集成来说非常重要,甚至可以说是必不可少的先决条件。通常情况下,实现持续集成的最大障碍就是测试不足。

自测代码和持续集成如此紧密地联系在一起并不奇怪。持续集成最初是作为极限编程的一部分开发的,而测试一直是极限编程的核心实践。这种测试通常以测试驱动开发(TDD)的形式完成,这种实践指导我们永远不要编写新的代码,除非它修复了我们刚刚开发且运行失败的测试。TDD对于持续集成来说不是必需的,因为只要在集成之前完成,测试可以在生产代码之后编写。但我发现大多数时候TDD是编写自测代码的最佳方法。

自测能对代码库健康状况进行自动检查,而测试是这种代码自动验证的关键,许多编程环境提供额外的验证工具。Linter可以检测糟糕的编程实践, 并确保代码遵循团队首选的格式化方法,漏洞扫描器可以发现安全漏洞。

当然,我们不能指望测试发现所有缺陷。正如人们常说的:测试并不能证明没有缺陷。然而,完美并不是我们从自测构建中获得回报的唯一点。经常运行的不完美测试比从未编写的完美测试要好得多。

每人每天都向主干分支提交代码

集成主要是关于沟通的。集成允许开发人员告诉其他开发人员他们所做的变更。频繁的沟通可以让人们在变更发生时快速了解情况。

开发人员提交到主干分支的先决条件是他们能够正确地构建代码。当然,这包括通过构建测试。与任何提交周期一样,开发人员首先更新他们的工作副本以匹配主干分支,解决与主干分支的任何冲突,然后在本地机器上构建。如果构建通过,那么他们就可以自由地推送到主干分支。

如果每个人都频繁地提交到主干分支,开发人员可以快速地发现两个开发人员之间的冲突。快速修复问题的关键是快速发现问题。开发人员每几个小时提交一次,冲突可以在发生后几个小时内检测到,在那个时候还没有发生太多的代码变更,出现的冲突则很容易解决。而几个星期没有被发现的冲突可能很难解决。****

半集成

对于“集成”这个术语,人们通常会感到困惑。许多开发分支的程序员,也许是功能分支或个人开发分支,会定期从主干拉出变更。他们会在拉出后通过构建和测试来检查这些变更是否会破坏他们的工作。他们可能会使用 CI 服务来完成这项工作,检查任何对主干的变更是否会破坏他们的分支。

但这并不是完整的集成过程。完整的主干集成要求开发人员将他们的工作推回到主干。如果他们不这样做,那么其他团队成员就无法看到他们的工作,并检查任何冲突。这种半集成不能防止分支的分歧, 冲突的恶化,以及所有低频率集成的问题。持续集成要求完整的主干集成,没有代码在分支上停留超过几个小时而不被推回主干。

代码库中的冲突有不同的形式,其中最容易发现和解决的是“合并冲突”,当两个开发人员以不同的方法编辑相同的代码片段时,一旦第二个开发人员将更新的主干分支代码拉入他们的工作副本,版本控制工具很容易检测到这些冲突。更难的问题是语义冲突,如果我的同事变更了一个函数的名字,而我在新添加的代码中调用这个函数,版本控制系统也无法帮助我们。在静态类型的语言中,我们会得到一个编译失败,这很容易检测到,但在动态语言中,我们得不到这样的帮助。当同事变更了我调用的函数的函数体时,即使静态类型的编译也无法帮助我们,对它所做的事情做出了微妙的变更,这就是为什么自测代码如此重要。

测试失败会提醒我们在变更之间存在冲突,但是我们仍然需要找出冲突是什么,以及如何解决它。由于提交之间的变更只有几个小时,所以问题可能只隐藏在许多地方。此外,由于没有太多的变更,我们可以使用差异调试来帮助我们找到缺陷。

我的经验法则是每个开发人员都应该每天都提交到主干分支。在实践中,那些有持续集成经验的人集成的频率要比这个频率高。我们集成的频率越高,我们需要寻找冲突缺陷的地方就越少,我们修复冲突的速度就越快。

频繁的提交鼓励开发人员将他们的工作分解成几个小时一小块。这有助于跟踪进度并提供一种进展感。通常人们最初会觉得他们无法在几个小时内做一些有意义的事情,但我们发现指导和实践有助于我们学习。

每一次pr都应该触发一次构建

如果团队中的每个人至少每天都集成,这应该意味着主干保持健康状态。然而,在实践中,事情仍然会出错。这可能是由于操作上的疏忽,忽略了在推之前更新和构建,也可能是开发人员工作区之间的环境差异。

因此,我们需要确保每个提交都在参考环境中得到验证。通常的做法是使用持续集成服务(CI Service)来监控主干。(CI Service的例子是Jenkins、GitHub Actions、Circle CI等工具)每当主干收到一个提交时,CI Service就会将主干的头部签入到集成环境中,并执行完整的构建。只有当这个集成构建是绿色的,开发人员才能认为集成已经完成。通过确保我们拥有每个推送的构建, 如果我们遇到了故障,我们就知道故障在于最新的推送,缩小了需要修复它的范围。

我在这里想强调的是,当我们使用CI服务时,我们只在主干上使用它,主干是版本控制系统参考实例的主分支。使用CI服务来监控和构建多个分支是很常见的,但是集成的全部意义在于让所有提交共存于一个分支上。虽然使用CI服务为不同的分支进行自动化构建可能是有用的,但这与持续集成是不一样的,使用持续集成的团队只需要CI服务来监控产品的单个分支。

虽然现在几乎所有的团队都在使用CI服务,但是没有CI服务也可以进行持续集成。团队成员可以手动将主干上的头文件签入到集成机器上,并执行构建来验证集成。但是当自动化如此免费时,手动过程就没有什么意义了。

立即修复失败的构建

只有主干保持健康状态,持续集成才能工作。如果集成构建失败,那么就需要立即修复。正如Kent Beck所说:“没有人比修复构建更重要的任务。” 这并不意味着团队中的每个人都必须停止他们正在做的事情来修复构建,通常只需要几个人就可以让事情再次工作。它确实意味着有意识地将构建修复作为紧急的、高优先级的任务进行优先级排序。

通常修复构建的最佳方法是恢复来自主干的最新提交,将系统回退到最近的一次成功构建。如果问题的原因是显而易见的,那么它可以直接通过新的提交来修复,但如果不是这样,那么恢复主干可以让一些人在单独的开发环境中找出问题,让团队的其他成员继续使用主干。

有些团队喜欢使用Pending Head(也称为Pre-tested,Delayed,或Gated Commit)来消除所有破坏主干的风险。要做到这一点,CI服务需要设置一些东西,以便推送到主干进行集成的提交不会立即进入主干。相反,它们被放置在另一个分支上,直到构建完成,并且只有在绿色构建之后才迁移到主干。虽然这种技术避免了任何破坏主干的危险,但一个高效的团队应该很少看到红色主干,在少数几次发生时,它的可见性鼓励人们学习如何避免它。

保持快速构建

持续集成的意义在于提供快速的反馈。没有什么比一个耗时很长的构建更能消耗持续集成的精力了。在这里,我必须承认一个古怪的老家伙对被认为是长时间的构建感到好笑。我的大多数同事认为一个耗时一小时的构建是完全不合理的。我记得团队梦想着他们可以这么快就完成,偶尔我们仍然遇到很难达到那种速度的情况。

然而,对于大多数项目来说,十分钟构建的XP指导方针是完全合理的。我们大多数现代项目都达到了这个目标。为了实现它,我们值得集中精力,因为从构建时间中削减的每一分钟都是为每个开发人员在每次提交时节省的一分钟。由于持续集成要求频繁提交,这会增加很多时间。

如果我们盯着一个小时的构建时间,那么得到一个更快的构建可能看起来是一个令人畏惧的前景。它甚至可以令人生畏地工作在一个新项目上,并考虑如何保持快速。至少对于企业应用程序,我们已经发现通常的瓶颈是测试 - 特别是涉及外部服务(如数据库)的测试。

也许最关键的一步是建立一个部署流水线。部署流水线(也称为构建流水线或分阶段构建)背后的理念是,实际上有多个构建是按顺序完成的。对主干的提交触发第一个构建——我称之为提交构建。提交构建是当有人将提交推送到主干时所需的构建。提交构建必须快速完成,因此它将采取一些捷径,这将降低检测缺陷的能力。诀窍是平衡缺陷发现和速度的需求,以便一个好的提交构建足够稳定,供其他人使用。

一旦提交的版本是好的,那么其他人就可以放心地在代码上工作了。然而,我们可以开始做更深入、更慢的测试。额外的机器可以对构建运行更深入的测试例程,这需要更长的时间。

一个简单的例子就是两阶段的部署流水线。第一阶段会进行编译和运行测试,这些测试是更本地化的单元测试,用Test Doubles替换慢速的服务,比如一个假的内存数据库或外部服务的存根。这样的测试可以运行得很快,保持在10分钟的指导原则内。然而,任何涉及大规模交互的缺陷,特别是那些涉及实际数据库的缺陷,都不会被发现。第二阶段构建运行一套不同的测试,它会触及一个实际的数据库,并涉及更多的端到端行为。这个套件可能需要几个小时才能运行。

在这种情况下,人们使用第一阶段作为提交构建,并将其作为主要的CI周期。如果次要构建失败,那么它可能没有同样的“停止一切”质量,但团队的目标是尽快修复这些bug,同时保持提交构建的运行。由于次要构建可能要慢得多,它可能不会在每次提交后运行。在这种情况下,它会尽可能频繁地运行,从提交阶段选择最后一个好的构建。

如果辅助构建检测到一个 bug,这表明提交构建可以进行另一个测试。我们尽可能地希望确保任何后期的失败都会导致提交构建中的新测试捕获 bug,因此 bug 在提交构建中保持固定。这样,无论什么时候有东西通过了提交测试,提交测试都会得到加强。有些情况下,没有办法构建一个快速运行的测试来暴露 bug,因此我们可能决定只在辅助构建中测试该条件。幸运的是,大多数时候,我们可以向提交构建添加合适的测试。

另一种加速的方法是使用并行性和多台机器。特别是云环境,允许团队轻松地为构建旋转一小队服务器。提供测试可以合理地独立运行,编写良好的测试可以,然后使用这样的舰队可以获得非常快的构建时间。这种并行云构建可能对开发人员的集成前构建也是有价值的。

当我们考虑更广泛的构建过程时,值得提及另一类自动化,即与依赖项的交互。大多数软件使用由不同组织生产的大量依赖软件。这些依赖项的变化可能会导致产品中断。因此,团队应该自动检查依赖项的新版本,并将它们集成到构建中, 本质上就像它们是另一个团队成员一样。这应该经常进行,通常至少每天进行一次,这取决于依赖项的变化速率。运行合同测试时应该使用类似的方法。如果这些依赖项的交互变成红色,它们不会像常规构建失败那样具有“停止生产线”的效果,但确实需要团队立即采取行动进行调查和修复。

Hide Work-in-Progress

持续集成意味着只要有一点前进的进展,构建是健康的,就进行集成。这通常意味着在用户可见的特性完全形成并准备发布之前进行集成。因此,我们需要考虑如何处理潜在的代码:代码是当前发布中未完成特性的一部分。

有些人担心潜在代码,因为它把非生产质量的代码放到了发布的可执行文件中。进行持续集成的团队确保所有发送到主干的代码都是生产质量的,以及验证代码的测试。潜在代码可能永远不会在生产中执行,但这并不会阻止它在测试中被执行。

我们可以通过使用基石接口来防止代码在生产中执行 - 确保提供新功能路径的接口是我们添加到代码库的最后一件事。测试仍然可以在最终接口之外的所有级别检查代码。在一个设计良好的系统中,这样的接口元素应该是最小的,因此可以简单地用一个短的编程片段添加。

使用Dark Launching,我们可以在让用户看到之前在生产环境中测试一些变更。这种技术对于评估性能的影响很有用。

Dark Launching(或者叫Dark Testing) 是Fackbook使用的一种测试产品新功能的测试方法,这种方法一般使用在用户较多的情况下。如何模拟百万个用户使用一个新的功能?一般对用户界面不做改变,通过一个隐藏的方法(或请求)去访问后台服务,这样即使后台服务有错误,也不会反应在用户界面上,后台可以通过日志修改这些错误。比如facebook把一个普通输入框换成带自动完成功能的框就使用这种测试方法。后台部署后,界面不改变,用户输入时可以挂一个事件,悄悄地发送请求到后台服务。对用户来说没有任何改变,但后台可以通过日志检查错误。

当我们要执行潜在代码时,就会检查Feature Flags,它们被设置为环境的一部分,也许在一个特定于环境的配置文件中。这样,潜在代码可以被测试激活,但在生产中被禁用。除了支持持续集成,Feature Flags还使A/B测试和Canary Release的运行时切换变得更容易。然后,我们确保在功能完全发布后立即删除这个逻辑,这样Flags就不会扰乱代码库。

抽象分支是管理潜在代码的另一种技术,对于代码库中大型基础设施变更特别有用。本质上,这创建了一个到正在变更的模块的内部接口。然后,该接口可以在新旧逻辑之间路由,随着时间的推移逐渐替换执行路径。我们已经看到这种方法被用于切换像变更持久性平台这样的普遍元素。

当引入一个新特性时,我们应该始终确保我们可以在出现问题时回滚。并行变更(又名扩展-合约)将变更分解为可逆的步骤。例如,如果我们重命名一个数据库字段,我们首先创建一个新字段,然后写入新旧字段,然后从现有的旧字段复制数据,然后从新字段读取数据,然后删除旧字段。我们可以逆转这些步骤中的任何一个,如果我们一次性做出这样的变更,这是不可能的。使用持续集成的团队通常希望以这种方法分解变更,保持变更小且易于撤销。

在克隆的生产环境中进行测试

基础设施即代码

代码基础设施是一种通过源代码定义计算和网络基础设施的方法,这些源代码可以像任何软件系统一样对待。这些代码可以保存在源代码控制中,以允许可审计性和可复制构建,但要遵守测试实践和持续交付的完整规程。这是过去十年来一直用于处理不断增长的云计算平台的方法,并将成为未来处理计算基础设施的主要方法。

测试的重点是在受限条件下,排除系统在生产中可能出现的任何问题。其中一个重要部分是生产系统运行的环境。如果我们在不同的环境中进行测试,那么每个差异都会导致一种风险,即在测试中发生的事情在生产中不会发生。

因此,我们希望建立我们的测试环境,尽可能地精确地模拟我们的生产环境。使用相同的数据库软件,具有相同的版本,使用相同版本的操作系统。把所有适当的库放在生产环境中,到测试环境中,即使系统实际上没有使用它们,使用相同的IP地址和端口,在相同的硬件上运行。

虚拟环境使这比过去容易得多。我们在容器中运行产品软件,并可靠地构建完全相同的容器进行测试,即使是在开发人员的工作空间中。这样做是值得的,与寻找由环境不匹配所造成的漏洞而产生的单个缺陷相比,这种成本通常很小。

有些软件被设计为在多个环境中运行,例如不同的操作系统和平台版本。部署流水线应该安排在所有这些环境中并行测试。

需要注意的一点是,生产环境不如开发环境好。生产软件是否会在连接到不稳定的wifi的机器上运行,比如智能手机?那么确保测试环境可以模拟不稳定的网络连接。

每个人都能看到发生了什么

持续集成是关于沟通的,所以我们希望确保每个人都能轻松地看到系统的状态和已经对其做出的变更。

最重要的事情之一是沟通主干构建的状态。CI服务有仪表板,允许每个人看到他们正在运行的任何构建的状态。它们经常与其他工具连接,以向内部社交媒体工具(如Slack)广播构建信息。IDE通常有连接到这些机制的钩子, 因此开发人员可以在仍然在他们正在使用的工具中进行大部分工作时收到警告。许多团队只在构建失败时发送通知,但我认为在成功时也值得发送消息。这样人们就会习惯定期发送信号,并对构建的长度有一个概念。更不用说每天都能得到“做得好”的消息了,即使它只是来自CI服务器。

共享物理空间的团队通常会为构建提供某种类型的始终在线的物理显示。通常这会采用显示简化仪表板的大屏幕的形式。这对于提醒每个人构建出错尤其有价值,通常会使用主干提交构建的红/绿颜色。

我非常喜欢的一个较老的物理显示器是红色和绿色熔岩灯的使用。熔岩灯的一个特点是,它们打开一段时间后开始冒泡。这个想法是,如果红色灯亮起来,团队应该在它开始冒泡之前修复构建。构建状态的物理显示器通常很有趣,为团队的工作空间添加了一些古怪的个性。我对一只跳舞的兔子有美好的回忆。

除了当前的构建状态,这些显示可以显示有关最近历史的有用信息,这可以是项目健康状况的指示器。回到世纪之交,我曾与一个团队合作,他们有无法创建稳定构建的历史。我们在墙上贴了一张日历,上面有一个小方块,表示一整年。如果QA团队每天都收到一个通过提交测试的稳定构建版本,他们就会在当天贴上绿色贴纸,否则就是红色方块。随着时间的推移,日历显示了构建过程的状态,显示了稳定的改进,直到绿色方块变得如此普遍,以至于日历消失了——它的目标达到了。

自动化部署

为了进行持续集成,我们需要多个环境,一个用于运行提交测试,可能更多的用于运行部署流水线的其他部分。由于我们每天都在这些环境之间多次移动可执行文件,因此我们希望能够自动完成此操作。因此,拥有允许我们轻松地将应用程序部署到任何环境中的脚本是很重要的。

有了现代的虚拟化、容器化和无服务器工具,我们可以走得更远。不仅有部署产品的脚本,还有从头开始构建所需环境的脚本。这样我们就可以从现成的简陋环境开始,创建产品运行所需的环境,安装产品,并运行它 - 所有这些都是完全自动的。如果我们使用特性标志来隐藏正在进行的工作,那么这些环境可以设置所有特性标志,因此这些特性可以与所有内在的交互进行测试。

这样做的自然结果是,这些相同的脚本允许我们以类似的便利将代码部署到生产环境中。许多团队每天使用这些自动化将新代码多次部署到生产环境中,但即使我们选择更频繁的频率,自动部署也有助于加快流程并减少缺陷。它也是一个廉价的选择,因为它只是使用了我们用于部署到测试环境中的相同功能。

如果我们自动部署到生产环境中,我们发现一个额外的功能是自动回滚。坏事时不时地发生,如果有臭味的棕色物质撞击旋转的金属,最好能够快速地回到最后已知的良好状态。能够自动恢复还减少了部署的紧张程度, 鼓励人们更频繁地部署,从而快速向用户提供新功能。Blue Green Deployment 允许我们通过在部署的版本之间转移流量,既能快速发布新版本,又能在需要时同样快速地回滚。

自动化部署使建立金丝雀版本变得更容易,将产品的新版本部署到我们的用户子集,以便在向全部人口发布之前排除问题。

移动应用程序是将自动化部署到测试环境中的重要性的一个很好的例子,在这种情况下,部署到设备上,这样在调用App Store的监护人之前就可以探索新版本。实际上,任何设备绑定的软件都需要有一种方法来轻松地将新版本部署到测试设备上。

当像这样部署软件时,请记住确保版本信息是可见的。一个关于屏幕应该包含一个与版本控制绑定的构建ID,日志应该使人们很容易看到正在运行的软件版本,应该有一些API端点来提供版本信息。

03/ 集成方法

到目前为止,我已经描述了一种集成方法,但如果它不是通用的,那么肯定还有其他方法。与任何事物一样,我给出的任何分类都有模糊的界限,但我发现考虑处理集成的三种方法是有用的:发布前集成、特性分支和持续集成。

最古老的是我在80年代仓库里看到的版本预集成。它将集成视为软件项目的阶段,这个概念是瀑布模型的一个自然组成部分。在这样的项目中,工作被划分为单元,可以由个人或小团队完成。每个单元都是软件的一部分,与其他单元的交互最小。这些单元自己构建和测试(术语“单元测试”的原始用法)。然后,一旦单元准备就绪,我们就将它们集成到最终产品中。这种集成发生一次,然后是集成测试,然后是发布。因此,如果我们考虑这项工作,我们会看到两个阶段,一个是每个人都在并行地开发功能, 然后是单一的集成工作流。

这种方法的集成频率与发布频率有关,通常是软件的主要版本,通常以月或年为单位。这些团队将使用不同的流程来处理紧急的 bug 修复,因此它们可以单独地发布到常规的集成计划中。

现在最流行的集成方法之一是使用特性分支。在这种方法中,特性被分配给个人或小团队,就像老方法中的单元一样。然而,开发人员不是等到所有的单元都完成后再集成,而是在完成特性后将其集成到主干中。有些团队会在每个特性集成后发布到生产环境中,有些团队则喜欢批量发布一些特性。

使用特性分支的团队通常希望每个人定期从主干拉取,但这是半集成。如果丽贝卡和我正在开发不同的特性,我们可能每天都从主干拉取, 但我们不会看到彼此的变更,直到我们其中一个人完成我们的特性并集成,将其推送到主干。然后另一个人将在下一次拉取中看到该代码,将其集成到他们的工作副本中。因此,在每个特性被推送到主干后,其他开发人员将进行集成工作,以将最新的主干推送与他们自己的特性分支结合起来。

这只是半整合,因为每个开发人员都将主干上的变更合并到他们自己的本地分支上。完全整合不会发生,直到开发人员推送他们的变更,导致另一轮的半整合。即使丽贝卡和我都从主干上拉出相同的变更,我们也只是集成了这些变更,而不是与彼此的分支集成。

使用持续集成,我们每天都在将我们的变更推送到主干,并将每个人的变更拉入我们自己的工作。这导致了更多的集成工作,但每一次都小得多。在一个代码库上组合几个小时的工作比组合几天要容易得多。

04/ 持续集成的好处

当讨论这三种集成方法的相对优点时, 大多数讨论实际上是关于集成频率的。发布前集成和特性分支都可以在不同的频率下运行,并且有可能在不改变集成方法的情况下改变集成频率。如果我们正在使用发布前集成,那么月度发布和年度发布之间存在很大的差异。特性分支通常以更高的频率工作,因为当每个特性被单独推送到主干时,集成就会发生,而不是等待一批单元一起批量构建。如果一个团队正在使用特性分支,并且其所有特性都需要不到一天的工作来构建,那么它们实际上与持续集成是相同的。但是持续集成的不同之处在于它是一种高频率的方法。持续集成强调将集成频率本身作为一个目标,而不是将其绑定到特性完成或发布频率上。

因此,大多数团队可以通过增加频率而不改变方法来看到我下面将讨论的因素的有用改进。将特性的大小从两个月减少到两个星期有显著的好处。持续集成的优势是将高频率集成作为基线,设置使其可持续的习惯和实践。

降低交付延迟的风险

很难估计做一个复杂的集成需要多长时间。有时在git中合并可能很困难,但之后一切都运行良好。其他时候它可以是一个快速的合并,但一个微妙的集成缺陷需要几天的时间来找到和修复。集成之间的时间越长,要集成的代码越多,需要的时间就越长——但更糟糕的是不可预测性的增加。

这一切都使得发布前集成成为一种特殊形式的噩梦。因为集成是发布前的最后一个步骤,时间已经很紧,压力很大。在一天的晚些时候有一个难以预测的阶段意味着我们有一个非常难以减轻的重大风险。这就是为什么我对80年代的记忆如此强烈,而且这几乎不是我唯一一次看到项目陷入集成地狱,每次他们修复一个集成缺陷,就会有两个弹出。

任何增加集成频率的步骤都会降低这种风险。要做的集成越少,新版本准备就绪之前未知的时间就越少。特性分支通过将这种集成工作推到单独的特性流上提供帮助,因此,如果单独放置,那么一旦特性准备就绪,流就可以推到主干。

但这一点很重要。如果其他人推送到主干,那么我们在完成该特性之前引入一些集成工作。由于分支是隔离的,在一个分支上工作的开发人员对其他特性可能推送的内容没有太多的可见性, 以及集成它们需要多少工作。虽然存在高优先级特性面临集成延迟的危险,但我们可以通过防止低优先级特性的推送来管理这一点。

持续集成有效地消除了交付风险。集成是如此之小,以至于它们通常不加评论地进行。一个笨拙的集成将是一个需要超过几分钟来解决的问题。最糟糕的情况是冲突,导致有人从头开始他们的工作,但这仍然少于一天的工作损失,因此不会成为可能困扰利益相关者董事会的事情。此外,我们在开发软件时定期进行集成,因此我们可以在我们有更多时间处理它们并练习如何解决它们时面对问题。

即使一个团队没有定期发布到生产环境中,持续集成也是重要的,因为它允许每个人都能准确地看到产品的状态。在发布之前没有任何隐藏的集成工作需要完成,任何集成工作都已经被烘烤了。

减少集成时间

我还没有看到任何严肃的研究,衡量花在集成上的时间与集成的大小如何匹配,但我的轶事证据强烈表明,这种关系不是线性的。如果有两倍的代码要集成,执行集成的时间很可能是四倍。这更像是我们如何需要三行来完全连接三个节点,但六行来连接四个节点。集成都是关于连接的,因此非线性增长,这反映在我的同事的经验中。

在使用特性分支的组织中,大部分时间损失是由个人来承担的。花几个小时试图对主干的一个大变更进行重基是很令人沮丧的。花几天时间等待对一个已完成的 pull request 的代码审查,而等待期间又发生了另一个大的主干变更,这更令人沮丧。不得不把新特性的工作放在一边,以调试在两周前完成的特性集成测试中发现的问题,这会降低生产率。

当我们进行持续集成时,集成通常是非事件的。我拉下主干,运行构建,然后推。如果有冲突,我写的少量代码在我脑海中是新鲜的,所以通常很容易看到。工作流是常规的,所以我们练习它,我们鼓励尽可能地自动化它。

像许多非线性效应一样,集成很容易成为人们学到缺陷经验的陷阱。一个困难的集成可能会给团队带来创伤,以至于团队决定应该减少集成的频率,这只会加剧未来的问题。

这里发生的事情是我们看到团队成员之间更加紧密的合作。如果两个开发人员做出了冲突的决定,我们在集成时就会发现。所以集成之间的时间越短,我们发现冲突的时间就越短,我们可以在冲突变得太大之前处理它。通过高频率的集成,我们的源代码管理系统成为一个沟通渠道, 一个可以沟通的事情,否则可以不说。

更少的缺陷

缺陷——这些讨厌的东西会摧毁信心,扰乱进度和声誉。部署软件中的缺陷会让用户生气。在常规开发过程中突然出现的缺陷会阻碍我们, 使软件的其他部分更难正确工作。

持续集成不能摆脱缺陷,但它确实使它们更容易发现和删除。这与其说是高频率的集成,还不如说是自测试代码的引入。没有自测试代码,持续集成就无法工作,因为没有像样的测试,我们就无法保持健康的主干。因此,持续集成建立了一个有规律的测试制度。如果测试不足,团队将很快注意到,并采取纠正措施。如果一个缺陷是由于语义冲突而出现的, 那么很容易检测到,因为只有少量的代码需要集成。频繁的集成也与差异调试一起工作得很好,因此即使一个缺陷在几周后被发现,也可以缩小到一个小的变更。

缺陷也是累积的。我们拥有的缺陷越多,就越难去除每一个缺陷。部分原因是我们得到缺陷的交互,其中失败显示为多个缺陷的结果 - 使得每个缺陷都更难找到。这也是心理上的 - 当有很多缺陷时,人们没有精力去找到和摆脱缺陷。因此,通过持续集成来增强自测试代码在减少缺陷引起的问题方面具有另一个指数效应。

这又碰到了另一个很多人认为是违反直觉的现象。看到引入变更意味着引入缺陷的频率,人们得出结论,要想拥有高可靠性的软件,他们需要放慢发布速度。这与Nicole Forsgren领导的DORA研究项目完全相反。他们发现,精英团队在进行这些变更时,更快、更频繁地部署到生产环境中,并且有极低的失败发生率。研究还发现,当团队在应用程序的代码库中只有三个或更少的活动分支时,他们有更高的性能水平,每天至少将分支合并到主干一次,并且没有代码冻结或集成阶段。

支持重构以保持生产力

大多数团队都观察到,随着时间的推移,代码库会恶化。早期的决策在当时是好的,但在六个月的工作后不再是最佳的。但是变更代码以合并团队所学到的知识意味着在现有代码中引入变更, 这会导致困难的合并,既耗时又充满风险。每个人都记得,某个人做出了对未来来说是一个很好的变更,但却导致几天的努力破坏了其他人的工作。鉴于这种经验,没有人想要重建现有代码的结构,即使现在每个人都在构建它, 从而减缓了新功能的交付。

重构是减缓甚至逆转这种衰退过程的重要技术。定期重构的团队拥有一种严格的技术,通过使用代码的小型、行为保持的转换来改进代码库的结构。这些转换的特性极大地降低了引入 bug 的几率,并且可以快速完成,特别是在自测试代码的基础上。团队可以利用每次机会应用重构,以改进现有代码库的结构,使其更容易、更快地添加新功能。

但是这个快乐的故事可能会被集成的灾难所破坏。一个两周的重构会议可能会极大地改进代码,但会导致长时间的合并,因为其他人已经花了最后两周的时间在旧结构上工作。这将重构的成本提高到令人望而却步的水平。频繁的集成通过确保那些做重构的人和其他人定期同步他们的工作来解决这个困境。当使用持续集成时,如果有人对我正在使用的核心库做出了侵入性的变更,我只需要调整几个小时的编程来适应这些变更。如果他们做了与我的变化方向相冲突的事情,我会马上知道,所以有机会与他们交谈,这样我们就可以找出更好的前进方法。

到目前为止,在本文中,我已经提出了几个关于高频率集成优点的反直觉概念:我们集成得越频繁,我们花在集成上的时间就越少,而且频繁的集成会导致更少的 bug。这可能是软件开发中最重要的反直觉概念:团队花费大量精力保持代码库的健康,以更快和更低的成本交付功能。在编写测试和重构上投入的时间在交付速度上带来了令人印象深刻的回报,而持续集成是团队设置中实现这一工作的核心部分。

什么时候应该使用持续集成

所有这些好处听起来都非常诱人。但是像我这样经验丰富的人总是对光有好处的清单持怀疑态度。没有什么东西是没有成本的,关于架构和流程的决策通常是一个权衡问题。

但我承认持续集成是少数几个例子之一,对于一个忠诚和有技巧的团队来说,使用它几乎没有什么不利因素。零星集成的成本是如此之大,几乎任何团队都可以通过增加他们的集成频率而受益。当好处停止累积时,有一些限制,但这个限制在小时而不是天,这正是持续集成的领域。自测试代码、持续集成和重构之间的相互作用是特别强大的。我们在Thoughtworks已经使用这种方法二十年了,我们唯一的问题是如何更有效地做它 - 核心方法是经过验证的。

但这并不意味着持续集成适合所有人。你可能注意到我说过“对于团队来说,使用它几乎没有什么不利因素”。这两个形容词表明了持续集成并不适合的上下文。

这里的“committed”,我指的是全职致力于一个产品的团队。一个很好的反例是一个经典的开源项目,其中有一个或两个维护者和许多贡献者。在这种情况下,即使维护者每周也只在项目上花费几个小时, 他们也不太了解贡献者,也不清楚贡献者何时贡献,或者他们在做贡献时应该遵循什么标准。这种环境导致了功能分支工作流和拉取请求。在这种情况下,持续集成是不可行的, 尽管努力增加集成频率仍然是有价值的。

持续集成更适合全职开发产品的团队,这通常是商业软件的情况。但在经典的开源和全职模型之间有很多中间地带。我们需要判断使用什么样的集成策略,以适应团队的承诺。

第二个形容词是看团队在遵循必要实践方面的技能。如果一个团队尝试在没有强大的测试套件的情况下进行持续集成,他们将遇到各种各样的麻烦,因为他们没有筛选出 bug 的机制。如果他们不自动化,集成将花费太长时间,干扰开发流程。如果人们不遵守纪律,以确保他们向主干推送的是绿色构建版本,那么主干将一直中断,妨碍每个人的工作。

任何考虑引入持续集成的人都必须牢记这些技能。如果没有代码自测试,持续集成就无法工作,而且它也会给人留下持续集成在做得好的不准确的印象。

也就是说,我并不认为技能要求是特别困难的。我们不需要摇滚明星开发人员来让这个过程在团队中工作。(事实上,摇滚明星开发人员通常是一个障碍,因为那些认为自己是摇滚明星的人通常不是很有纪律性。) 这些技术实践的技能并不难学习,通常问题是找到一个好的老师, 并形成明确纪律的习惯。一旦团队掌握了流程,它通常会感到舒适、顺利和快速。

05/ Q&A

1.持续集成从何而来?

持续集成是Kent Beck在20世纪90年代作为极限编程的一部分开发的一种实践。当时,发布前集成是标准做法,发布频率通常以年为单位。随着更快的发布周期,迭代开发有了普遍的推动。但很少有团队在发布之间的几周内进行思考。Kent定义了这种实践,并通过他参与的项目对其进行了开发,并确定了它与它所依赖的其他关键实践之间的相互作用。

微软以每天构建(通常是隔夜)而闻名,但没有测试制度或对修复缺陷的关注,而这些缺陷是持续集成的关键元素。

有些人认为 Grady Booch 创造了这个术语,但是他只是在他的面向对象设计书中随意地用了一个句子来描述这个短语。他没有把它当作一个定义好的实践, 实际上它也没有出现在索引中。

2.持续集成和基于主干的开发有什么区别?

随着CI服务的流行,很多人使用它们在特性分支上运行常规构建。正如上面解释的,这根本不是持续集成,但它导致很多人说他们正在做持续集成,而他们正在做一些明显不同的事情,这导致了许多困惑。

一些人决定通过创造一个新术语来解决这种语义扩散:基于主干的开发。一般来说,我认为这是持续集成的同义词, 并承认它不会与“在我们的特性分支上运行Jenkins”混淆。我读过一些人试图阐明两者之间的区别,但我发现这些区别既不一致也不引人注目。

我没有使用“基于主干的开发”这个术语,部分原因是我认为创造一个新名字并不是对抗语义扩散的好方法, 但主要原因是重命名这种技术粗鲁地抹杀了那些人的工作,特别是Kent Beck,他在一开始就倡导并开发了持续集成。

尽管我避免使用这个术语,但是有很多关于持续集成的好信息都是在基于主干的开发的旗帜下写的。特别是,Paul Hammant 在他的站点上写了很多优秀的材料。

3.可以在特性分支上运行 CI 服务吗?

简单的回答是“是的,但你在做持续集成”。这里的关键原则是“每个人每天都向主干提交”。在功能分支上进行自动化构建是有用的,但它只是半集成。

然而,以这种方法使用守护进程构建是持续集成的常见困惑。这种困惑来自于称这些工具为持续集成服务,更好的术语应该是“持续构建服务”。虽然使用CI服务对进行持续集成很有帮助,但我们不应该将工具与实践混淆。

3.持续集成和持续交付之间的区别是什么?

早期对持续集成的描述主要关注开发人员在团队开发环境中与主干集成的周期。这种描述没有过多地谈到从集成的主干到产品版本的旅程。那。并不意味着它们不在人们的脑海中。像“自动化部署”和“在生产环境的克隆中测试”这样的实践清楚地表明了对生产路径的认识。

在某些情况下,主干集成之后就没有什么其他的了。我记得 Kent 给我看过一个他在 90 年代末在瑞士开发的系统,他们每天都自动地部署到生产环境中。但这是一个 Smalltalk 系统,没有复杂的生产部署步骤。在 2000 年代早期的 Thoughtworks,我们经常遇到生产路径复杂得多的情况。这导致了一种概念,即在持续集成之外还有一种活动可以解决这个问题。那。这种活动被称为持续交付。

持续交付的目标是产品应该始终处于我们可以发布最新构建版本的状态。这从根本上确保了向生产环境发布的决定是一个商业决策。

对于许多人来说,持续集成是关于将代码集成到开发团队环境中的主干, 而持续交付是部署管道的其余部分,以生产版本为目标。有些人认为持续交付包含了持续集成,其他人则认为它们是紧密联系的伙伴,通常用CI/CD这个名字,其他人认为持续交付仅仅是持续集成的同义词。

4.如何做 pull request 和 code review?

Pull Requests,GitHub的产物,现在被广泛用于软件项目。本质上,它们提供了一种向主干推送添加一些流程的方法,通常涉及到集成前的代码审查,需要另一名开发人员在推送被接受到主干之前批准。它们主要在开源项目的功能分支上下文中开发,以确保项目的维护人员可以审查贡献是否适当地符合项目的方法和未来意图。

集成前的代码评审对于持续集成来说可能是个问题,因为它通常会给集成过程增加显著的阻力。我们不能使用一个可以在几分钟内完成的自动化过程,而是必须找人来做代码评审,安排他们的时间,并在接受评审之前等待反馈。虽然有些组织可能能够在几分钟内进入流程,但这很容易花费数小时或数天的时间——破坏了使持续集成工作的时间安排。

那些做持续集成的人通过重新规划代码评审如何适应他们的工作流来处理这个问题。结对编程很受欢迎,因为它在编写代码时创建了一个连续的实时代码评审,为评审产生了一个更快的反馈循环。Ship / Show / Ask 过程鼓励团队只在必要时使用阻塞代码评审,认识到集成后的评审通常是一个更好的选择,因为它不会干扰集成频率。许多团队发现,细化代码评审是维护一个健康的代码库的重要力量,但当持续集成产生一个对重构友好的环境时,它会发挥最佳作用。

我们应该记住,集成前审查起源于开源环境,在这种环境中,贡献来自弱连接的开发人员。在这种环境中有效的实践需要重新评估,以便为一个紧密联系的全职团队提供支持。

5.如何处理数据库?

当我们增加集成频率时,数据库提供了一个特殊的挑战。在版本控制源中包含数据库模式定义和加载脚本来测试数据很容易。但是这对版本控制之外的数据没有帮助,例如生产数据库。如果我们变更数据库模式,我们需要知道如何处理现有数据。

对于传统的预发布集成,数据迁移是一个相当大的挑战,通常需要组建专门的团队来执行迁移。乍一看,尝试高频率的集成会引入大量的数据迁移工作。

然而,在实践中,视角的变化消除了这个问题。我们在Thoughtworks早期使用持续集成的项目中遇到了这个问题,并通过转向由我的同事Pramod Sadalage开发的演化数据库设计方法来解决它。这种方法的关键是通过一系列迁移脚本定义数据库模式和数据, 这些脚本同时改变了数据库模式和数据。每个迁移都很小, 因此很容易推理和测试。迁移是自然组成的, 因此我们可以顺序运行数百个迁移来执行重大的模式变更,并随着我们前进迁移数据。我们可以将这些迁移存储在版本控制中,与应用程序中的数据访问代码同步, 允许我们使用正确的模式和正确结构化的数据构建任何版本的软件。这些迁移可以在测试数据和生产数据库上运行。


下方扫码关注 软件质量保障,与质量君一起学习成长、共同进步,做一个职场最贵Tester!

好文推荐

往期推荐

聊聊工作中的自我管理和向上管理

经验分享|测试工程师转型测试开发历程

聊聊UI自动化的PageObject设计模式

细读《阿里测试之道》