持续集成:稳定性压倒一切

Posted by 肖哥shelwin on March 30, 2019

在《持续集成的初心》一文中,我讲到持续集成存在的目的是验证开发者(Developer)的代码改动是否能够合入代码主干(Master)。持续集成能否实现这一目的,关键在于持续集成是否足够稳定。稳定性是持续集成第一位的内在要求

什么是持续集成的稳定性?可以从两个角度理解。一方面,当没有代码改动时,持续集成是否在主干代码上能够稳定地成功?另一方面,当开发者提交了一个会破坏主干的代码改动时,持续集成是否能够稳定地失败从而给与对应开发者一致的反馈

当这两个问题都有肯定的答案时,持续集成才是稳定的。反之,则不稳定。试想,如果没有任何代码改动时,持续集成就一直失败(或是一会儿成功一会儿失败),那么当有改动改动并且对应持续集成失败时,怎么能认为失败是由这个代码改动造成的呢?

再试想,如果同样的代码改动,持续集成一开始失败了,然后在什么都不变的情况下重复执行一次又成功了,那么这个代码改动是有问题还是没有问题,是能够合入主干还是不能合入主干呢?

可见,一个不稳定的持续集成,不能有效地实现持续集成的目的。持续集成可以有一些其他的内在要求,例如更高的覆盖度(Coverage),更快的执行速度,等等。相比这些,稳定性无疑是最重要的。提高持续集成的稳定性,应当是持续集成工作的中心内容。那么,如何实现这一目标呢?

在《两阶段持续集成》一文中,我讲到持续集成是由两个相辅相成互不可缺的阶段,即代码合入前的Pre-merge CI和代码合入后的Post-merge CI组成的。一个稳定的持续集成,要求Pre-merge CI和Post-merge CI都是稳定的。我们注意到,Pre-merge CI是基于主干对代码改动进行验证的,因此主干(即Post-merge CI)的稳定性是基础

在项目初始时,一般主干上的代码数量是有限的,这时候的主干通常比较稳定。随着项目的推进,会有源源不断的代码改动合入主干。要保持主干的稳定性,就要严守主干的准入门槛。一切改动,无论大小,在合入主干之前,都需要经过Pre-merge CI的验证。没有通过Pre-merge CI验证的代码改动,是不能够合入主干的

这是一条原则性的规定,在任何时候都要坚守。在实践中,可能有来自开发者的各种各样的借口意图去跳过(SKIP)Pre-merge CI。例如:”这个代码改动非常紧急,没有时间去执行Pre-merge CI”,”目测我的代码改动不会造成Pre-merge CI失败,没有必要去执行Pre-merge CI”。

我们不相信主观的臆断,只相信客观的结果。除非从技术角度能够证明特定类型的代码改动不需要执行特定类型的验证(例如,回归测试集选择技术就能根据代码改动内容有选择性地执行一部分回归测试用例),并且这样的技术已经成为持续集成的一部分(由持续集成脚本自动执行),否则我们没有理由去跳过任何一个代码改动的Pre-merge CI。

“差之毫厘,谬以千里”。对软件的复杂性,我们要心存敬畏。一个极其微小的代码改动,可能造成整个系统无法运行。针对每一个代码改动进行Pre-merge CI验证,是一种负责任的行为,也是一种高效率的行为。

说它是高效的,是因为一旦这个代码改动有问题,那么主干就会被破坏(break)。这时不仅会造成持续集成无法工作(中断),而且修复持续集成需要花费更多的时间和人力。此时,往往还需要再提交新的代码改动才能解决持续集成的问题。从效率上看,这种情况无疑是低下的。

对于确实非常紧急的代码改动,我们可以使用优先级机制(例如,Jenkins的Priority Sorter插件)让它优先执行Pre-merge CI,而不是完全跳过Pre-merge CI。这样就能够在不牺牲质量的前提下,提高特定情况下持续集成的效率。

需要注意的是,无论如何严守Pre-merge CI的关卡,我们都无法保证Post-merge CI不会被破坏。正如我在《两阶段持续集成》一文中指出的,Per-merge CI是高度并行的,也就是说有许多代码改动同时被验证,并且不断地有通过了Pre-merge CI验证的代码改动合入主干。一旦主干发生变化(有新的代码改动合入),那么基于旧的主干进行验证的代码改动的验证结果从某种意义上来说就是“过期”了。

