我们是如何用 K8S 搞定1000个应用的测试环境

时间:2022-07-25
本文章向大家介绍我们是如何用 K8S 搞定1000个应用的测试环境,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

到今天为止,信也科技的研发团队已经全部使用星云系统作为开发测试环境了。星云是一套基于 K8S 的环境管理系统,它主要用于管理多个平行的测试环境。它维护这些测试环境,使它们可以稳定工作。星云系统所体现出来的简单、高效已经被大家所认可。我想可以把关于它的开发经历写一写,作为分享。

问题与现状

差不多是半年以前,基础框架团队接到了一个任务:环境治理。究其原因是开发团队被测试环境的问题困扰,影响了研发效率和进度,急需一套解决方案。

让我们看看当时的情景,信也科技是一家有着10多年历史的金融互联网公司,业务产品非常多,应用数目累计1000+ 的样子,整个系统可以说是非常庞大。有同事形象地形容这套系统,这是一条从新疆到上海的铁路,用户在新疆上车,途径了10来个省(团队)的地盘,最后在上海下车。

可想而知,这条路上无论那个环节出了问题,整条路可能就不通了。当时公司内部有3套比较完整的大环境,分别是 FAT、UAT、PRE。不过 UAT 与 PRE 都不是开发直接使用,所有的开发测试都在使用 FAT 环境,后果就是冲突非常严重,新功能测试上线,旧 Bug 修复,日常组件故障都时时困扰着大家。可以说那个时候 通讯基本靠吼,如果出了问题,有时候必须不同组件、不同团队排查问题,非常影响效率。

核心问题

现状知道了,核心问题是什么呢?通过分析,我们认为核心问题有2个。

  1. 稳定的系统需要稳定的组件。只有一套FAT环境,大家都在上面做变更,这造成了整套系统的某些模块没有充分测试,那么模块的不稳定,造成了整个系统的不稳定。
  2. 高速发展的业务要求快速迭代组件,不停地进行组件更新。业务的需求多变,业务推进,Bug 修复,新需求要测试,如果没有变更,那么业务无法进行,所以必须去变更组件。

最终系统稳定不下来,想稳定就要组件稳定,但是业务高速发展,就要不断引入组件和新版本。这2者矛盾了。

问题的核心是什么?

资源冲突,只有一套完整环境,资源成为了瓶颈,如果我们多复制几套环境 问题不就解决了吗?

但是问题来了,复制几套环境,如何复制这个环境呢?复制的资源成本和维护成本怎么控制?我们希望复制维护的成本足够的小,其次,多套环境意味着需要不停地将各个环境的应用版本进行同步,否则会出现一个个环境版本老化,劣化,逐渐失去它存在的意义,最终大家又回到一个测试环境的情况。

所以我们最终是这样设计的。

小目标

根据核心问题的定义,我们的也随之定了2个小目标。

  1. 提供一个稳定的测试环境 保证整个测试环境的稳定。它会随着生产环境同步 保证它一直是线上最新的。
  2. 为每个团队提供多套私有的测试环境,每个团队在私有环境测试不会影响其他团队。

在实施目标之前,我们也已经有足够的技术储备和开发规范,首先是容器及 K8S 已经落地,应用已经基本完全容器化,容器的发布流程已经平台化,团队的应用配置也使用了 Apollo 这样的中心配置服务,MQ 等组件支持 Restful 接口,容器具有静态IP分配技术。这些技术条件是项目最终成功的技术基石。

通过努力,星云系统诞生了。利用它,我们操作 K8S 完成了环境的创建、管理、销毁,很好地达成预计的目标。(根据本公司的实际情况,大部分应用都是利用 Restful RPC 进行交互,利用域名进行寻址,一般情况下用 Nginx 进行消息转发,所有设计以此为前提。)

四大优点

  1. 快速地创建测试环境,
  2. 应用可在不同环境里复用。
  3. 零配置使用系统。
  4. 应用无需改造

这一切是如何做到的呢?

整体设计

我们如何利用 K8S 完成这一切的呢?整体组件及流程这样

我们创建了星云多环境管理平台,用户可以自助在上面创建测试环境,在创建指令发出后,星云平台会把指令转换成一批创建容器的指令,利用现有的容器发布平台进行容器创建(主要利用已有稳定服务)。现有容器发布平台会让 K8S 将指定的镜像,应用按照指定顺序启动,并进行相应的配置。

在一个环境里所有的应用都启动以后,一个测试环境就准备好了,这时候测试人员或者开发人员就可以利用这个独立的测试环境进行开发、测试了。每个环境都是个独立的环境,相互不影响。

在这里我们只是利用 K8S 管理容器,让 K8S 进行 Pod 的管理,并不涉及任何其他服务,对它的依赖降低到了最低。

架构及原理

