{T}

一人一套测试环境:测试环境何时能释放出来使用?

本文我们开始讲第十六次架构经历:一人一套测试环境。这个经历还蛮特别的,因为网上很少有人讲解这样的方案。

业务场景(架构经历十六)

当时,我们公司的基础设施使用的是虚拟机,且还未迁移到容器。

我们一共搭建了 3 套测试环境。之所以搭建了 3 套之多而不是只有 1 套,主要是考虑到多个并行项目同时进行时,需要实现分开测试和分开上线。而 3 套测试环境在一定程度上可以避免这些并行项目因为排队导致延期的情况。

一般来说,研发流程是这样的:需求宣讲 ——> 接口/方案设计 ——> 功能开发 ——> 联调 ——> 测试 ——> 预生产 ——> 上线。

在这 3 套测试环境中,有 1 套专门用于联调,另外 2 套专门用于测试。

那么,1 套联调测试环境够用吗? 答案是不太够,因为我们经常需要排期使用。那么 2 套测试环境够用吗?还是不够。这里,我讲一个具体的例子你就明白了。

之前我们有一个项目已经上了测试环境,功能测试反馈没问题后就等第三方验收了,可是第三方的验收拖了很久,以至于我们不得不继续占有这套测试环境。

之后,又有一个小的迭代项目要求下周四上线,并且还有一个上百人做的超大项目刚进入测试阶段,这就需要 2 套测试环境。此时测试环境立马不够用了,而且联调环境都被征用了。

然后,业务方还提了一个加急需求要求本周四上线,于是出现了下面这段对话。

“我们有个紧急需求这周四要求上线,你们能不能把测试 1 让一下?”

“不行啊,我们这个功能需要测试一周,下周四就要上线了。如果让给你们一天,我们就要延期一天上线了。”

“其实是 2 天……”

“那更不行。要不你问问 XX,他们正在做的项目周期长,应该能匀给你们两天。”

“不行吧,那个项目号称公司第一优先级,我开不了口啊。”

“不然你们就用测试 3?”

“我哪敢啊,那个验收项目是领导亲自跟的。”

“可是我们也不能延期啊,业务方都确认过很多次了,我们也跟合作伙伴谈好了。”

“……”

后面就是因为抢测试环境的问题,导致我们的紧急需求上不了线,有苦也没地方说。

在实际工作中,一个组同时开展好几个项目的情况经常发生,尤其是业务对接方比较多的小组。为此,我们决定好好解决这个问题。

解决思路

我们希望达成的目标是可以快速搭建一套新的测试环境,使用完立马销毁。

针对这个目标,我们的解决思路是这样的:

利用容器的特性,在几秒内快速启动了服务实例;

将测试环境需要搭建的服务通过容器实例部署起来;

将这些容器通过 Kubernetes 管理(编排)起来。

那么,这一整套测试环境都需要包含哪些服务器呢?请看下图所示内容。

图 1

以上就是每套测试环境中需要部署的组件。

决定使用容器灵活创建测试环境后,我们曾经对每一套容器环境是包含全部组件还是部分特定组件调研了很久。

使用过容器的开发人员都知道,在容器中部署 MQ、ZooKeeper、Redis 或配置中心是一件很简单的事情。比如使用容器部署 Redis,我们只需要输入如下 2 行命令就可以搞定。

$ docker pull redis

$ docker run --name a-redis-name -d redis

而使用容器部署 ZooKeeper、MQ 的方法也是类似。不过,这里有点不一样的是我们公司所有的中间件基本不是纯洁的开源版本。比如配置中心,我们并没有使用 Spring Cloud Config,也没有使用 Nacos,而是使用了一个完全自研的产品(包括 MQ 和网关都是自研),它既不支持容器,也不支持单机版。而 ZooKeeper、Redis 是基于开源版本,并在服务端加了一些封装。

此时,客户端强制我们使用一个自定义的客户端 SDK,且使用的中间件必须强绑定配置中心。

之前我们评估过,如果把这些中间件部署到容器中,将会出现如下 3 种情况。

中间件服务端改造成本大;

客户端的 SDK 需要进行大量的改造;

最重要的一点是会导致容器环境与其他普通环境存在很大的代码差异。因此,就算我们在容器中测试没问题,还需要在其他环境进行大量测试,此时容器测试环境就没有什么意义了。

