12 持续集成:你说的CI和我说的CI是一回事吗? 你好,我是石雪峰。今天我来跟你聊聊CI。

之前,我曾应邀参加某公司的DevOps交流活动,他们质量团队的负责人分享了DevOps平台建设方面的经验,其中有一大半时间都在讲CI。刚开始还挺好的,可是后来,我越听越觉得奇怪,以至于在交流环节,我只想提一个问题:“你觉得CI是个啥意思?”后来,为了不被主办方鄙视,话到嘴边我又努力憋回去了。

回来的路上,我就一直在思考这个问题。很多时候,人们嘴上总是挂着CI,但是他们说的CI和我理解的CI好像并不是一回事。比如,有时候CI被用来指代负责内部工具平台建设的团队;有时候,CI类似一种技术实践,间接等同于软件的编译和打包;有时候,CI又成了一种职能和角色,指代负责版本的集成和发布的人。可见,CI的定义跟DevOps一样,每个人的理解都千差万别。

可问题是,如果不能理解CI原本的含义,怎么发挥CI真正的价值呢?以CI的名义打造的平台又怎么能不跑偏,并且解决真正的问题呢?

所以,今天,就让我们一起重新认识下这个“最熟悉的陌生人”。

CI是Continuous Integration的缩写,也就是我们熟悉的持续集成,顾名思义,这里面有两个关键的问题:集成什么东西?为什么要持续?要回答这两个问题,就得从CI诞生的历史说起了。

在20世纪90年代,软件开发还是瀑布模式的天下,人们发现,在很长一段时间里,软件是根本无法运行的。因为按照项目计划,软件的功能被拆分成各个模块,由不同的团队分别开发,只有到了开发完成之后的集成阶段,软件才会被真正地组装到一起。可是,往往几个月开发下来,到了集成的时候,大量分支合并带来的冲突和功能问题集中爆发,团队疲于奔命,各种救火,甚至有时候发现压根集成不起来。

我最初工作的时候,做的就是类似这样的项目。我们负责客户端程序的开发,到了集成的时候才发现,客户的数据库使用的是Oracle,而我们为了省事,使用的是微软Office套件中的Access,估计现在很多刚工作的年轻工程师都没听说过这个数据库,这就导致客户下发的数据没法导入到本地数据库中。结果,整整一个元旦假期,我们都在加班加点,好不容易赶工了一个数据中间层,这才把两端集成起来。

所以,软件集成是一件高风险的、不确定的事情,国外甚至有个专门的说法,叫作“集成地狱”。也正因为如此,人们就更倾向于不做集成,这就导致开发末端的集成环节变得更加困难,从而形成了一个恶性循环。

为了解决这个问题,CI的思想应运而生。CI本身源于肯特·贝克(Kent Beck)在1996年提出的极限编程方法(ExtremeProgramming,简称XP)。顾名思义,极限编程是一种软件开发方法,作为敏捷开发的方法之一,目的在于通过缩短开发周期,提高发布频率来提升软件质量,改善用户需求响应速度。

不知道为什么,每次听到极限编程,我心中都热血沸腾。不管在任何时代,总有那么一群程序员走在时代前沿,代表和传承着极客精神,就像咱们平台的名字极客时间,就代表了不甘于平庸、追求极致的精神,特别好。

扯远了,让我们回归正题。极限编程方法中提出的实践,现在看来依然相当前沿,比如结对编程、软件重构、测试驱动开发、编程规范等,这些词我们都耳熟能详,但是真正能做到的却是凤毛麟角。其中还有一个特别有意思的实践规范,叫作每周40小时工作制,也就是一周工作5天,每天工作8小时。联想到前些日子在网络上引发激烈争论的“996”,就可以看出,极限编程方法在国内的发展还是任重而道远啊。

当然,在这么多实践中,持续集成可以说是第一个被广泛接受和认可的。

关于CI的定义,我在这里引用一下马丁·福勒(Martin Fowler)的一篇博客中的内容,这也是当前最为业界公认的定义之一: CI是一种软件开发实践,团队成员频繁地将他们的工作成果集成到一起(通常每人每天至少提交一次,这样每天就会有多次集成),并且在每次提交后,自动触发运行一次包含自动化验证集的构建任务,以便尽早地发现集成问题。

CI采用了一种反常规的思路来解决软件集成的困境,其核心理念就是:越是痛苦的事情,就要越频繁地做。很多人不理解为什么,举个例子你就明白了。我小时候身体非常不好,经常要喝中药,第一次喝的时候,每喝一口都想吐,可是连续喝了一个星期之后,我发现中药跟水的味道也没什么区别。这其实是因为人的适应力很强,慢慢就习惯了中药的味道。对于软件开发来说,也是这个道理。

如果开发周期末端的一次性集成有这么大的风险和不确定性,那不如把集成的频率提高,让每次集成的内容减少,这样即便失败,影响的也仅仅是一次小的集成内容,问题定位和修复都可以更加快速地完成。这样一来,不仅提高了软件的质量,也大大降低了最后阶段的返工所带来的浪费,还提升了软件交付效率。

