三、持续集成(Continuous Integration)

在软件工程中,集成是一个漫长而又不可预测的过程。因此一些公司开始尝试让团队中的成员每天集成一次,这样开发者不会偏离项目目标太远,只需要短短的几分钟就可以纠正过来。集成中的错误也可以尽快的被发现和修复。

当人们第一次听到这种方法是,有两种反应:

  • 在这里行不通
  • 这样做不能带来多少改变

但当他们尝试的时候,实践它比听着要简单,而且给开发带来了巨大的不同。然后人们的反应就是:“是的,我们这样做,你没有这样做怎么活下来的?”

尽管,CI不需要特殊的工具来部署,但我们发现使用一个CI服务器非常有用。比较出名的有Cruise control

Building a Feature with Continuous Integration

使用CI开发一个功能

最简单的介绍CI的方式展示一个例子。假设,我要开发一个功能,不论这个功能大小,假设它可以在几个小时内开发出来。
我开始从源码库拉代码。一个源码控制系统保存着项目上的所有源码。系统当前状态通常被叫做“主线”,即mainline。任何时候,一个开发者从源码库复制代码到本地机器,被称作‘检出’,即checking out。开发者本机的复制品被称为‘工作副本’,即working copy。

一切就绪后,我开始尽我所能的完成这个功能。这里面包括调整生产的代码以及增加或者改变自动化测试用例。CI需要一个高度的自动化的测试:称为自测代码,这一般需要一个自动化测试框架。
当我的开发完成后,在我开发机器上执行一个自动化编译。这会编译我的代码,并将我的代码链接到一个可执行文件,并跑自动化测试。只有编译和测试都没有错误才算完成。

本地编译通过后,就会考虑提交我的变更到代码仓库。当然,其他开发者可能在我之前已经提交了他的变更。所以首先我要使用他的变更更新我本地的代码副本,然后重新编译。如果他的变更和我的变更冲突,在编译或单元测试的时候都会报错。这是我的责任就是修复这个冲突,然后重新编译,直到编译通过,然后提交变更到仓库。

至此还未结束,代码提交到仓库后,会在CI机器上自动编译,或者认为触发编译,这是如果编译报错应该尽快被解决。在一个好的团队中,编译成功的次数应该大于编译失败的次数。

以上工作完成后,会产生一个包含很少bug,运行良好的的代码。

CI的一些实践

上面大概介绍了CI,以及在工作中是怎么运用的。让上面的一切能正常工作并不容易。下面简单介绍一些关键点:

1. 维护一个源代码仓库

一个软件项目包含了很多文件,跟踪记录这些文件很费时,特别是有很多人一起工作的时候。所以使用一些源代码管理工具就可以大大提高开发效率。常见有:

仓库可以包含你完成这个项目所有需要的所有东西,包括测试脚本、配置文件、数据库schema、安装脚本、其他第三方库等。

2. 自动化编译

让一堆代码变成可以运行的系统是一个复杂的工作,包括编译、链接、加载表结构到数据库等等一系列动作。但是这些工作都可以被自动化。让人来敲这些奇怪的命令或者点击对话框既费时又容易出错。
在Unix世界,自动化的工具已经出现了很久,JAVA社区开发出了Ant,.NET社区有Nant,现在又有了MSBuild。
在自动化编译中,一个常见的错误是不将所有的工作包含在其中。一个主要的原则是:任何一个人可以在一台裸机上,拉取源码后,执行一个简单的命令就可以让这个系统在机器上运行。
一个完整的编译会很耗时,如果你只改了很少的代码,不希望编译所有的文件。所有一个好的编译工具可以分析出哪些文件做了改变需要编译。常见的方法是检查源码的日期和对象文件的日期,只编译源码日期靠后的。但如果文件之间有依赖可能就发现不了。

3. 自动化测试

程序可以运行,但并不表示做着正确的事情。静态输入语言可以发现很多bug,但还是有漏网之鱼。
一个更快更有效发现bugs的方法是在build的过程中包含自动化测试。测试并不是完美的,但可以发现很多bugs。特别是随着极限编程(XP)和测试驱动开发(TDD)的兴起,自测代码(self-testing code)变得越来越流行。

为了测试代码,需要一整套的自动化测试用例来检查大部分的代码是否有bug。测试用例需要能够很简单的就发起,并能自测。测试用例失败必须引起整个build失败。
近几年(指二十一世纪初)开源的XUnit家族开始流行,当然还有其他流行的单元测试框架:

