EagleBear2002 的博客

这里必须根绝一切犹豫,这里任何怯懦都无济于事

软件工程管理-持续集成

摘要

概述

  • 持续集成(Continuous Integration, CI) 是一种软件开发实践,在实践中项目成员频繁地进行集成,通常每个成员每天都会做集成工作,如此,每天整个项目将会有多次集成。
  • 许多团队都发现这种方法大大地减少了集成问题,并且能够快速地开发出高内聚性的软件。

单次瀑布模型与敏捷软件开发:

  • 单次瀑布模型:软件集成是一个漫长并且无法预测的过程。
  • 敏捷软件开发:每个开发人员离共享的工程状态只有咫尺之遥,并且可以在几分钟之内将自己的代码集成进去。任何集成错误都能被快速地发现并得到快速的修正。
  • “持续集成” 源自于极限编程(XP),并且是 XP 最初的 12 种实践之一。
  • “持续集成”服务:Jenkins(https://jenkins.io/)

简单持续集成实践描述

假设我们需要向软件添加一点功能,至于是什么样的功能并不重要,我们假定它很小并且可以在几个小时内完成。

  1. 首先我们需要在本地机器上保留一份当前已经处于集成状态的代码的拷贝。通过代码管理系统(GIT、SVN 等)在代码库的主线(mainline)上拉下(check out)一份工作代码拷贝。
  2. 为了完成软件的功能添加,对本地代码进行修改,其中既包括修改产品代码,也包括添加自动化测试。持续集成非常看重测试,并且在软件代码本身中达到了测试自动化——自测试代码,通常使用流行的 XUnit 测试框架的某个版本。
  3. 当完成了功能开发(或者在开发过程的不同阶段),在本地开发机上完成自动化构建。构建过程将编译并链接本地代码,然后跑自动化测试。只有当构建和测试都没有错误时,该次构建才能算是成功的构建。
  4. 有了本地的成功构建,可以考虑将程序员修改的代码提交到代码库了。但是,在提交之前,其他开发人员可能已经向主线提交了他们的修改,所以首先需要将他们的修改更新到本地并且重新构建。如果他人的修改与我的修改有冲突,那么在本地编译或者测试阶段将会发生错误,这种情况下,我需要负责修改本地代码直到与主线代码保持适当同步为止。
  5. 当本地代码与主线代码同步之后,便可以向主线提交自己的修改了(有些公司规定必须通过代码静态检查和 code review),代码库也得以更新。
  6. 然而,单是提交了修改并不表示工作就完成了。程序员需要再次构建,但这次是在一台拥有主线代码的集成机器上进行。只有这次构建成功了才表示任务完成。通常会出现这样的情况:程序员忘了提交本地机器上的一些东西,因此代码库并没有得到适当的更新。只有程序员提交的修改在集成机器上成功构建之后,工作才算完成。这样的集成构建可以由程序员手动完成,也可以由 Jenkins 等工具自动完成。
  7. 当两个开发者的代码有冲突时,通常会在第二个开发者更新本地代码时捕获到,否则,集成构建应该会失败。在这两种途径中,错误都可以被快速地发现。在持续集成环境中,你决不应该使失败的集成构建保留太长时间。一个好的团队每天都应该有许多成功的构建。当然,失败的构建也会时常发生,但需要尽快的修复。
  8. 这样做的结果是,我们总会得到一个稳定并且工作正常的软件。每个人都围绕着一个共享并稳定的基础代码库工作,绝不离基础代码库太远以至于需要很长的时间将自己的修改集成到基础代码库中。如此这般,我们花在找 bug 上的时间减少了,因为 bug 在频繁的集成中经常出现。

持续集成关键实践

  • 维护一个单一的代码库
  • 使构建自动化
  • 使构建自测试
  • 每人每天都向代码库提交代码
  • 每次提交都应在集成服务器上进行构建
  • 快速构建
  • 在与生产环境相同的环境中运行测试
  • 使任何人都能轻易获得可执行文件
  • 人人都能看到正在发生什么
  • 自动化部署

维护一个单一的代码库

  • 作为最基本的持续集成实践,请保证你使用一款代码管理系统。
  • GIT、SVN……
  • 当你有了代码管理系统之后,确保每个开发者都能方便地获得到源代码。不应该有人还在问:“foo-whiffle 文件在哪儿?”所有东西都必须在代码库里。
  • 原则是:在一台新机器上 check out 代码后构建也能构建成功。新机器上的东西应该尽量的少,通常包括很大的,难于安装的,并且稳定的软件,比如操作系统,Java 开发环境或者数据库管理系统等。
  • 将大家经常操作的东西放进去比如 IDE 配置。构建所需的所有都应该包含在代码库里,包括测试脚本,属性文件,数据库模式文件,安装脚本和第三方库等。
  • 版本控制系统的一大功能是它允许你创建多个分支,以此来处理不同的“开发流”。这种功能很有用,但却经常被过度使用以至给开发者带来了不少麻烦。所以,你需要将分支的使用最小化,特别建议使用主线,即项目中只有单一的开发分支,并且每人在多数时间里都在“离线”工作。
  • 不应该将构建的输出放进去。

使构建自动化

  • 将源代码变成一个能运行的软件系统通常是一个复杂的过程,包括编译,文件搬移,加载数据库模式等等。但其中大多数任务都是可以自动化的,并且也应该被自动化。让人去输入奇怪的命令或点击对话框是非常耗时的,而且从根本上来说也是个错误的做法。
  • 构建所需的自动化环境对于软件系统来说是一个通用功能。Unix 的 Make 已经诞生好多年了,Java 社区有 Ant,.NET 社区有 Nant,现在又有了 MSBuild。当你用这些工具构建和启动系统时,请确保只使用一个命令完成任务。
  • 一个常见的错误是,在自动化构建里并没有完全包括构建所需的东西,比如构建过程中应该从代码库里取得数据库模式文件并自动执行之。
  • 任何人都应该能够在一台新机器上拉下代码库中的代码,并只用一个命令将系统运行起来。
  • 优秀的构建工具能够分析出哪些地方需要做相应的修改,并将这个分析过程本身做为整个构建过程的一部分。
  • 根据自己的需要(考虑到集成代价),你可以选择不同的东西进行构建。构建中既可以包括测试,也可以不包括,甚至可以包括不同的测试板块。有些组件可以进行单独构建。构建脚本应该能够允许你针对不同的情形进行不同的构建目标。
  • 我们大多数都使用 IDE 而多数 IDE 都或多或少地集成了构建管理功能。但是这样构建文件通常是特定于 IDE 的,而且非常脆弱。
  • 虽然对于开发者个人来说,在 IDE 中做这样的构建配置并无不妥,但对于持续集成服务器来说,一份能够被其它脚本调用的主构建脚本却是至关重要的。

使构建自测试

  • 传统意义上的构建包括只编译,链接等过程。此时程序也许能运行起来,但这并不意味着系统就能正确地运行。
  • 一种快速并高效发现 bug 的方法是将自动化测试包含到构建过程中。当然,测试也不见得完美,但的确能发现很多 bug——足够多了。随着极限编程(XP)的流行,测试驱动开发(TDD)也使自测试代码流行起来,越来越多的人开始注意到这种技术的价值所在。
  • 对于自测试代码而言,你需要一组自动化测试来检测大部分代码库中的 bug。测试能通过一个简单得命令来运行并且具备自检功能。测试的结果应该能指出哪些测试是失败的。对于自测试的构建来说,测试失败应导致构建失败。

每人每天都向代码库提交代码

  • 集成首先在于交流,它使其他成员能够看到你所做的修改。在这种频繁的交流下,大家都能很快地知道开发过程中所做的修改。
  • 在向主线提交代码之前,开发人员必须保证本地构建成功。这当然也包括使测试全部通过。另外,在提交之前需要更新本地代码以匹配主线代码,然后在本地解决主线代码与本地代码之间的冲突,再在本地进行构建。如果构建成功,便可以向主线提交代码了。
  • 在这种频繁提交下,开发者可以快速地发现自己代码与他人代码之间的冲突。快速解决问题的关键在于快速地发现问题。几个小时的提交间隔使得代码冲突也可以在几个小时内发现,此时大家的修改都不多,冲突也不大,因此解决冲突也很简单。对于好几周都发现不了的冲突,通常是很难解决的。
  • 基本原则是:每个开发者每天都应当向代码库进行提交。

每次提交都应在集成服务器上进行构建

  • 在实践中,有出错的时候,原因之一在于纪律——有人并没有在提交之前进行本地更新和构建。另外,不同开发机器之间的环境不同也是一个原因。
  • 应该保证在集成服务器上进行构建,只有当集服务器机上构建成功后,才表明你的任务完成了。由于提交者需要对自己的提交负责,他就得盯着主线上的构建,如果失败,马上修改。
  • 主线构建:一是手动构建,二是使用持续集成服务器(Jenkins)。

快速构建

  • 持续集成的关键在于快速反馈,需要长时间构建的 CI 是极其糟糕的。
  • 对于多数项目来说,将构建时间维持在 10 分钟之内是合理的,这也是 XP 的方针之一。
  • 引入阶段性构建---对于企业级应用来说,我们发现构建时间的瓶颈通常发生在测试上,特别是那些需要于外部交互的测试——比如数据库。
  • 一个简单的例子是将构建分为两个阶段,第一个阶段完成编译,并且跑那些不需要外部交互的单元测试,数据库交互也通过 stub 的方式完全消除掉。
  • 在这种情况下,通常将第一阶段视为提交构建,并将此做为主要的 CI 周期。第二阶段则可在有必要时才进行,如果这个阶段构建失败,它也不需要像第一阶段那样“停下全部手头的工作”,但也应该得到尽快的修改。

在与生产环境相同的环境中运行测试

  • 测试旨在发现可能在生产环境中出现的问题,因此如果你的测试环境与生产环境不同,那么测试很有可能发现不了生产环境中的 bug。
  • 虚拟化技术 :虚拟机、Docker(https://www.docker.com/)

使任何人都能轻易获得可执行文件

  • 软件开发最困难的事情之一便是你不能保证所开发的是正确的软件。人们往往很难预知自己究竟想要什么,而相反,对已有的东西进行评判和修改却容易的多。
  • 项目中的所有成员都应能够获得最新的可执行文件并能成功的运行,目的可以包括做演示,浏览测试或者仅仅看看项目本周有何修改。
  • 确保一个通用的地方来存放最新

人人都能看到正在发生什么

  • 持续集成主要在于交流,因此应当保证每人都能轻易看到当前系统的状态和已做的修改。
  • CI 服务器:Jenkins

自动化部署

  • 自动化部署脚本,不仅包括测试环境的脚本,也包括针对生产环境的部署脚本。虽然我们不是每天都向生产环境部署,但自动化部署不仅可以加速部署过程,并且能够减少部署错误。
  • 如果你已经有了生产环境的自动化部署,那么也应该考虑一下相应的自动化回滚。由于失败是时而会发生的事情,在这种情况下,我们希望能快速回滚到失败之前的状态。
  • 在集群环境中,有每次只向一个节点部署的情况,由此在几个小时之内逐渐完成所有节点的部署。
  • 对于一些面向公众的 Web 应用,我所了解的另外一种很有趣的部署方式是,先试验性针对一部分用户进行部署,再通过这些用户的试用情况来决定是否向所有用户部署。(灰度发布、A/B 测试)

持续集成的好处

降低风险!

  • 已经处于项目的末期,但是仍然不知道何时才能结束。
  • 延期集成的缺点在于,很难预测集成到底要花多少时间,更糟的是,你很难了解集成的进展情况。
  • 持续集成正好解决了这些问题。每次集成的时间都不长,任何时候你都知道自己所处的情况,软件的哪些地方在工作,哪些没有。
  • Bug:持续集成并不能消除 bug,却能帮你快速的发现 bug 并予以清除。Bug 也存在积累性,bug 越多,越难清除。部分原因在于 bug 之间存在牵连。另外也存在心理因素,bug 一多,人便没那么多精力去修了——这就是所谓的“Broken Windows 综合征”。
  • 持续部署:有了持续集成,频繁部署也不是什么难事了。频繁部署的价值在于,你的客户可以快速的享用软件的新功能,并能快速的提出反馈。这将有利于清除客户和开发之间的障碍——我认为这是软件开发最大的障碍。

引入持续集成

  1. 第一步需要将构建自动化,并将你所需的所有东西都放在代码管理系统中,可以通过一个命令来构建整个系统。
  2. 在构建中引入一些自动化测试,试着确定出现问题的主要范围,并用自动化测试去发现这些问题。
  3. 使提交构建快速完成。
  4. 对于新项目,从项目开始就采用持续集成。
  5. 寻找帮助,找有经验的人帮助你。