你可能会说,这个道理我也懂啊,我们的持续集成就是这样的。别急,我们一起来测试一下。

假如你认为自己所在的项目和团队在践行CI,那么你可以思考3个问题,看看你们是否做到了。

  • 每一次代码提交,是否都会触发一次完整的流水线?
  • 每次流水线是否会触发自动化的测试环节?
  • 如果流水线出现了问题,是否能够在10分钟之内修复?

我曾在现场做过很多次这个测试,如果参与者认为做到了,就会举手表示;如果没有做到,就会把手放下。每次面对一群自信满满的CI“信徒们”,三连问的结果总会让人“暗爽”,因为最开始几乎所有人都会举手,他们坚信自己在实践持续集成。但接下来,我每问一个问题,就会有一半的人把手放下,坚持到最后的人寥寥无几,这几个人面对周边人的目光,内心也开始怀疑起来,如果我再适时地追问两下,基本就都放下了。

这么看来,CI听起来简单易懂,但实施起来并没有那么容易。可以说CI涵盖了三个阶段,每个阶段都蕴含了一组思想和实践,只有把这些都做到了,那才是真正地在实施CI。接下来,让我们逐一看下这三个阶段。

第一阶段:每次提交触发完整的流水线

第一个阶段的关键词是:快速集成。这是对CI核心理念的最好诠释,也就是集成速度做到极致,每次变更都会触发CI。

当然,这里的变更有可能是代码变更,也有可能是配置、环境、数据变更。我之前强调过,要将一切都纳入版本控制,这样,所有的元数据变更都会被版本管理系统捕获,并通过事件或者Webhook的方式通知持续集成平台。

对于现代的持续集成平台,比如大家常用的Jenkins,默认支持多种触发方式,比如定时触发、轮询触发或者Webhook触发。那么,如果想做到每次提交都触发持续集成的话,首先就需要打通版本控制系统和持续集成系统,比如GitLab和Jenkins的集成,网上已经有很多现成的材料,大家照着操作一般都不会有太多问题。但是,只要打通两个系统就足够了吗?显然没有这么简单。实施提交触发流水线,还需要一些前置条件。

1.统一的分支策略

既然CI的目的是集成,那么首先就需要有一条以集成为目的的分支。这条分支可以是研发主线,也可以是专门的集成分支,一旦这条分支上发生任何变更,就会触发相应的CI过程。那么,可能有人会问,很多时候开发都是在特性分支或者版本分支上进行的,难道这些分支上的提交就不要经过CI环节了吗?这就引出了第2个前置条件。

2.清晰的集成规则

对于一个大中型团队来说,每天的提交量是非常惊人的,这就要求持续集成具备足够的吞吐率,能够及时处理这些请求。而对于不同分支来说,持续集成的步骤和要求也不尽相同。不同分支的集成目的不同,相应的环节自然也不相同。

比如,对于研发特性分支而言,目的主要是快速验证和反馈,那么速度就是不可忽视的因素,所以这个层面的持续集成,主要以验证打包和代码质量为主;而对于系统集成分支而言,它的目的不仅是验证打包和代码质量,还要关注接口和业务层面的正确性,所以集成的步骤会更加复杂,成本也会随之上升。所以,根据分支策略选择合适的集成规则,对于CI的有效运转来说非常重要

3.标准化的资源池

资源池作为CI的基础设施,重要性不言而喻。

首先,资源池需要实现环境标准化,也就是任何任务在任何节点都具备可运行的能力,这个能力就包括了工具、配置等一系列要素。如果CI任务在一个节点可以运行,跑到另外一个节点就运行失败,那么CI的公信力就会受到影响。

另外,资源池的并发吞吐量应该可以满足集中提交的场景,可以动态按需初始化的资源池就成了最佳选择。当然,同时还要兼顾成本因素,因为大量资源的投入如果没有被有效利用,那么造成的浪费是巨大的。

4.足够快的反馈周期

越是初级CI,对速度的敏感性就越强。一般来讲,如果CI环节超过10~15分钟还没有反馈结果,那么研发人员就会失去耐心,所以CI的运行速度是一个需要纳入监控的重要指标。对于不同的系统而言,要约定能够容忍的CI最大时长,如果超过这个时长,同样会导致CI失败。所以,这就需要环境、平台、开发团队共同维护。

你看,一套基本可用的CI所依赖的条件远不止这些,核心还是为了能够在最短的时间内完成集成动作并给出反馈。如果你们公司已经实现了代码提交的CI,并且不会有大量失败和排队的情况发生,那么,恭喜你,第一阶段就算通过了。

第二阶段:每次流水线触发自动化测试

第二个阶段的关键词是:质量内建。关于质量内建,我会在专栏后面的内容中详细介绍。实际上,CI的目的是尽早发现问题,这些问题既包括构建失败,也包括质量不达标,比如测试不通过,或者代码规约静态扫描等不符合标准。

我见过的很多CI都是“瘸腿”CI,因为缺失了自动化测试的能力注入,或者自动化测试的能力很差,基本无法发现有效问题。这里面有几个重要的关注点,我们来看一下。