星云的设计和实现 从用户的角度看大概是这样的。环境之间是完全隔绝的。

首先我们需要让每个环境正常的运行起来,相互之间不影响,如何实现这一切呢?

在这点上,我们其实参考的是 K8S 本身的架构,K8S 是如何让整个系统运行起来的?

在 K8S 内部有2个很重要的组件 一个是 CoreDNS,一个是 Ingress, 它们分别完成了内部寻址和对外交流这2个重要的功能。在星云里,我们也是借鉴了这一设计思想。

我们在每个新拉起的环境里都预先加入了2个重要的组件,一个是 Dns,另一个是流量网关(Nginx)。

它们为只为单独环境服务,是单个环境的核心。DNS 组件首先启动,它和星云多环境管理平台通讯,获取本测试环境内实例的数据,而网关就是普通的 nginx,它也会和星云管理平台通讯,获得相应的转发策略,按照转发策略工作。

当环境里的普通业务应用启动后,他们会首先从环境内的 DNS 组件里获取信息,这样他们就能进行相互定位,它们之间通过网关转发消息。当用户在外部需要利用这个环境进行测试的时候,测试将测试流量打到网关这个容器上。网关预设的转发策略将流量分发到环境内合适的容器上,让整个测试正常工作。

Dns实现

Dns 本身的实现其实也不复杂,下图简单的描述了 DNS 的原理,星云多环境管理平台会根据环境里的实例状况,把信息同步到每个环境的 Dns 组件里,同一个站点的记录有优先级的区分。这样就可以定制出我们想要的效果。

环境1里的应用访问 A.com 的时候,会得到 192.168.2.200 这个结果。访问 B.com 的时候会得到 192.168.2.101 这个结果,而访问 C.com 的时候 会得到 172.168.2.4 这个结果。

因为有优先级,有自动同步的数据,也可以用户自由设置,这样我们在星云中心是可以随意设计环境内部的 DNS。

总结成一句话,每个环境里的 DNS 里的记录是星云控制的,大部分是自动的,但是你也可以控制它。

网关实现

网关的实现的原理也类似,星云控制中心会根据不同的环境,计算出它的网关的规则是什么样子。星云把规则推送到网关服务器上。这样通过网关和 Dns 的配合,星云系统完成控制层面的操作,网关和 DNS 执行数据流层面的操作。

RPC 实现

在 DNS 和 网关完成以后,我们只是完成初步的环境搭建,还有很多细节需要优化,其中最关键的是 RPC 部分。

因为我们有1000+的实例,如果靠简单的拉起实例,那么一个环境就需要拉起1000个实例,10套环境就有1万+实例,首先资源上这是一个巨大的负载,其次维护这么多环境的维护成本会很高。所以我们需要让应用能在不同的环境复用。

正所谓一图胜千言,整个系统差不多就是下面这张图所描述的场景。

公共基础环境就是 Default 基础环境,它和生产保持一致,我们让它保持稳定。项目环境-1,项目环境-2,项目环境-3 则是星云利用 K8S 的容器管理技术生成的。用户在这里测试新的需求,修复 Bug,相互之间是不会影响的。也不会影响到 FAT 环境的稳定。

我们最终实现的效果大概这样。

带颜色的箭头代表了不同环境RPC消息流转。

RPC改造设计

首先我们参考了 Google 的论文:Dapper,大规模分布式系统的跟踪系统。大部分 APM (Application Performance Management)都以此为基础开发。我们基于这个设计做了一些扩展来达成我们的需求。

在 APM 中有一个概念叫 TraceID, 它可以唯一标识一次调用。它本身只是唯一,没有其他含义。它可以在一个请求和它的子请求中保持。在我们的系统里,我们也需要类似的能力,除了需要 TraceID 以外,还需要一个 EnvID,当请求和它的子请求在不同的系统里传输的时候,除了会带上唯一的 TraceID, 还会带上 EnvID。我们依靠 EnvID 来实现多个环境的流转。

在上图箭头代表了 RPC 消息,不同的颜色代表这个消息里含有的 Env-ID 是不同的。比如红色箭头代表这个消息含有的 env-ID 是 FAT1,绿色的箭头代表含有的 env-ID 是 FAT2,蓝色箭头代表含有 Env-ID 是 Default。

在此时我们需要解决3个问题:

  1. EnvID 怎么生成?它具体什么样子
  2. EnvID 在系统中如何传输?
  3. EnvID 在什么时候工作,它具体工作的原理是怎么样的?它为什么能解决问题?

为此我们用染色、透传、智能路由3个解决方案来解决这3个问题。下面就是具体的过程。

问题1:EnvID 的生成,它具体什么样子?

目前所有的系统,都是靠客户端来发起的,比如 H5、手机、App 都是客户端,它们用标准的 Http 的请求和后端系统进行交互,发起业务。客户端使用类似这样的请求和服务器进行交互。

