cover_image

猿辅导构建测试虚环境的实践

猿辅导技术
2021年07月30日 10:32

图片

  作者介绍

猿辅导基础架构团队,通过建设一流的工程基础设施,不断提升整个公司各个业务应用的稳定性与开发效率。目前的主要产品包括支持多云混布的私有云平台、监控报警平台、服务治理平台、消息队列中间件、大前端工程平台以及核心公共组件,为猿辅导、斑马、猿编程、小猿搜题、小猿口算等公司核心业务的顺利运转提供有力支撑。

    高玮: 从事service mesh等相关工作

    江杨雅迪 : 从事服务框架、虚环境、基础库等相关工作

    李亚斌:从事消息队列相关工作


  TL;DR

虚环境是基础架构团队通过流量染色和根据染色标签进行路由控制的一种环境隔离方案,实现了业务能够使用不同分支进行独立测试,大大提高了测试效率,缩减了测试成本,支持HTTP、RPC、MQ三种协议。


  背景和目的

我们在进行功能测试时,通常会有同一时间不同的人员需要测试不同分支的情况,但是因为测试环境只有一个,就需要协商排期,A分支测试完了才能给B分支测试; 还有一种情况是当新开发的功能涉及到多个服务进行联调时,但是一个服务中同一环境的实例间并没有区分度,导致辛辛苦苦构造的测试case调用到其他实例上了,没有路由到想要测试的实例上,因此我们必须先发布到正式的测试环境再进行测试,流程复杂,如果有问题还需要回滚,容易影响测试环境稳定性。

因此我们需要能够将多个服务或者同一服务不同的分支进行独立测试,做到互不影响。为此我们研发了一套能够进行分支独立测试的开发模型-虚(virtual)环境来解决这个问题,业界又称之为泳道、隔离环境等。


  研发流程变革

之前我们的研发流程是: 开发 → review → 合代码 → 测试环境 → 线上环境。

如下图所示,因为我们只有一个稳定的测试环境,想联调就得把没有测试的代码也部署到公共测试环境,带来的问题是当同时开发两个分支的时候,需要协商测试顺序,如果测试环境测出问题,则需要FIX并重新提交代码发布,会造成大量FIX commit和频繁回滚。协作效率低下,稳定性差。


图片


理想的研发流程应该如下,业务开发完毕后可以将不同分支单独部署进行测试,且各环境间的流量能够做到互不影响,等测试没问题后再merge代码并部署到公共的测试环境。


图片


我们通过虚环境来实现上述研发流程,虚环境字面意思是虚拟环境,是一个依附于基础环境衍生出来的弱隔离环境,与基础环境共享部分资源(有状态资源如DB等),流量通过逻辑隔离开,使得虚环境的流量可以在相应的虚环境链路里互相调用,而不会干扰其他环境。


  虚环境的优势

  1. 更贴合gerrit使用习惯,以change为单位部署,不需要合代码也不必要开分支,迅速自测/集成测试/功能测试

  2. 主干更稳定,保证每次合并到主干前都经过了自测和集成测试和功能测试,减少代码冲突/功能覆盖的出现

  3. 提升开发效率,不需要等测试环境,不需要和同时开发的同学商量,也屏蔽他人部署对自身测试的干扰,联调更便捷

  4. 变更更可控,每一次合到主干前都经过测试,可以立即上线持续部署,降低每次部署的变更范围,一次部署一个变更,提升部署信心

  5. 对于相同change-id,每次push代码自动部署生效,无需反复部署


  虚环境实现原理

概念解释

基线环境:基线环境是指基础的测试环境,每个服务必须有一个基线环境才能有虚环境。

虚环境: 依附于基础环境的弱隔离环境,与基线环境共享部分基础资源。虚环境必须依附于一个基础环境才有使用的意义,一个虚环境指拥有某个染色标签的全部染色实例,这些染色实例可以属于多个服务。

压测环境: 特殊的虚环境,使用场景为链路压测,用于染色的标签有压测标记,需要全链路染色,来强隔离压测染色流量,需要配置影子资源来隔离有状态资源

流量: 微服务系统内,服务和服务之间、服务和外部系统之间的流动的数据,主要包括HTTP、RPC、MQ。

流量染色: 给流量加上标签的过程,带有标签的流量称为有色流量,不带标签的流量称为普通流量,如果两个流量之间带有的标签不同,则这两个流量的颜色不同

实例染色: 给服务部署实例加上标签的过程,带有标签的实例称为染色实例,认为其处于虚环境; 不带标签的实例称为普通实例,认为其处于基线环境; 一个服务可以有多个不同标签的实例部署,不同服务可以部署带有同一个标签的实例;如果两个实例之间带有的标签不同,则这两个实例的颜色不同