1.匹配合适的测试活动

对于不同层级的CI而言,同样需要根据集成规则来确定需要注入的质量活动。比如,最初级的提交集成就不适合那些运行过于复杂、时间太长的测试活动,快速的代码检查和冒烟测试就足以证明这个版本已经达到了最基本的要求。而对于系统层的集成来说,质量要求会更高,这样一来,一些接口测试、UI测试等就可以纳入到CI里面来。

2.树立测试结果的公信度

自动化测试的目标是帮助研发提前发现问题,但是,如果因为自动化测试能力自身的缺陷或者环境不稳定等因素,造成了CI的大量失败,那么,这个CI对于研发来说就可有可无了。所以,我们要对CI失败进行分类分级,重点关注那些异常和误报的情况,并进行相应的持续优化和改善

3.提升测试活动的有效性

考虑到CI对于速度的敏感性,那么如何在最短的时间内运行最有效的测试任务,就成了一个关键问题。显然,大而全的测试套件是不合时宜的,只有在基础功能验证的基础上,结合与本次CI的变更点相关的测试任务,发现问题的概率才会大大提升。所以,根据CI变更,自动识别匹配对应的测试任务也是一个挑战。

当你的CI已经集成了自动化验证集,并且该验证集可以有效地发现问题,那么恭喜你,第二阶段也成功了。但这并不是“一锤子买卖”,毕竟,由于业务需求的不断变化,自动化测试要持续更新,才能保证始终有效。

第三阶段:出了问题可以在第一时间修复

到现在为止,我们已经做到了快速集成和质量内建,说实话,利用现有的开源工具和框架快速搭建一套CI平台并不困难,真正让CI发挥价值的关键,还是在于团队面对持续集成的态度,以及团队内是否建立了持续集成的文化

硅谷的很多公司都有一种不成文的规定,那就是员工每天下班前要先确认持续集成是正常的,然后再离开公司,同时,公司也不建议在深夜或者周末上线代码,因为一旦出了问题,很难在第一时间修复,造成的影响难以估计。

其实,很多企业并不知道他们花费大量人力、物力建设CI的平均修复时长是多少,也缺乏这方面的数据统计。就现状而言,有些时候,他们可以做到在10分钟内修复,而有些时候就需要几个小时,原因可能是负责人出去开会了,或者是赶上了午休的时间。

当然,也有一些企业质疑10分钟这个时间长度,因为软件项目的特殊性,很有可能每次集成周期就远大于10分钟。如果你也是这样想的,那你可能就误解CI的理念和初衷了,毕竟我也不相信马丁·福勒能够保证在10分钟内修复问题。在这么短的时间里,人为因素其实并不可控,所以,人不是关键,建立机制才是关键

什么是机制呢?机制就是一种约定,人们愿意遵守这样的行为,并且做了会得到好处。对于CI而言,保证集成主线的可用性,其实就是团队成员间的一种约定。这不在于谁出的问题谁去修复,而在于我们是否能够保证CI的稳定性,足够清楚问题的降级路径,并且主动关注、分析和推动问题解决。

另外,团队要建立清晰的规则,比如10分钟内没有修复则自动回滚代码,比如当CI“亮红灯”的时候,团队不再提交新的代码,因为在错误的基础上没有办法验证新的提交,这时需要集体放下手中的工作,共同恢复CI的状态。

只有团队成员深信CI带给团队的长期好处远大于短期投入,并且愿意身体力行地践行CI,这个“10分钟”规则才有可能得到保障,并落在实处。

总结

在这一讲中,我们回顾了CI诞生的历史和CI试图解决的根本问题。同时,我们也介绍了CI落地建设的三个阶段和其中的核心理念,即快速集成、质量内建和文化建立。

最后,我特别想再提一点,很多人经常会把工具和实践混为一谈,一旦结果没有达到预期,就会质疑实践是否靠谱,工具是否好用,很容易陷入工具决定论的怪圈。实际上,CI的核心理念从未有过什么改变,但工具却一直在升级换代。工具是实践的载体,实践是工具的根基,单纯的工具建设仅仅是千里之行的一小步,这一点,我们必须要明白。

思考题

可以说,一个良好的CI体现了整个研发团队方方面面的能力,那么,你对企业内部实践CI都有哪些问题和心得呢?

欢迎在留言区写下你的思考和答案,我们一起讨论,共同学习进步。如果你觉得这篇文章对你有所帮助,欢迎你把文章分享给你的朋友。

参考资料

https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/DevOps%e5%ae%9e%e6%88%98%e7%ac%94%e8%ae%b0/12%20%e6%8c%81%e7%bb%ad%e9%9b%86%e6%88%90%ef%bc%9a%e4%bd%a0%e8%af%b4%e7%9a%84CI%e5%92%8c%e6%88%91%e8%af%b4%e7%9a%84CI%e6%98%af%e4%b8%80%e5%9b%9e%e4%ba%8b%e5%90%97%ef%bc%9f.md