还有很多不详细列举。
当然不可能依靠测试发现所有的问题。但是不完美的测试跑100遍,也好过完美的测试一遍就不跑。

4. 每个人每天都要提交代码

集成主要在于沟通。集成允许一个开发者告诉其他开发者他做了哪些变更。频繁的沟通可以让开发者很快的知道代码做了哪些改变。
代码提交的越频繁,发现代码冲突的可能就越大,需要解决的冲突就越小。
频繁提交代码,也可以让开发者更合理地分解自己的工作

5. 每一次主干分支上的提交都应该在CI机器上编译

由于代码提交频繁了,就必须保证主干分支状态正常。所以每次提交必须保证编译都能成功通过。开发者需要关注自己的每次代码提交后编译的结果,最好能有编译监控,失败后可以发送邮件给到相关开发人员。
不要在晚上编译(一些公司会这么做),这样会让编译错误在一天后才能被发现。错误被发现的越晚,修复的成本就越高。

6. 快速修复失败的build

CI中最关键的一点就是主干编译失败,需要马上修复,必须保证主干分支在一个稳定的状态。
Kent Beck曾说过:“没有什么任务比修复一个build优先级更高。”
但这并不意味着需要整个开发团队停止工作来做这件事情,通常一两个人负责这个事情就行了。

一般最快修复build的方法就是回滚上一次的提交,让系统回到最近一个好的编译。当然最好不要尝试在一个坏的主干上做调试,除非可以很容易发现问题在哪里,只需要回滚,然后在一个开发机器上调试这个问题。
可以使用pending head来避免损坏主干分支。

刚开始运行CI这种工作方式,出错在所难免。所以要有耐心,遇到问题不要气馁。

7. 让编译尽可能的快

CI的关键就是要尽可能快的反馈问题,没有什么比编译慢更能堵塞CI的进行。
大多数项目的编译时间可以保证在10分钟以内,这也是XP提倡的。
由于编译会频繁的进行,所以每次编译节省一分钟,累计下来可以为开发人员节省大量的实践。
通常编译的瓶颈在测试,特别是需要依赖外部服务比如数据库。
还可以建立一个部署pipeline,让多个build有序的完成。代码提交到主干后编译是第一个build,被叫做commit build。commit build需要在很短的时间内完成。这一步主要是代码编译和本地的一些单元测试。但是需要依赖其他外部组件的测试需要很长时间,这个测试可以放到另外的机器上完成。
commit build经常作为CI主要的步骤。如果在第二个stage中发现bug,那么可以考虑这个bug的测试是否可以放在commit
build中,这样会增强第一步测试的准确性。

8. 在准生产测试

环境的不同,可能导致测试结果的不同,所以测试环境要尽量模拟生产。比如使用同一个数据库软件和版本、同一个操作系统、同样的库文件、同一种硬件等等。
在实际生产实践中可能有一些限制,如果你在开发一款桌面应用,可能没有那么容易在每一个用户的桌面环境中测试。还有一种情况是复制生产环境需要高昂的成本。尽管如此,但还是要尽量模拟生产,并了解测试和生产的不同之处在哪里。

9. 方便获取最新的可执行文件

软件开发中最困难的之一就是确保制作正确的软件。我们发现要提前知道人们要什么并且是想要的很困难。为了做到这一点,开发团队的任何人需要可以拿到最新的可执行文件并可以运行它:用来验证、测试或者看看最近有哪些改变。
做到这一点很容易:确保这些可执行文件放在一个大家都知道的地方。

10. 每一个人可以看到当前发生了什么

CI主要是关于沟通,所以需要确保每一个可以很容易看到系统的状态以及最近的一些改变。
通常在CI服务器的编译过程中会有很多指示编译状态变化的标识,比如红色表示编译失败,绿色表示编译成功

11. 自动部署

为了完成CI,你需要多套环境,一个跑主干测试,一个或者多个跑分支测试。所以你需要将可以执行文件在多套环境中移动,当然你想让这些步骤都是自动。

如果你是自动部署生产环境,那么可能也会考虑自动回滚。意外经常会发生,如果能够快速回滚到上一个稳定的版本自然很好。可以自动回滚,也可以鼓励人们更多的部署而不必考虑太多。
集群环境的部署一般采用金丝雀发布,即一次只部署一个节点,让流量平滑的切换到整个集群。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容