强隔离和弱隔离

我们在设计虚环境方案时,可以采用强隔离或弱隔离的方式来实现流量隔离。

强隔离指应用所依赖的所有资源都是隔离的一套,可以理解为是两套完全不同的环境,彼此之间没有任何交集。


图片


如果我们将全链路上所有的服务都部署一套,并只在同一个环境内部路由流量,那么我们其实是能做到独立测试的,强隔离虽然能实现环境的隔离,但是其资源冗余,链路长的时候部署难度极高,想要部署一套虚环境,调用链上所有其他相关服务均需要部署一套虚环境。开发效率很差且测试资源成本浪费严重,正常情况下业务只关心少量的上下游进行联调,其他服务并没有变更,其实是可以复用的。

弱隔离是指只隔离流量,底层依赖的中间件、数据库、MQ等依然是同一套,其依赖的上下游服务也可以共享。通过逻辑隔离的方式实现,效果上也能实现业务逻辑隔离,但成本很低,只需要部署一次feature有相关变更的服务即可实现联调测试,如果只有自己的代码变更了,那么只需要部署自己的服务即可实现独立测试。


图片


如上图所示我们可以只把需要联调的服务进行部署虚环境,其他服务使用基线环境,上图中黑色线条所示,首先我们有一条从ServerA1 → ServerB1 → ServerC1 → ServerD1→ ServerE1的基线测试环境,然后我们有个新feature需要开发,如图中绿色线条所示,涉及到ServerB2和ServerD2的改造,当我们开发完毕后部署ServerB2和ServerD2的虚环境,因为其他服务并没有变更,我们无需部署其他服务的虚环境,我们的实际调用链路为ServerA1 → ServerB2 → ServerC1 → ServerD2 → ServerE1,这里的关键是流量从ServerB2路由到ServerC1后又能回到绿色的ServerD2,流量颜色不管去虚环境还是基线环境,染色标签一直在链路上传递,从而既实现了流量隔离测试,又无需部署其他非相关服务的虚环境,最终能够解决全隔离的协作效率和成本问题。

流量染色与路由

实现虚环境的核心思路是流量染色和路由

路由规则

首先我们会将RPC、HTTP、MQ的流量进行流量染色,在路由时我们根据颜色信息进行相应的路由。下面为路由规则:

  1. 带有普通标签的染色流量首先尝试路由到有同样标签的染色实例(通常情况下只有一个),无匹配时根据LB策略选择基线环境的一个实例

  2. 带有压测标签的染色流量过滤出同样标签的压测染色实例(通常情况下多于一个),无匹配时抛异常,标志调用失败,无可用压测染色实例

  3. 不带标签的普通流量过滤出不带标签的普通实例,根据LB策略选择其中一个

  4. 我们实现的路由规则是:如果有与流量颜色相匹配的虚环境,则路由到相应的虚环境,否则路由到基线环境,向下继续调用时如果发现依然有相匹配的虚环境,则路由回相应的染色虚环境。如图中的绿色线,因为流量为绿色,流量从ServerA1路由到了 ServerB2(绿色实例),因为ServerB调用ServerC时发现没有绿色 ServerC,因此调用到基线环境ServerC1,从ServerC到ServerD的调用链路中我们发现有绿色实例ServerD2,因此流量又路由到ServerD2,以此类推。

设定好路由规则后,我们需要解决的问题就只有一个问题,如何染色。

HTTP流量

HTTP的流量染色主要流程如下

图片


我们主要通过域名中的tag标签来进行流量染色。

URI规则为:$tagname–$subdomain.$cluster.$domain

其中:

  • tagname:染色流量标签名,与虚环境同名

  • subdomain:服务原始域名的3级域名

  • cluster:机房信息,目前只支持rz-venv

  • domain:服务原始域名的2级和1级域名

举例说明,原域名为:conan-test.zhenguanyu.com, 染色标签为mytest,机器属于rz机房,则对应的染色域名为:mytest--conan-test.rz-venv.zhenguanyu.com

通过染色域名进行http调用,我们会将颜色信息注入到HTTP Header中,在对请求进行处理时,我们从Header中的信息识别流量颜色并通过ThreadLocal在调用链路中传递颜色信息。

RPC流量