这种”过期“的风险在于,这个代码改动可能与主干上新合入的代码改动在功能上存在不兼容,即如果合并在一起将会造成持续集成中断。注意这里是功能上隐性的不兼容,而不是形式上显性的代码冲突(code conflict)。后者能够立即被版本控制工具(例如Git)检测到。

那么,有没有办法降低Post-merge CI被破坏的概率呢?一种做法是避免Pre-merge CI结果严重过期的代码改动合入主干。比如,当Pre-merge CI的结果超过了24小时,或者当Pre-merge CI触发之后已经有5个其他的代码改动合入主干时,我们可以认为Pre-merge CI的结果严重过期,并强制这个代码改动重新执行Pre-merge CI。

这样做虽然能够降低概率,但是无法把概率降低到0。将概率降低到0,意味着凡是主干有更新,所有基于旧主干的代码改动的都需要重新进行Pre-merge CI验证。并且一旦再次验证期间,主干又有更新了,那么还需要重复验证。极端情况下,有些代码改动可能陷入无限循环验证,迟迟无法合入主干。这样做从资源和效率角度是不可接受的。

由于我们无法保证基于旧的主干通过Pre-merge CI验证的代码改动,与新的主干仍然能够集成成功。因此,我们便需要Post-merge CI。尽管在绝大多数情况下,Pre-merge CI成功的代码改动,Post-merge CI也能成功。但是仍然时不时会出现Pre-merge CI成功但是Post-merge CI却失败的代码改动。

只要Post-merge CI被破坏,就应该以最高优先级处理。具体来说,我们可以立即中断持续集成,阻止任何新的代码改动合入,以避免情况变得更加复杂而失控。一旦定位到有问题的代码改动,需要立即执行回退(Revert)。需要注意的是,回退可能不是立马就能够完成,因为回退本质上也是一次代码改动,也需要通过了Pre-merge CI验证才能合入主干(顺便可以确认回退是否确实能够修复问题)。

修复主干是一个争分夺秒的过程。主干得不到修复,软件开发工作就无法恢复,软件交付工作也无法继续。为了加快修复,可能需要汇集持续集成,软件开发,软件测试等各个领域的力量来一起定位和解决问题。

一般来说,修复主干中断需要一定的时间。在紧锣密鼓修复主干的同时,还需要警惕“破窗效应” 。所谓破窗效应,指的是环境中的不良现象如果被放任存在,会诱使人们仿效,甚至变本加厉。具体来说,就是要防止有人以持续集成无法工作为理由,跳过持续集成合入更多的可能有问题的代码改动。

也就是说,持续集成出现问题,会诱使开发人员直接合入代码。一旦有人这么做了,那么下次会有更多人这么做。这样,“破罐子破摔”,最后持续集成长时间无法修复。因此,在持续集成中断期间,要顶住各种压力,严格禁止各种代码改动合入,除非这个代码改动就是来修复中断的。

根据海因法则,每一次严重事故的背后,必然有29次轻微事故和300起未遂先兆以及1000次事故隐患。对于持续集成中断这种严重事故,在问题解决之后,相关人士应该坐在一起进行根因分析(Root Cause Analysis, RCA),调查问题出现的原因,讨论改进的措施。

需要注意的是,完成RCA,只是改进的开始而不是结束。只有所有的改进措施落地了,并且从后续的观测数据来看,同类事故的发生频率确实有效地得到减少甚至完全消除,才是改进工作的完成

至此,我们讨论了什么是持续集成的稳定性,以及如何应对影响持续集成稳定性的主干中断问题。事实上,持续集成中的稳定性问题,既可能是必现(Permanent)问题(即主干中断),也可能是偶现(Occasional)问题。后者的危害性同样不容忽视。那么如何应对持续集成中的偶现问题呢?我们在后面的文章中探讨。

我是肖哥shelwin,一个高质量软件工程实践者和推动者。欢迎扫描下方二维码,添加我的个人公众号测试不将就,获得更多自动化测试, 持续集成, 软件工程实践, Python编程等领域原创文章。

公众号