为此,最终我们决定在容器测试环境中只部署独立的 API 服务或后端服务,其他组件直接重用测试环境的中间件,如下图所示:

图 2

基于以上设计方案,如果我们想快速部署一套独立的测试环境,一般需要解决哪些问题?因为我们的容器测试环境复用了测试环境的一些组件,所以需要解决如下 5 个问题。

1. API 服务间的隔离

如何确保容器环境的客户端请求到达容器的 API 服务?而非容器环境的客户端还是正常到达测试环境的 API 服务?

原来系统是这么设计的:每一个 API 服务中都会带一个配置项 channelID,然后客户端每次访问 API 时都需要带上一个 channelID 参数。网关层接收到这个请求后,会根据 channelID 将请求匹配到对应 channelID 的 API 服务中(当然 URI 也需要匹配),此时整个隔离过程就比较简单了。

先介绍一下具体的研发流程:每个项目都有一个 JIRA Issue,而 XXXX123 这个项目就是一个 JIRA Issue ID,我们会为每个项目单独起一套容器测试环境,于是这个 Issue ID 自然而然地被当作了环境标识。

再回到 API 的隔离,一般来说,客户端会把上面 channelID 放在配置文件中,等到容器测试时再打一个包,此包中 channelID 的配置值为 JIRA Issue ID 就是容器测试环境的标识。最后,我们会在容器环境打包 API 服务时,自动将 channelID 的配置值改为 JIRA Issue ID。

具体的调用请求处理过程如下图所示:

图 3

在图中,我们发现网关层接收到所有请求后,会根据不同的 channelID 将请求分发到不同的 API 服务中。这样,API 服务的隔离问题就解决了。

2. 后台服务间的隔离

如何确保容器环境部署的服务只调用容器服务?而测试环境虚拟机的服务只调用虚拟机服务?

原本系统是这样设计的:在打包 RPC 服务时,我们将一个环境变量 env 的值设置为容器测试环境的标识,也就是 JIRA Issue ID,比如 XXXX123。然后每个 RPC 服务注册 ZooKeeper 时,我们将在 service 的 meta data 中加一个 tag,并设置 tag 的值为 XXXX123。之后,RPC 服务只会调用同样 tag 的服务,什么意思呢?

比如测试环境中有 3 个 UserService,其中 1 个是测试环境的虚拟机,2 个是容器测试环境部署的 UserService,前者的 tag 为空,后两个容器 UserService 注册 ZooKeeper 后,它们的 tag 值分别为 XXXX123 和 XXXX245。另一个 OrderService 调用 UserService 时,如果 Order Service 也是 XXXX123 这个容器环境的服务,则它只会调用带 XXXX123 这个 tag 值的 UserService;如果它是正常虚拟机的服务,则只会调用不带 tag 值的 UserService。

这样,后台 RPC 服务间的隔离问题就搞定了。

这里,你会发现以上要点中我们并没有提及 ZooKeeper,因为 API 和 RPC 服务的隔离问题解决后,ZooKeeper 的数据隔离问题基本也解决了。其实,ZooKeeper 在每套测试环境中起的作用只是 API 服务和 RPC 服务的注册发现。

3. MQ 和 Redis 隔离

如何确保容器环境和虚拟机之间的 MQ 消息不互串、Redis 数据不互相影响。

本来我们想使用类似 tag 的概念解决这个问题,通过封装 MQ 与 Redis 的客户端代码,让他们只消费同样 env 环境变量值的服务生产的内容。

但是,我们还需要遵循这么一个原则:尽量减少容器测试环境与正式环境的代码差异。针对这个问题,我们讨论了很久,觉得没必要专门定制,只需保证走测试流程时使用不同的测试数据就可以了(本来不同的项目就会使用不同的测试数据,包括不同的用户、不同的订单等),这样基本不会再出现不同容器测试环境流转同样的 MQ 消息、缓存数据的情况了。

当然,Redis 中的一些通用数据还是会共同使用,比如城市之类的基础数据。不过,这些数据就算不同容器测试环境之间互相串联也不要紧。

4.配置中心数据的隔离

对于配置是这样设计的,如果容器测试环境的值与虚拟机测试环境的值不一样,我们不会修改配置中心的值,而是在容器环境的启动脚本中动态加上针对各自容器测试环境的环境变量,然后在业务代码中启动环境变量优先级高于配置中心的参数,这样就确保了容器测试环境的特殊配置,从而不影响配置中心的值。