我们唯一需要做的,就是在流量进入各个环境的时候,需要加一个标识,表明自己的来源。类似这样

我们用这个标识表明了消息来源于那个环境,这个过程我们称之认为是染色。它会在整个生命周期里一直存在,在它和它的子请求里不会变化。

染色方案的比较:关于如何染色有2个方案,一个我们称之为客户端染色,另一种为网关染色。

客户端染色

这种方案,需要改造客户端,让客户端发出的请求直接带上环境标识。这种方案的优点是标识可定制,操作性强。但缺点同样明显, 它会入侵前端业务,不灵活,H5、 Web等客户端可能不支持,如果环境标识的内容需要扩展,则所有客户端都需要重新改造。

网关染色

网关染色的方案灵活很多,它不需要改造客户端,对业务没有入侵,对 H5、Web 也都能支持,升级也很方便。所以我们选择了网关染色的方案。

问题2:EnvID 在系统中如何传输?

这个其实是一个技术债了,其本质就是获得 APM 技术里类似 PinPoint、 zipkin、 jaeger、 skywalking 透传信息的能力。

结合我们项目的本身特点,如果选用有代码入侵的方案,大量现有系统改造成本太高。只能采用无代码入侵的方案。最后我们实现了一个J ava Agent 实现了类似 Skywalking/pip 的能力。将 Env-ID 能够通过应用,在子请求中继续传播下去。

在下面的例子可以看到 应用1 在收到一个请求后,它的子请求上都带上了一样的Env-id

EnvID 在什么时候工作,它具体工作的原理是怎么样的?

虽然我们已经知道了 EnvID,如何产生,如何透传,如何传播,但是我们仍然搞不清楚,它怎么帮助我们完成多个环境应用复用呢?那么下面我们将详细解释EnvID 是如何工作的,我们称这个过程为智能路由

这个过程可以分为2种情况。在这里我们演示了 A 应用调用 B 应用,B 应用调用 C 应用,这样一次调用过程。

普通情况

这种情况很简单,这里的情况说明每个环境里都有不同的版本,应用之间调用不会跨环境,每个应用都只调用本环境内的其他应用。

真实的运行情况,其实是这样的。每次请求都是通过网关转发一下的,并没有直接连接的。

星云多环境平台控制了 Dns,也控制网关转发规则,星云多环境平台系统让所有应用将请求都发给网关,网关拿到请求,根据规则再转发出去。在这种情况下,似乎没有太大意义,直连也可以。好我们看下一种场景。

特殊情况

在这种场景下 我们将实现下面的效果

这里基本涵盖了所有的应用复用的所有情况。

在测试环境 FAT1 里,A 应用的版本1实例希望将请求发往 B 应用,但是 FAT1 环境里没有 B 应用的实例,所以请求被发往了 Default 环境的 B 应用的稳定版本实例,在 Default 环境的 B 应用的稳定版本实例处理后,B 应用的稳定版本实例希望将请求发往 C 应用,因为 FAT1 环境里也没有 C 应用,所以请求继续发往了 Default 环境的 C 应用的稳定版本。

在测试环境 FAT2 里,A 应用的版本2实例希望将请求发往 B 应用,但是 FAT2环境也没有 B 应用的实例,所以请求被发往了 Default 环境的 B 应用的稳定版本实例,在 Default 环境的 B 应用的稳定版本实例处理后,B 应用的稳定版本实例希望将请求发往 C 应用,因为 FAT-2 环境有 C 应用的实例. 所以 Default 环境的 B 应用的稳定版本实例将请求发往了测试环境 Fat2 里的 C 版本2。最终实现了消息在不同环境内的穿梭,也实现了我们先要的应用复用。

在 Default 环境里的消息 则和上面一样。究竟这是如何实现的呢?真实的流程大概如下。

每个环境的网关的规则是星云系统自动生成的,星云系统知道所有环境实例的状态,所以星云是这样写网关规则的,如果一个环境里有某个应用的实例,那么网关就把请求指向本环境实例,如果没有此实例,则把这个应用的目标指向 Default的网关。

这样在本例中,当测试环境的 FAT1,FAT2 网关收到一个发往B应用的请求时,因为本环境里没有B 应用,那么请求就直接被发往了 Default 网关。

Default 网关又是如何工作的呢?Default 网关是一台 Openresty 服务器,我们略微扩展了一下它的能力,它的工作流程如下:

  1. 它收到请求消息后,先解析请求,先找出Env-ID和目标域名,确定它是来自哪个环境的,它想去那个目标应用。
  2. 它会利用目标应用域名和Env-ID 去星云里查找这个实例信息.
  3. 如果星云告诉它 域名+Env-ID有实例存在,则把消息转发给 Env-ID环境的网关,如果域名+Env-ID实例不存在,就把消息转发给Default环境里对应的实例。

