高途
测试环境治理实践
+
蔡沈锦
01
前言
需求迭代在开发或者测试阶段经常碰到的问题中,测试环境不稳定与环境抢占或环境覆盖名列前茅。这里主要的矛盾点在以下几个方面:
多个迭代并行开发时,上游服务请求下游服务,当下游服务部署了不稳定代码时,上游服务调用失败导致测试流程被阻断,只能等下游服务恢复。或者上游调用的返参不符合预期时,QA就开始找RD查问题了,整个请求链路排查完之后发现是调用的下游服务部署的的代码不符合预期。
某个RD或QA把迭代1的代码部署到服务器后,被其他迭代的代码把环境冲掉了,然后就发现群里有人在相互“问候”了,“xxx,你又把我的代码冲掉了,我正测试着呢,快点给我换回来”。
当业务处于发展初期,并行迭代较少时,上面的问题可能还不明显。但当业务处于高速发展期时,并行的迭代越来越多,这类问题也会越发明显,严重的影响迭代上线效率。测试环境服务隔离与流量隔离就成了硬需求,此时产生了物理隔离与逻辑隔离两类方案:
物理隔离:创建多套测试环境,每套测试环境覆盖当前迭代调用链上的所有服务,每个服务机器独占,可通过修改机器环境变量与机器hosts文件,指定调用的下游服务实例。
逻辑隔离:即多泳道方式,不同于物理隔离,主泳道的存在可以让子泳道只部署当前迭代涉及修改的服务,从而减少资源的消耗。但随之而来的是需要去处理如实例打标,流量隔离,资源隔离等问题。
从两种方案对比上可以看到,对M个应用N套环境的场景,物理隔离需要M*N个常驻服务,多泳道逻辑隔离方案需要的服务数量是M + 当前迭代涉及的修改服务数。因而多泳道的方案在物理资源成本上会有极大的优势,在降本增效的大环境下,为在尽可能低的机器成本前提下实现测试环境隔离,我们选择了使用多泳道的方案来做高途的测试环境治理。
02
什么是多泳道
所谓多泳道,借鉴的是游泳比赛的泳道的概念,在游泳比赛中,处于某个泳道中的运动员只能在自己的泳道内行进,一旦进入到其他的泳道,则为犯规。抽象到软件设计中,我们将每一次的请求视为游泳运动员,请求的流量标识即为运动员编号,不同的流量标识决定了请求在路由时使用的泳道,而将请求流经的服务在逻辑上进行分组,从而划分出不同的泳道。
根据部署代码的稳定程度,我们将泳道进一步划分为两种类型:部署稳定代码的主泳道(在高途的代码分支模型中为部署release分支的代码,即此时进入了集成测试,为待上线状态)与部署相对不稳定代码的子泳道(在高途的代码分支模型中为feature分支的代码,此时处于RD自测或者QA测试阶段)。不同于游泳比赛中的运动员只能在自己的泳道中游泳,为减少部署次数并节省机器资源成本,当请求在进行下游的服务选路时,如果发现没有对应子泳道的服务实例,将会Fallback到主泳道的服务实例。从而在迭代流程中,只需要部署档次迭代关联的服务到指定子泳道即可。具体流量的走向参考下图:
图1:多泳道流量走向图
各泳道说明如下:
test泳道:即为主泳道,部署release分支的代码,提供稳定的功能,使用场景为QA对feature分支的代码测试完毕后将feature分支代码合并到release分支后进行集成测试。
test-xxx泳道:如test-beijing/test-wuhan,为子泳道,部署feature分支的代码,使用场景包括RD自测,联调,提测后QA测试。
local泳道:为RD本地起的服务,只有当主泳道没有对应实例时,才会将请求路由到本地起的服务。原则上本地服务不属于多泳道的范畴,local泳道的设计主要为了满足在开发场景中RD本地起服务进行调式的需求。
03
怎么实现多泳道
在多泳道实现方案上,需要解决如下问题:
请求打标与流量隔离:上游服务在调用下游时,可以通过哪些方式对某个请求指定流量标识?以及如何按照“根据请求的流量标识,优先选择流量标识指定泳道的实例,而主泳道的实例做fallback”的逻辑去选路,以及在微服务场景下,跨进程和服务内跨线程的流量标识的如何透传。▶对后端服务,基于注册中心(如eureka)做服务发现的场景,如何在选路时识别实例所属泳道?▶对前端服务,如何根据流量标识做Server实例的选择,从而获取不同泳道的静态资源文件?
资源隔离:资源这块我们主要针对两类:DB与MQ。不同泳道的实例,有些期望做消息隔离(只消费本泳道生产的消息),有些不期望做消息隔离。有些期望做db的隔离(只使用本泳道指定的db),有些期望可以和主泳道共用db,如何足够灵活的支持不同业务场景的个性化需求?
实例隔离:如何指定泳道发布以及如何对实例本身做泳道的标记。这里包括两个问题:
▶CD侧:抽象出来泳道如何与发布本身做实体映射,在容器化的大趋势下,多泳道如何与K8S的资源管理打通?高途的多泳道实践主要针对使用K8S做服务编排的场景。
▶服务打标:如何进行微服务的打标,对使用注册中心(如eureka)的场景,如何保证对服务打的标识能传递到注册中心。
3.1 请求打标与流量隔离
3.1.1 请求打标
所谓请求打标,即在用户发起请求时,需要告知其期望请求进入到哪个泳道,比如QA当前在test-beijing子泳道进行测试,那么QA在发起测试请求时,需要通过打标的方式标识此次请求期望进入到test-beijing这个泳道。
针对不同的业务场景,我们提供了不同类型的流量标识的指定方式,具体包含如下三类:
通过Header指定:通过请求指定header的方式来指定请求打到哪个泳道,这种是最通用的方式。
通过Cookie指定:对不方便使用header的场景,我们提供种cookie的方式来指定泳道。
通过QueryString指定:针对第三方回调的场景,通过在请求参数中指定流量标识。
3.1.2 流量隔离
请求打标之后,如何保证请求进入到指定的泳道,并且在请求的完整链路中,如何保证按照上面提到的流量走向进行,具体分为如下两类场景:
3.1.2.1 前端部分
对已容器化的前端服务,不同泳道的前端服务实例发布在同一K8S的不同Namespace中,在Ingress这一层,我们基于开源的APISIX做二开,实现基于特定的header/cookie做路由,从而实现在请求头中指定泳道名或者通过种特定cookie的方式,获取对应泳道的静态资源文件。
图2 前端流量隔离方案
工欲善其事必先利其器,前端是请求的发起侧,为方便QA测试或RD自测时指定泳道,对B端使用浏览器的场景,我们通过调研,最终选用了Chrome浏览器的ModHeader插件,可以通过插件快捷的指定前后端泳道。对C端业务,通过前端团队开发的一个小组件(gaotu-fe/cube),通过在页面上点击即可选择期望使用的泳道。
3.1.2.2 后端部分
不同于前端部分ingress后面就是对应的前端服务这类的简单的单层调用模式,后端在微服务场景下调用层级更深。一个业务请求,从Nginx发起,经由网关转发到API服务,后续可能会有多次的跨进程的RPC通信,而在每个进程中,又可能存在多次跨线程的业务处理。
针对后端服务较为复杂的场景,通常存在两类处理方式:
1. 对公司的所有第三方组件做二开,以处理流量标识的透传和RPC通信的选路问题,在所有组件二开完成之后,业务侧再来配合做业务改造。这类方式需要大量的人力,并且在后续维护和升级时都需要较高的成本。
2. 采用字节码增强方式,使用Java-Agent来对需要二开的第三方组件做字节码增强,从而完成选路与流量透传的逻辑,这类方式在人力和维护成本上存在较大的优势。
我们最终选型基于Skywalking Agent做二开,在原有APM功能基础上,通过自研的Traffic组件,实现流量标识的跨线程与跨进程自动透传,以及在RPC选路时实现上面设计的泳道规则,从而完成下游实例的选路。GAPM-AGENT的具体架构如下图,在原有APM能力基础上,通过自研的Traffic类组件,补全了流量隔离与MQ隔离的能力,同时Agent通过长轮询与管理端交互,实现Agent的各类配置的秒级生效。
图3 GAPM-AGNET架构图
3.2 资源隔离
资源隔离在公司的业务场景中主要分为DB(Mysql,Redis,HBase等)与MQ,对资源侧的处理,由于隔离与共享两类需求都存在,在经过评估后最终按如下思路处理:
在业务方做容器化改造时,将所有配置收拢到配置平台(公司使用的是开源的Apollo),从而所有的资源依赖通过Apollo下发。
对资源共享的业务场景,不需要做配置修改,所有泳道使用一个Apollo集群即可。对需要资源隔离的业务场景,通过新增对应泳道的Apollo集群,并在的新集群中指定需要隔离的MQ或者DB配置。与之配套,CD侧会在服务发布到不同泳道时,通过修改JVM启动参数指定使用的Apollo集群。
同时,针对业务方反馈的新增Apollo集群配置较为繁琐,我们也在后续的迭代中也相继推出了GAPM-AGENT自动创建MQ GroupId,配合GAPM-AGENT的MQ隔离能力与隔离配置实时下发能力,从而便捷的实现了服务的MQ隔离需求。
3.3 实例隔离
上面在提到实现流量隔离和资源隔离时,都需要在做实例隔离时配合做如实例打标以及指定JVM参数的事情。这块我们通过自研的轻舟发布系统做了这块的能力支持。
在发布能力这块,我们的目标是RD/QA可以快捷的创建和销毁所需要的泳道,并便捷的在子泳道中部署当前迭代涉及的服务。需要说明的是,由于公司当前主要服务都已经容器化,并且我们使用了不同云商(阿里云/腾讯云等)提供的K8S做容器的编排,对已经容器化的服务,我们在发布层面抽象出了服务组的概念,并基于K8S设计了服务组的实现。从而服务在使用轻舟平台进行发布时,通过选择服务组,即可实现将服务发布到指定的泳道,从而实现实例的隔离。
3.3.1 服务组
服务组是服务的原子发布单元,一个服务可使用一个或多个服务组。每个服务组会映射到一个泳道。一个服务组对应K8S的实现其包含如下属性:
Region:服务组所属的区域。
Zone:服务组所属的可用区。
K8S Namespace:服务组在指定区域和可用区的K8S集群中部署的Namespace。
如将服务发布到服务组test-beijing-BJ-Ali1,即表示此服务将会被发布到一个测试K8S集群的test-beijing这个Namespace,并且此K8S集群Region为BJ(Beijing),Zone为Ali1(阿里云)。从而,RD/QA在做服务发布时,需要将服务发布到哪个泳道,只需要指定对应的服务组即可。
3.3.2 JVM参数
对应公司后端Java项目使用Eureka作为服务注册中心,因而在服务启动时,为将服务的标识注册到注册中心,我们指定了如下启动参数:
-Deureka.instance.metadataMap.trafficEnv=test-beijing:指定实例所属泳道标识。
-Deureka.instance.metadataMap.regionId=BJ:指定实例所属Region标识。
-Deureka.instance.metadataMap.zoneId=Ali1:指定实例所属Zone标识。
从而,在GAPM-AGENT在做下游实例选路时,可根据这些标识,按如下优先级进行服务实例的选择:
同泳道同Region同Zone > 同泳道同Region异Zone > 同泳道异Region异Zone > 主泳道同Region同Zone > 主泳道同Region异Zone > 主泳道异Region异Zone > local服务
3.4 基于K8S实现的多泳道治理
在使用K8S做容器编排的场景下,如何利用容器编排的灵活性,更高效的进行测试环境的多版本治理,我们进行了如下探索。
3.4.1 跨泳道的服务copy与服务回收
基于K8S的编排的灵活性, 轻舟平台提供了如下能力:
一键将某个泳道的多个服务拉取到另一个泳道。
对某个主泳道服务不稳定的情况, 一键拉取线上使用的稳定镜像到主泳道。
对某个泳道中不需要的服务,提供一键回收能力。
3.4.2 泳道的自助化创建与全生命周期管理
为应对灵活多变的测试需求,我们将子泳道分为了如下两种类型:
常驻泳道:公司跨业务线联调常用的子泳道,我们创建了10多个子泳道,此类子泳道只做子泳道的服务回收,不做泳道回收。其中,服务回收按照子泳道的服务存活时长进行,对超出存活时长的服务做删除,但只删pod,保留Deployment,以方便快速的临时恢复。
临时泳道:某个业务线有临时需求,期望使用一个单独的子泳道,轻舟平台针对此类需求,提供了即创即用,用后即走的生命周期管理策略。QA通过轻舟平台云开发的泳道管理自助创建子泳道并绑定到对应服务后即可通过轻舟平台发布系统将对应服务分支发布到子泳道,在迭代周期结束后,轻舟平台会自动检测子泳道是否可回收,当可回收时即会删除泳道在K8S集群中对应的namespace,下次有新需求需要使用,重新创建泳道即可。
04
结语
本文着眼点于测试环境的多泳道建设与基于K8S的测试环境治理方案,高途基础架构组通过多泳道的建设,优雅且低成本的支持了多迭代需求的并行开发和并行测试:
即创即用,用后即走的泳道创建策略让RD与QA同学再也不用为了环境占用而发愁,不同业务团队可以更好地协同工作,开发和测试效率得到了极大提升。
轻舟平台GAPM提供的APM能力对测试流量提供了更好的可见性和跟踪性,QA同学通过请求的traceId即可查询请求在整个链路上的流转情况,异常请求是在哪里出现了异常,慢请求是哪个环节慢,从而放心大胆精确的提bug,并且不需要通过RD去定位问题也极大降低了对RD的打断次数。
相比物理隔离的方式,基于K8S的多泳道实践在机器成本上得到了成倍的降低,并且借助K8S的超卖机制,进一步降低了测试环境的机器成本。
以科技赋能产业
以创新引领发展
未来,我来!