最近经常听到有人在项目或社区谈论微服务架构,但讨论的重点更多的是微服务拆分、分布式架构、微服务门槛、DevOps配套等话题。但是在我眼里,真正能够称之为微服务架构的,少之又少。原因也很简单。我见过的大多数所谓的微服务架构项目,都达不到微服务架构的一个基本要求:服务的独立部署(交付)。这里的独立部署和自动化部署还不是一个概念。服务的自动化部署是比较简单的,目前已经有大量的工具可以帮助我们做到。但是这里说的独立部署,我觉得重点和难点不是“部署”,而是“独立”。如果失去独立部署(交付)服务的能力,微服务架构的能力将大大降低。虽然我们的系统在物理上被拆分成了多个小服务,但是从最终交付的角度来看,它还是作为一个整体存在的,就像一个单体应用一样,存在很多问题。为什么独立提供服务不容易?那为什么每个服务不能独立部署到生产环境呢?问题的答案是:不是不能,而是不敢!为了表达清楚,我们来看一个例子。如下图,我现在是帅气的程序员(真我扮演),突然有一天心血来潮开发了一个网上商城。代码推送到Github并通过CI构建持续交付流水线,最终自动部署到云端产品环境供用户访问使用。随着用户和访问量的增加,需求和功能也随之增加,系统也变得越来越复杂。从网上了解到最近很流行微服务架构,也赶上了潮流。当然,我也觉得这个架构确实可以帮助我解决目前的一些问题。分析完系统后,我将商城的后台部分拆分成了三个服务,为了简单起见,我们称之为ABC三服务。我们假设一种极端情况,三个服务相互调用(先不考虑这是否合理),每个服务都通过自己的持续交付流水线独立部署到生产环境。当前生产环境中各个服务的版本是:A:1.0,B:2.0,C:3.0,一切都很完美吧?看!我们实现了服务的独立部署!Soeasy~当然,事情肯定不会这么简单。问题出现在我对A服务进行新的提交时,A服务的***版本升级到了1.1。不幸的是,这个新版本不小心破坏了A和B之间的契约,错误地调用了B的接口,导致错误。虽然我的A服务和B服务都有比较完善的UT(单元测试),但是由于UT无法发现服务之间的集成是否被破坏,所以只有UT作为质量保证的A服务的持续交付流水线自然无法发现这个问题AB服务集成被破坏的地方。最后将导致问题的A1.1版本部署到生产环境,生产环境出现了严重的bug。请问在座的同学,遇到这种情况你们会怎么处理?“添加集成测试!”这位同学说的很好,我这么聪明,自然而然地想到这个,不就是为了测试集成吗?UT如果不行,就加个集成测试。为了统一语言,毕竟各种考试的名字太容易混淆了。参考《微服务测试策略》中MartinFowler的定义。在本文中,我们将这种多服务集成的测试称为端到端测试(end-to-endtest,简称E2E测试)。添加E2E测试后,我的交付管道如下所示。因为端到端测试的存在,问题就迎刃而解了。当新版本的A服务中断与B服务的集成时,端到端测试会及时诊断,阻止最新版本的A服务流向生产环境,保证产品环境不被破坏。这似乎没有问题。通过加入端到端的测试,解决了服务间集成的验证问题,但不知不觉中,我们也失去了微服务架构的那个重要特性:“服务的独立交付”。怎么说?别着急,我们往下看。假设在服务A修复过程中,服务B和C也提交了新代码。我们假设这两个提交都没有问题,但是因为服务A的1.1版本导致端到端测试挂掉的问题还没有修复,所以新版本的B和C也被端到端测试挂掉了。这个时候端到端的测试就像一个红灯路口,堵住了所有服务到生产环境的通道。所以,加上端到端集中测试,在保证质量的同时,我们的“微服务架构”却悄悄失去了自主交付服务的能力,杀敌一千自破八百,损失惨重!这不是我假设的场景,在我经历的几个真实项目中,这个问题一直困扰着我们。带来了各种衍生问题,比如E2E测试长期失败,无人修复,难以修复,服务交付拥堵,为了保持交付通道畅通,我们不得不引入CodeFrezze机制,提交Token还有很大的副作用机制等。可以看出,虽然我们可以在代码库、部署结构、甚至组织上对服务进行拆分,但是因为这最后的十里路口交付,***这个红绿灯,让所有的服务都被他们又纠结了,所有面向服务的拆分都是徒劳。最后,我们得到的只是一个看起来像微服务架构的单个应用程序。拆掉红绿灯,走自己的路,收复失地!那么,如何去掉这个“红绿灯”,让服务在保证质量的前提下独立交付呢?这就是本文要解决的问题,让我们继续往下看。我的解决方案其实很简单:InlineE2Etests。即不再为E2E测试增加一个新的集中式Pipeline,而是在每个服务的Pipeline中增加一个相同E2E测试的stage,相当于将E2E测试内联到每个服务各自的部署pipeline中,如图如下所示。事实上,InlineE2E测试并不是最关键的一点。最关键的变化点是假设A服务有一个新的提交。使用服务C的最新代码库版本进行集成验证,获取当前产品环境下服务B和C当前部署的版本进行集成验证。例如图中,服务A的版本已经从1.0升级到1.1,B和C在当前产品环境中的版本分别是2.0和3.0。在ServiceA的Pipeline上执行E2E测试时,验证A1.1和B2.0的集成有问题,测试变红,Pipeline挂掉,从而阻塞1.1版本的部署将ServiceA迁移到生产环境,保证生产环境不被A的1.1版本破坏。同样,假设在A固定之前,B也有新的提交,产生了新的B2.1版本。此时B服务Pipeline上的端到端测试,并不是获取当前A服务1.1的最新版本代码库做集成测试,而是获取产??品环境上的当前A1.0版本进行集成测试。我们假设B2.1和A1.0的集成没有问题,测试通过,所以B的2.1版本成功下发到生产环境,而A服务在生产环境的版本还是1.0.看!服务之间的阻塞已经神奇地解决了。这些服务将不再在统一的十字路口受阻,而是按照自己的方式行事。A的车道发生事故,是A的问题,应该由A承担,后果和问题的解决应该不会影响其他服务,仍然可以持续交付到生产环境。向前看是持续集成,向后看是持续交付!看到这里,有些朋友可能会觉得有些失望。时间长了,不就是把E2E测试集成到各个服务的pipeline中,然后把获取的版本从***代码改成产品环境吗?有什么了不起的。然而,在我看来,这个看似简单的变化意义重大:它揭示了“持续集成”和“持续交付”之间的重大区别。“持续集成”和“持续交付”,这两个概念相信大家都不陌生。它们在软件领域已经被提及多年,并不是什么新概念或新技术。但是对于这两个概念,我们经常一起提到,也经常混淆。我们不知道两者之间的区别是什么。我们可能会认为持续交付只是持续集成的一种进化,只不过是新瓶装旧酒而已。但实际上它们有着根本的不同。“持续集成”关注的是每个集成单元的最新版本的集成,即某个集成单元的最新版本是否破坏了系统的整体集成。我称这种观点为:“展望未来”。“持续交付”的重点不应该是一个集成单元的最新版本之间的集成问题,而是一个集成单元的最新版本是否可以(可以并且敢于)部署到生产环境中。换句话说,就是保持产品环境中的其他服务不变,只将当前集成单元的最新版本部署到产品环境中,看产品是否还可用,没有损坏。所以从“持续交付”的角度,我们要关注当前集成单元是否兼容生产环境中其他服务的版本。我称这种视角为:“向后看”。向前看是持续集成,向后看是持续交付。不看正反面就是裸奔。不过,肯定有同学心中疑惑了。把端到端的测试放在每一个为自己服务的Pipeline上靠谱吗?是不是太重了?根据测试金字塔,端到端测试应该属于接近金字塔顶端的测试类型。数量和覆盖面不宜过多。我们如何依靠它来保证服务之间的所有集成点和契约?主角出场——contracttesting同学们一定发现了,在上面最后一张图里,我悄悄把E2Etest改成了CT,也就是ContractTest,contracttest。契约测试也是近两年随着微服务架构的兴起而经常被提及的一种比较新的测试类型。在测试金字塔中,他的位置介于E2E和ComponentTests(可以理解为单个服务的API测试)之间。简单理解,契约测试是一种测试技术,可以使用类似于单元测试的技术来验证两个服务之间的集成。与下层单元测试相比,它的优势在于可以测试集成(两个服务之间)。与更高级别的端到端测试相比,其优势在于实现方式类似于单元测试,更轻量级,更易于运行。速度越快,覆盖范围自然会越广越细。将E2E测试替换为合约测试后,整个架构会变得更加复杂。目前有很多合约测试框架,比如经常提到的Pact或者SpringContracts。这里我以Pact为例进行说明。其他框架的实现可能有些差异,但是思路是一样的。A服务调用B服务的一个API,我们说A和B之间有一个契约,即B应该根据这个契约提供一??个满足契约要求的API,A也应该根据A的契约调用B这个API。在这个过程中,A是调用者,我们称之为Consumer端。B是callee,我们称之为Provider端。如果A和B都履行合约,按照合约定义调用和被调用,我们可以认为整合是没有问题的。但是无论是B擅自修改API毁约,还是A擅自修改调用API的方法毁约,都会毁约,在测试中体现契约测试会失败,如果反映在产品中,功能将被破坏。有一个错误。每一个合约,比如A->B,都会有Consumer端和Provider端生成的两个输出:分别是a-b.consumer.json.1.1(Consumer端生成的合约文件,所以版本也是Consumer端A版本号)和a-b.provider.jar.2.0(Provider端生成的合约验证测试包,由Provider端生成,所以版本为B的版本)。这个jar包其实就是一组测试。它的输入是a-b.consumer.json,它的输出是测试的结果,也就是合约的验证结果:成功或失败。服务A生产的合约文件a-b.consumer.json.1.1可以想象成一把钥匙,服务B在Provider端生产的测试a-b.provider.jar.2.0可以想象成一把锁。合约测试的执行过程就像试图用钥匙打开锁:如果能打开,我们认为合约A1.1->B2.0满足,否则合约被破坏。值得注意的是,合约测试不像E2E测试,它是有方向的,所以我们看到a-b和b-a是两个不同的合约。因此,只有验证了A1.1->B2.0和B2.0->A1.1的双向合约,才能认为A1.1版本和B2版本的集成没有问题。0。用契约测试代替端到端测试回到前面的例子,假设我们在ABC的三个服务中的两个服务之间建立了一个契约测试。此时服务A有新的提交,已经升级到1.1版本,那么如何通过合约测试验证A1.1版本是否可以交付到生产环境呢?答案是只要通过A的1.1版本的***代码,生成作为消费者端的A的所有合约文件(a-b.consumer.json.1.1和a-c.consumer.json.1.1),并使用将这两把“钥匙”尝试打开(执行Provider端测试作为输入)对应的两个产品环境放“锁”(a-b.provider.jar.2.0和a-c.provider.jar.3.0)。如果都可以打开(测试通过),则证明A的新1.1版本兼容生产环境中作为消费端的B和C的服务。等等,别着急,还没完……因为我们还需要考虑A作为Provider的情况,方法是通过***代码生成版本A作为Provider端的合约测试A的1.1版本(b-a.provider.jar.1.1和c-a.provider.jar.1.1),持有这两把“新锁”,然后尝试用两把“钥匙”(b-a.consumer.json.2.0和c-a.consumer.json.2.0)打开c-a.consumer.json3.0)在生产环境中。如果两者都可以开通(测试通过),则证明A的新1.1版本作为Provider端也可以兼容生产环境中B和C的服务。至此,在验证A的新版本1.1作为调用端和被调用端都满足产品环境中的其他服务契约后,我们认为A1.1与B2.0和C3.0集成没有问题,这意味着A1.1可以安全地部署到生产环境,取代当前的1.0版本。***、敲黑板划重点微服务架构下的独立部署(delivery)很重要,但往往容易被忽视,没有引起足够的重视。为了实现微服务的独立持续交付,我们需要“向后”看而不是“向前”,即关注当前变更服务与部署环境中其他服务的兼容性,而不是关注当前更改服务和其他服务***版本兼容性。用合同测试替代E2E测试,降低测试成本,提高测试覆盖率,尽早测试。并通过不断完善契约管理,保证微服务架构的质量,防止微服务架构腐败僵化。【本文为专栏作家《ThoughtWorks》原创稿件,微信公众号:Thinkworker,转载请联系原作者】点此查看该作者更多好文
