背景
微服务框架下服务个数多、调用链路较长,其中一个服务出问题会影响到整条链路。但 QA 提测往往需要该条链路上的多个服务配套测试,甚至是同时测试一个服务的多个演进版本。
提供稳定环境 和 多服务/多版本同时测试 看似相悖的需求,但可以通过泳道(swimlane)能够得到解决。
测试过程中会遇到的问题:
- 关于一个服务上的多个需求的同时测试,存在服务抢占分支测试的问题。
- 不同的业务组在测试时依赖的第三方服务有改动或正在进行新需求测试影响本业务测试。
什么是泳道
对服务链按需求进行分组复制,并实现逻辑、物理的隔离,使得不同需求的服务链运行在相隔的物理机器上,逻辑上如同游泳场中的泳道。
一个环境内会有一条骨干链路(该环境的默认链路)和多条泳道。
如图所示,泳道隔离出了一条调用边界:处于[泳道-1]内的服务B要调用服务C,若在[泳道-1]内部署了C服务,则B只能调用泳道内的C服务,而不能调用骨干链路或其他泳道的C服务;若在[泳道-1]内没有部署C服务,则流量会调回骨干链路。
优势:
- 并行测试。(因此可以根据测试需要,部署不同分支的服务分组,多个泳道并行,多个服务/多个版本可同时提测)
- 提供稳定的骨干链路。(保证整个测试流程始终能正常运行)
- 错误隔离。(泳道内的服务发生异常 不会影响其他泳道)
泳道的特性
泳道相当于提供了多条“请求的跑道”,理解泳道主要在于理解“流量跑到哪去了”:
泳道内如果没有部署被调用服务,流量会 fallback 到骨干。
- 比如上图[泳道-2]中的 B 服务节点 调用了 [骨干链路]中的 C 服务节点。
泳道内若存在被调用节点,那么流量是一定不会 fallback 的 (包括不可用的和禁用的)
- 比如上图[泳道-2]中的A服务节点 只会调用 [泳道-2]中的 B 服务节点,即使[泳道-2]中的B不可用,也是不会 fallback 的。
骨干环境是一定不会调用到泳道内的。
- 比如上图中绝逼不会有 从[骨干链路]到[泳道-2]的调用。
泳道之间是一定不会互相调用的。
- 比如上图中绝逼不会有 [泳道-1]与[泳道-2]之间的调用。
泳道的实现
泳道实现的重点在于服务的注册、发现和服务导流。
后端服务的注册和发现的流程如下:
- 服务B启动,上报 ip、port、appkey、swimlane 等信息。
- 骨干链路上的服务 A 节点要调用 B,先去取B的服务列表,并进行过滤:A 不带有泳道标识,所以只会调用不带泳道标识的B服务节点。
- 泳道1上的服务 A 节点要调用 B,也会先去取B的服务列表,并进行过滤:A 带有泳道=泳道1 标识,所以只会调用同样带有泳道=泳道1 标识的 B 服务节点。
关于服务导流:
- 通过域名划分泳道:为各个泳道申请单独的域名,根据域名进行分流。
- 通过 header 携带泳道信息:请求的 header 字段增加“swimlane=xxxx”,标识请求要打到名为 xxxx 的泳道里,分流系统会根据该字段做分流。
前端静态资源,基于泳道名进行隔离,在资源编译和打包的时候,指定发布的泳道名,然后资源会上传到该泳道对应的静态服务器中:
1 | const swimName = process.env.SWIM_ENV; |
2 | if (swimName) { |
3 | console.log('检测到泳道环境SWIM_ENV:' + swimName); |
4 | console.log('默认使用test01执行发布'); |
5 | } |
6 | const webpackConfig = merge(baseWebpackConfig, { |
7 | devtool: config.build.productionSourceMap ? '#source-map' : false, |
8 | output: { |
9 | path: swimName ? config.build.assetsRoot + `/${swimName}` : config.build.assetsRoot, |
10 | publicPath: swimName ? config.build.assetsPublicPath + `/${swimName}/` : config.build.assetsPublicPath, |
11 | filename: 'js/[name].bundle.[chunkhash].js', |
12 | chunkFilename: 'js/[name].[chunkhash:5].chunk.js', |
13 | library: '[name]' |
14 | }, |
15 | }); |