按照上面的规则,我们把上面的实现再看一遍。以测试环境 FAT2 为例:

  1. A应用的版本2,将请求发往B应用,请求首先被发往 FAT2 的网关
  2. Fat2 的网关收到消息,进行转发,因为 FAT2 环境里没有B应用的实例,按照预先设计,FAT2 环境的网关把请求转发到了 Default 网关
  3. Default 网关收到了这个消息,解析出 Env-ID:FAT2 和目标域名 B。
  4. Default 网关向星云查询:Env-ID 为 FAT2 域名为B的实例是否存在?星云答复没有实例
  5. Default 网关把实例发给了 Default 环境的B应用的稳定版本实例
  6. B应用的稳定版本实例处理完以后,继续请求C应用,B应用的稳定版本实例将请求发往 Default 网关
  7. Default 网关收到请求,解析出 Env-ID: Fat2 和目标域名C
  8. Default 网关向星云系统查询:Env-ID 为 FAT2 域名为C的实例是否存在?星云答复有实例
  9. Default 网关将请求转发至 FAT2 环境的网关
  10. Fat2 环境的网关收到请求,将消息转发至C应用的版本2实例上。

按照这个设计方案,我们在不改动应用的前提下实现了我们的目标。按照这套设计无论环境里有还是没有对应实例,消息都能按照我们设计的流程进行流转。这样可以帮助我们节省大量的实例。

RPC 改造成本

在项目启动之前,我也参考过很多公司经验,我认为其中有一项比较挑战的地方就是改造成本。不少大公司在 RPC 设计之初就已经考虑了染色和按标签路由的功能,他们在所有的应用在开发是已经注意了这个问题。而对于我们这个项目而言,存在大量历史系统,不具备这个条件,如果改造 RPC,我们需要把所有的项目都重新改造,打包,测试,时间成本和人力成本都太高。我们采用了目前这种成本较低的方案,把应用改造作为另一项长期方案来完成。最终做到快速、低成本上线。

如果在 RPC 这些方面基础措施做得好,那么实现这个目标会简单。此外还有入侵性较小的方案是采用 Service Mesh 的方式来实现,将网关的功能在 Sidecar 里实现, 但是消息透传仍然是需要解决的。

Redis优化

除了 RPC 以外, Redis 也是我们需要攻克的技术点。因为拉起了新环境,为避免数据冲突,是需要新的 Redis 实例。按照常规的做法,需要为各个环境里重新生成 Redis 实例。但是我们遇到了一些问题,因为 Redis 的使用方法很多,有的项目使用 IP+端口,有的采用域名,有的使用 Cachecloud,并不统一。长期地看规范使用 redis 是一个解决办法,但是如何解决现有问题呢?常见的办法是需要进行替换配置,这不可避免的会入侵到业务的配置。为减少这种入侵,我们采用了 Java Agent 的模式,拦截了应用调用 Redis 的接口,在 Redis 的 key 前面加了一个前缀,这种既做到了数据隔离,又做到节省资源。大概的原理这样

不同环境所有数据都被加上了一个前缀,做到了相互不干扰,这样一个 redis 实例就可以同时被多个环境共享了。(如果开发初期,规范里就预制了 redis 数据前缀配置的话,解决这个问题就会简单。)

前端优化

除了后端以外,我们的前端也都上到容器里,但是前端使用容器存在一个资源问题,大部分的前端站点都是编译后的静态文件,文件不大,资源消耗也很低,但是域名数量非常多,一套环境里经常有40+前端站点。按普通方案,我们需要启动40多个容器来完成。这样资源利用率较低,为了节省资源,我们将一个项目的全部静态资源最终打到一个镜像里。然后利用 dns 和网关配合,实现前端站点的代理,做到了一个应用同时服务几十个静态站点,不但资源节省了,站点管理起来也非常简单,灵活。

数据库

目前所有的环境都复用了一套数据库系统,究其原因还是因为数据库的规模太大,数据初始化,复用,数据合并上还有比较大的挑战。所以用户测试一般使用不同的区域、账号进行逻辑隔离,目前来看也是满足需要的。但是数据整理仍有很大的进步空间。

总结

依靠这些技术实施,我们创建了这套系统,可以在10分钟级别交付一个含有近百实例的测试环境,目前公司已经有上百套环境在同时运行,极大减轻了测试环境紧缺的问题。利用 K8S,我们灵活控制了所有的环境,这表明基于 K8S 的应用管理系统可以极大地提高研发效率,容器技术和容器编排这方面还有很大的潜力可以挖掘。愿我们的经验能给大家有所启迪。如有问题可以邮件(shenjiong@xinye.com 或 ppmsn2005@gmail.com)进行讨论。