持续集成:偶现问题应对策略

Posted by 肖哥shelwin on May 7, 2019

在《持续集成:稳定性压倒一切》一文中,我介绍了影响持续集成(Continuous Integration, CI,由Pre-merge CI和Post-merge CI两阶段组成)稳定性的必现问题的应对方法。需要注意的是,持续集成中的稳定性问题,既可能是必现(Permanent)问题,也可能是偶现(Occasional)问题。

所谓必现问题,就是重复执行能够必然(100%)复现(Reproduce)的问题;所谓偶现问题,就是重复执行只是偶然(具有一定的概率)复现的问题。

对于必现问题,一旦出现,意味着持续集成系统将完全无法工作,因而具有很大的危害性。对于偶现问题,虽然它的出现并不会导致持续集成系统停摆,然而其危害性同样是不容低估的,甚至从更长的时间窗口看,其危害性更大。

首先,偶现问题会掩盖必现问题,让必现问题变得难以被及时发现。在《持续集成:稳定性压倒一切》一文中,我们讲到一旦Post-merge CI失败,那么造成这个失败的代码改动就能立即被发现和回退。然而,如果Post-merge CI自身存在偶现问题,那么这时我们就无法快速判断这个失败是由偶现问题造成的,还是由新合入的代码改动造成的,从而错失发现破坏性代码改动的黄金机会

其次,偶现问题的存在,降低了持续集成结果的可信度。在持续集成中,开发者依据Pre-merge CI的执行结果来判断自己的代码改动是否满足合入主干的条件。如果在Pre-merge CI阶段存在偶现问题,那么当开发者提交代码遇到失败时,开发者无法确定是自己的代码改动有问题,还是持续集成中本身就存在某个偶现问题。这时,Pre-merge CI的执行结果将无法发挥作用,开发者也会陷入迷茫。

最后,偶现问题具有定位困难,生存周期长,分布广的特点。一般来说,一个软件问题能否快速定位,与它是否易于复现有很大关系。偶现问题是不能轻易复现的,因而调试和定位起来也就更困难。偶现问题从发生到被捕捉,从被捕捉到找到修改(Fix)方法,从修改方法被实现(Implemented)到修改方法被验证(Verified),都需要较长的时间,因而偶现问题的总体生存周期(从出现到被彻底解决)也就较长。另外,由于不断有新的代码合入,也就不断有新的偶现问题引入,因此偶现问题往往“按下葫芦浮起瓢”,在软件开发阶段广泛地分布着。

可见,持续集成中的偶现问题确实具有很大的危害性。那么,如何应对呢?认识问题,永远是解决问题的基础。对于偶现问题来说,我们需要花费功夫去认识它们。比如,系统中当前存在多少偶现问题?每一个偶现问题其出现频率是多少?每一个偶现问题一旦出现,其影响范围有多大?

要准确回答这些问题,我们需要有数据。创建并维护持续集成数据库(CI Database),是一项基础性的工作。持续集成系统每天均会产生大量的执行数据,我们需要将这些数据收集起来并进行持久化存储。这里的执行数据包括测试阶段(Pre-merge/Post-merge CI),测试用例,测试时间,测试结果,测试对象(对应代码改动的Commit ID),测试失败的错误提示等。一般使用自动化的脚本进行数据收集和存储工作。

下一步,我们便可以利用这个数据库,识别和提取出真正的偶现问题。这里要注意,并不是所有的失败结果都是偶现问题导致的。有两种情况下的失败记录能够被认为是偶现问题。一是在Post-merge CI阶段(即主干)中出现的偶现失败,二是在Pre-merge CI阶段,相同Commit ID触发的多次执行中出现的偶现失败

软件工程中有一句名言,“我们无法优化我们不能量化的东西”(We cannot improve what we cannot measure)。这句名言在处理持续集成偶现问题时仍然适用。基于提取的偶现问题数据,我们能够量化问题的偶现程度。例如可以用问题的出现概率(失败次数/总执行次数)来衡量一个问题的偶现程度。毕竟,执行10次就出现一次的问题,与执行1000次才出现一次的问题,其偶现程度是截然不同的。通过获取所有偶现问题的出现概率,并从高到低进行排序,我们可以有轻重缓急地去解决偶现问题。

通常来说,我们是按照偶现问题的出现概率来决定偶现问题的优先级,即出现概率越大,问题越需要优先解决。但是有些情况下,我们还需要结合偶现问题的影响程度来综合决定问题的优先级。例如,如果某个偶现问题一旦出现就会造成设备不可逆的损坏,那么即使它出现频率很低,我们仍然应该以高优先级去处理它。

监控偶现问题的出现频率是一个持续的过程,贯穿于软件开发的整个生命周期。针对偶现问题的修复(Fix)是否成功,应当以修复方法被实施后,后续观测到的偶现问题的出现概率是否大幅较少甚至降低到0为衡量标准。这就是数据驱动(Data driven)的工作方法

关于持续集成偶现问题,有一点需要认识到,那就是持续集成从原理上无法拦截偶现问题。持续集成是为发现代码改动问题而存在的,只有当代码改动会引入必现问题时,持续集成才能阻止这个代码改动合入主干或者发布到下一阶段。当代码改动会引入偶现问题时,持续集成既无法阻止这个代码改动合入主干,而无法阻止它发布到下一阶段。

基于持续集成这个与生俱来的性质(缺陷),我们在处理偶现问题时,可以展现一定的灵活性。既然持续集成存在这个特点,那么持续集成系统中不可避免就会存在或多或少的偶现问题。由于持续集成尤其是Pre-merge CI的执行频率很高,那么这些偶现问题就会经常出现在几十,几百甚至几千个开发者的代码改动上。这些开发者是否有意愿,有动力,有必要去花时间和精力分析一个与当前代码改动无关的问题?

在实际中,大多数开发者都不会去分析这种偶现问题,而是直接重新触发全部的Pre-merge CI 任务。基于这个现实情况,在Pre-merge CI阶段,当某个测试用例失败时,我们可以自动重新执行(重跑,rerun)这个测试用例。如果重跑成功了,则认为这是一个偶现失败,代码改动可以提交;如果重跑失败了,则认为这是一个必现问题,开发者需要着手分析解决(而不是去重新执行整个Pre-merge CI任务)。

注意到,自动重跑失败测试用例并不会牺牲Pre-merge CI的原则性,因为必现问题仍然会被拦截。在这个前提下,它提升了持续集成的有效性,节省了持续集成的资源消耗和开发者时间。当然,自动重跑并不是方案的全部,除此之外我们还应该记录每一次重跑的情况,尤其是在代码改动不变的情况下重跑成功的情况,并采用上述数据驱动的方法去逐一解决。

与偶现问题的斗争是一场持久战。由于持续集成无法拦截偶现问题,那么源源不断的代码合入,会引入越来越多的偶现问题。谷歌的研究数据表明,即使他们花费相当的精力去解决偶现问题,解决偶现问题的速度也仅仅与偶现问题增加的速度相当,也就是说,问题池里面的活跃偶现问题数量是稳定的。针对持续集成中的偶现问题,与自动化测试中的Flaky Test一样,我们既不能坐视不管,也不能试图毕其功于一役,这是一场艰苦的持久战。

持续集成是一个系统工程,既包含被测对象(开发者的代码改动),还包含测试用例,测试工具,持续集成脚本,持续集成基础设施等。持续集成中的问题,无论是必现问题还是偶现问题,都可能是代码改动之外的问题。”打铁还需自身硬“,持续集成自身的稳定性同样至关重要。那么,怎么打造高质量的持续集成系统呢?我们在后续文章再讨论。

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

公众号