5.数据库间的数据隔离

数据库互相影响的情况一般分为如下两种:

(1)测试数据互相影响

这点其实跟 MQ/Redis 的情况一样,我们只需要保证测试数据各自独立就可以了。

(2)数据库结构兼容问题

这个问题是这样的,比如同时进行 2 个项目,XXXX123 这个项目删除了 user 这张表的 updateFlag 字段,而 XXXX100 这个项目还需要使用这个字段。此时如果 2 个项目共用 1 个数据库就会互相影响。

其实,这点我们在 03 讲 中有谈过,每次版本迭代时,我们都需要保证数据库可以兼容前一个版本的代码。比如刚刚那个例子,我们不会直接在 XXXX123 中删掉 updateFlag 字段,而是等 XXXX123 上线了后再删掉。

关于数据库兼容前一个版本,我再举一个例子,比如我们在 XXXX123 这个项目中增加了 1 个字段,且 updateUserId 字段的值为必填,不然数据就会报错。而 XXXX100 这个项目并不会更新 updateUserId,这样如果 XXXX123 读到了 XXXX100 写入的数据就会报错。

这种情况该如何处理呢?此时我们可以在项目 XXXX123 中增加一些代码让它可以容错,即允许 updateUserId 为空。我们也可以将项目 XXXX123 与 项目 XXXX100 部署到不同测试环境的数据库中。

解决完上面这些问题后,基于现有测试环境快速部署多套容器环境的思路就没啥大问题了,接下来我们再简单介绍一下使用流程。

使用流程

我们的使用流程是这样的,每次新建一个工程时(新的 API 或者后台服务),我们都会在 Jenkins 上配置一个 Job,而这个 Job 需要接受如下 3 个参数。

Branch:需要部署的代码分支;

测试环境: test1/test2/test3(已经有 3 个测试环境,它决定了部署需要使用哪个测试环境的中间件);

容器测试环境标识: 也就是 JIRA Issue ID。

这个 Job 启动时,我们需要调用一个小工具,而这个小工具需要连接 Kubernetes 创建namespace(=JIRA Issue ID ),然后在 namespace 中增加一个 pod(pod 中运行的是专门为 JIRA Issue ID 打包的代码)。

在做某个项目时,比如 XXXX123 需要使用 UserAPI、UserService、OrderService、ProductService,我们会配置一个新的 Jenkins Job 联动 UserAPI、UserService、OrderService、ProductService 的 Job,并且将各个服务对应的 Branch、测试环境和 JIRA Issue ID 传入 Jenkins Job 中(这些值都通过 hard code 配置在新的 Jenkins Job 中)。之后,每次点击这个项目的 Jenkins Job,我们就可以对这个项目的容器测试环境进行部署了。

当然,如果项目成员想自己部署一套环境,此时只需单独配置一个新的 Jenkins Job,并找一个不一样的(比如开发任务的 Issue ID)容器测试环境标识就行了。

通过这套方案,我们就可以实现如下所示的效果了。

图 4

从此以后,我们再也不用在联调测试时求人了。

一人一套测试环境的方案成本其实非常小,因为代码改动很少,且 1 周半就可以把整个方案实施完成(时间主要花在申请服务器和部署 Kubernetes)。

此方案上线后,得到了使用者的一致好评,尤其是测试同学,这里我总结了三点原因:

再也不需要因为协调测试环境花很多时间沟通了;

一键就可以将相关服务部署起来,不像以前需要一个服务一个服务部署;

因为容器测试环境的搭建很简单,开发人员每完成一个功能,测试人员即可介入测试,而不需要等整个项目提测后再介入,大大缩短了提测后的测试周期。

总体来说,这个项目的效果非常棒,而且后面容器测试环境基本上保持人均一套的使用频率。

总结与预告

到这里,我的 16 次架构经历也就讲完了。接下来的结束语,我们不讲架构经历了,将通过 3 次真实的经历分享老板到底想要一个什么样的架构师。

如果你有更好的方案或者本文有什么疏漏的地方,

另外,如果你喜欢这些内容,欢迎分享给更多的好友哦。

为了不断提升课程服务质量,这里有一份课程改进的问卷,请你抽出几分钟的时间填写一下。同时,我们会根据内容反馈,挑选 5 名用户各赠送专栏 1 个。

问卷链接:https://wj.qq.com/s2/7871076/3f7f