持续集成:你不可不知的两阶段性质

Posted by 肖哥shelwin on February 23, 2019

在《持续集成的初心》一文中,我们讲到持续集成(Continuous Integration, CI)是由开发者提交代码改动所触发的,目的是验证代码改动是否能够合入(Merge)到主干(Master)的软件活动。

为了实现这一目的,持续集成一般包含两个阶段,那就是代码改动合入前的持续集成(Pre-merge CI)和代码改动合入后的持续集成(Post-merge CI)。

Pre-merge CI: 指的是代码改动从创建到合入主干这段时间的持续集成活动。集成对象是代码改动与当前主干最新代码合并之后的代码,目的是验证代码改动是否能够合入主干

Post-merge CI: 指的是代码改动合入主干之后到已更新的主干代码交付给下一个阶段(下游)的持续集成活动。集成对象是包含了代码改动的当前主干最新代码,目的是验证主干是否能够正常工作,并将集成通过的主干代码交付给下一个阶段

可见,Pre-merge CI的内容是集成(验证),而Post-merge CI的内容是集成(验证) + 交付。

经常有人产生这种疑问:既然代码改动在Pre-merge CI阶段已经通过验证,为什么在Post-merge CI阶段不直接交付呢?为什么还需要再次验证?难道不是多此一举吗?

这样做当然是有原因的。其出发点在于:同一个代码改动,在Pre-merge CI阶段验证通过,不代表在Post-merge CI阶段就一定能验证通过

Pre-merge CI和post-merge CI,虽然集成的对象都是代码改动 + 当前主干最新代码,但是却有着微妙差别

具体来说,只有一种情况下,Pre-merge CI和Post-merge CI的测试对象是完全相同的。那就是从这个代码改动触发Pre-merge CI到它合入主干的那段时间内,没有任何其他的改动合入主干。

这种可能性虽然存在但是概率较低,原因在于:Per-merge CI是高度并行的。也就是说,有许多代码改动同时在被验证,并且不断有通过了验证的代码改动合入主干。

一旦主干发生变化(有新的代码改动合入),那么基于旧的(在过去的某些时刻,是当时最新的)主干进行验证的代码改动的验证结果从某种意义上来说就是”过期“的。

这时,我们便需要Post-merge CI来进行二次验证,以确保基于旧的主干验证通过的代码改动合入之后,与最新的主干仍然能够成功集成。

或许有人会问,为什么不直接将基于旧的主干进行验证的代码改动的验证结果判定为无效,以阻止这些代码合入主干呢?这么做从技术上当然是可行的,但是却会大大降低持续集成效率。

倘若这么做,那么一旦主干有更新,所有基于旧主干进行集成的代码改动都需要再次集成,这将消耗大量的资源。并且如果再次集成期间,主干又有更新了,那么又要集成一次。极端情况下,一些代码改动可能陷入无限循环集成, 从而迟迟无法合入主干。

所以,普遍的做法还是通过Post-merge CI来进行再次验证。即先让这些代码改动合入主干,然后进行二次验证。如果某个代码改动在Post-merge CI阶段验证失败,那么这个改动就会立即被回退(Revert)。对于任何一个代码改动来说,有且仅有一次Post-merge CI就可以实现再次验证,从效率上看是较高的。

由于Post-merge CI是串行的(由合入动作所触发,从时间上是顺序的),因此我们能够逐个地对每一个合入的代码改动进行再次验证,而不会产生重复,遗漏或者冲突。

这里,我们解释了在Pre-merge CI存在的情况下,为什么还需要Post-merge CI。那么,在Post-merge CI存在的情况下,为什么还需要Pre-merge CI?

这是因为,尽管我们能够通过Post-merge CI来发现有问题的代码改动,并回退代码改动以修复被破坏的主干,但是我们并不希望这是常态。相反,我们希望把绝大多数集成问题发现和解决在Pre-merge CI阶段

要知道,在Per-merge CI阶段发现的集成问题,其影响范围和解决成本是远远小于在Post-merge CI阶段发现的。因为Pre-merge CI失败,只会影响范围影响某一个提交了对应代码改动的开发者;而Post-merge CI 失败,意味着主干被破坏,在那么所有基于这个已被破坏的主干所触发的Pre-merge CI,都将失败

说到这里,我们应该认识到了Pre-merge CI和Post-merge CI有着微妙差别的,并且都是不可或缺的。需要强调的是,Pre-merge CI和Post-merge CI的一致性应当远远超过其差异性

具体来说,同一个代码改动在进行Pre-merge CI验证(测试)与Post-merge CI验证(测试)时,除了测试对象所基于的主干代码的新旧不同,其他方面例如测试环境/测试形态/测试覆盖度等,都应该尽可能保持完全相同。为什么呢?

先看一种情况:Post-merge CI覆盖了的测试点,Pre-merge CI没有覆盖。这时,Pre-merge CI阶段无法拦住部分问题,将问题直接遗漏到Post-merge CI阶段,增加了Post-merge CI被破坏的概率,提高了发现和解决问题的成本。

再看另一种情况:Pre-merge CI覆盖了的测试点,Post-merge CI没有覆盖。这时Post-merge CI将无法拦住部分问题,将问题直接遗漏到下一阶段,造成更大的损失(背后的原因参见我之前的文章《谷歌测试定律的启示》)。

这两种情况都是我们应该竭力避免的。因此,我们应该尽量保持Pre-merge CI和Post-merge CI在各个方面的一致性。只有这样,我们才能:(1)在Pre-merge CI阶段, 尽可能发现更多的集成问题, (2) 在Post-merge CI 阶段,尽可能不把问题遗漏到下一阶段

讲到这里,我们可以看到Pre-merge CI和Post-merge CI的关系是相辅相成,充满辩证思想的。一方面,Post-merge CI的成功离不开Pre-merge CI的成功;另一方面,Pre-merge CI的稳定性又依赖于Post-merge CI的稳定性

我们知道,Pre-merge CI的测试对象的是主干代码 + 待验证的代码改动。它实现验证代码改动的目的的前提是主干代码足够稳定。这意味着,如果Post-merge CI不稳定,那么主干代码的质量就没有保证,Pre-merge CI就会不稳定。

稳定性是持续集成中压倒一切的事情。一个稳定的Post-merge CI,是一个稳定的Pre-merge CI的前提,也是一个稳定的CI的前提。怎么提高CI尤其是Post-merge CI的稳定性?这是CI的重要内容,是一个富有挑战性的问题。我们在后续文章中探讨。

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

公众号