我们的RPC是基于thrift实现,在通过RPC调用下游服务时,我们会将流量的颜色信息注入到thrift Header中,传递给下游。虚环境和基线环境使用同一注册中心进行服务注册与发现,目标服务上报自己的实例颜色到注册中心在;在进行服务发现时,我们根据颜色标签以及设定的路由规则选择需要路由到的实例;当下游服务在接受到外部请求后RPC框架通过thrift Header中的信息识别流量颜色并通过ThreadLocal在调用链路中传递颜色信息,从而实现在链路上根据流量颜色路由RPC请求。


图片


MQ流量

mq相对于RPC和http的处理会比较复杂,mq有集群的概念、topic、group的概念,但mq的生产消费和服务名本身其实没有关系。我们要实现的消息路由规则为"如果有相应颜色的消费者,则路由给染色消费者,否则路由到基线环境消费者;同时如果消费处理逻辑里进行再生产,这个消息的颜色仍然需要能够传递下去",为了实现上述语义,我们需要解决以下几个问题:

  1. 我们使用的阿里云服务,无法改造服务端,如何让不同的消费者互相感知到。

  2. 消息的染色信息该放在哪里,能像http和rpc一样收到消息后直接解析颜色向下透传吗,如果不行以什么样的规则进行染色?

  3. mq服务端又不区分客户端颜色,怎么能保证虚环境的消费者只消费相应颜色的虚消息,又怎么能保证没有启动匹配的虚环境时被基线环境消费者接管呢?

下面是我们实现MQ虚环境的架构图

图片


我们实现的整体思路是每个虚环境使用带有虚环境标签的group来进行消费,每个环境消费一份全量消息,然后在消费端进行消息过滤,过滤出只属于自己环境需要消费的消息。这样我们无需改造服务端就能实现mq虚环境能力。

  • 针对问题一:因为我们无法改造服务端,因此我们在客户端启动消费者时将所有消费者注册到了注册中心,并且基线环境的消费者watch了这些信息,从而能够让基线环境感知到目前都有哪些虚环境的消费者。在注册时我们将clusterName, topicName, groupName 三要素拼接成一个serviceName进行注册,这个key能全局唯一确定一个订阅关系的消费者。

  • 针对问题二:如果是来自外部请求的流量,并在同一个线程进行消息发送,我们会解析出流量的颜色信息并将染色信息放到消息的properties中实现染色。如果是应用内部自发的流量,我们会将实例的环境变量注入到properties中,还有一种流量比较复杂,就是消费消息后进行再生产,针对这种消息其实有两种场景,一种是单条消费,这种流量我们在回调业务前将颜色信息注入到threadlocal中,业务在同一个线程中进行再生产的话,我们可以再次解析出流量颜色并将颜色注入到新消息的properties中,另外一种消费是批量消费,批量消费拉取下来的消息可能是不同颜色的消息,我们实现时将所有消息先根据颜色聚合,然后将统一颜色的批量消息对业务做一次回调,这样业务进行再生产时依然可以像处理单条消费的方式一样进行染色。

  • 针对问题三:我们在启动虚环境消费者时,在group后面自动拼接了虚环境标签,这样相当于虚环境和基线环境实际使用的是不同的group,因此每个环境均可以消费到一个topic的全量消息,剩下的工作就是进行消息过滤。我们实现了两种过滤器,在基线环境,我们实现了过滤规则是"如果是无色流量,则进行消费;如果是有色流量但当前环境中没有相应颜色的虚consumer存活,则进行消费; 否则丢弃该条消息", 在虚环境我们实现的过滤规则是“如果消息颜色和实例颜色匹配,则进行消费,否则丢弃该条消息”,通过基线环境和虚环境均消费全量消息配合不同的过滤规则,我们巧妙地实现了虚环境mq功能。

虚环境的部署

在虚环境部署环节,我们新增了自动化的部署以及自动配置ingress, 整个过程对用户透明,进一步降低了用户使用虚环境的复杂度。当用户创建完虚环境后,会自动进行发布,并联动相关系统创建资源,有效期3天,完后会自动销毁实例。


图片

  尚未解决的问题

  • 如果使用Java原生的线程池或Thread异步调用请求,则会丢失染色信息。未来我们会定制线程池来解决这类问题。


  加入我们

作为一群追求效率与卓越的工程师,我们的愿景是建设世界上一流的工程师团队。未来我们希望更近一步,在业界的前沿技术方向上持续探索,我们要面临的挑战有 Kubernetes 多云调度、边缘计算、高并发流量网关、统一观测分析平台、Service Mesh、混合消息队列、客户端 Hybrid 一体化开发框架、DevOps 一体化平台等。期待你的加入。


扫码查看详细岗位需求


图片



图片

▼点击阅读原文,查看详细岗位需求

继续滑动看下一个
猿辅导技术
向上滑动看下一个