移动测试开发 QA 玩转 K8S 系列(一):测试环境部署领域的改造和应用实践
引言
持续交付,研发效能,是近几年来热度较高的话题,而以流水线基础设施为中心的工具链体系,也成为实现持续、快速交付的必要性工程基建,成为 devops 时代下的必需品。
过去的经验告诉我们,环境部署作为靠前的环节,已经成为影响流水线稳定性和成功率的一大因素,而随着微服务、业务自身的不断发展,业务架构日趋复杂,要随时构建一套可用环境的难度也不断增长。
环境的可用性、可靠性、稳定性,能否第一时间按需调起,故障能否第一时间发现,能否一定程度自愈,能否最大化满足多人、多项目、多场景的使用,是我们亟需解决的问题。
而 K8S 也已被无数个实践证明在解决如上问题上具备天然的优势和完善的解决方案。
本次分享主要围绕 K8S 改造测试环境、测试服务部署来展开。
测试环境部署存在的问题:
测试环境的部署,总会被这样那样的问题所困扰,如:
▶多人共用环境的冲突、污染问题
▶多版本/多分支并行部署需求
▶多场景部署需求—不同部署规模;不同数据量;不同测试用途;公共环境、feature 环境交叉调用
▶拉起一套环境越来越难,但迭代速度却越来越快,环境部署和数量跟不上生成要求
▶希望环境尽可能稳定(节点故障、进程故障、oom 等)
▶出了故障希望尽早发现,甚至还能一定程度自愈
▶有限资源抢占下的排队问题
▶环境清理需求
▶...
以往,在流水线环境部署任务节点中,脚本做了太多细节设置,但仍然有一些问题得不到解决。
Now,这些问题都要交给专业领域 K8S,而流水线,则希望真正回归到调度流转职责。
Why K8S?
考虑 K8S,主要从 2 个方面:
(一)天然的解决方案
毫无疑问,K8S 已成为云原生生态下,容器编排领域的事实标准,其强大功能包括但不限于如下:
▶对 docker 的应用:快速构建隔离、标准化环境的能力
▶集群管理能力
▶容器编排能力
▶强大的调度策略
▶合理的资源分配及驱逐策略
▶负载均衡、服务发现
▶自动化运维能力:健康检查机制,故障自愈,水平扩展,日志收集,监控告警
▶...
(二)K8S 的设计美学—集简约&强大于一身
K8S 的强大众所周知,但提供强大能力的同时,能够把 “把简单留给用户” 才是最值得称赞的。
这得益于 K8S 的声明式 API 和控制器模式这 2 大设计理念。
具体来说:
第 1 点:K8S 中,各种常见的概念都被统一抽象为 api 对象,如 pod,service,甚至 node;
第 2 点:这些对象,具备统一的表示方式,yaml 文件;
第 3 点:用户可以通过一个或多个对象的 yaml 文件达到对应用部署期望状态的描述,这个期望状态包括应用容器之间以什么形式编排,资源配额多少,什么情况下认定容器内进程异常,如遇异常如何处理等
第 4 点:调用 apiserver 暴露的对象创建接口,K8S 负责创建对象,并将其调度到合适的节点上;
第 5 点:控制器负责实时监测对象的实际状态与用户定义的期望状态是否一致,如果不一致,则调整到一致。期望状态来自于用户的定义,而实际状态,来自于 k8s 组件收集的信息,以及监控系统中保存的应用监控数据,或者控制器主动收集来的信息。
上述内容可以通过下图左侧部分体现。
下图右侧部分,是一个简单的部署例子:
图中下半部分是一个 pod 对象的定义,它描述了这个 pod 内运行一个 nginx 容器,而控制器 deployment 则负责确保携带 app=nginx 标签的 pod 副本个数永远等于 2 个。如果大于 2,就会有旧的 pod 被删除,如果小于 2,就会有新的 pod 被创建。
做一个 K8S 用户,我们在做应用部署时,需要做的,就是按照 K8S 的语言描述应用的 “期望状态”,剩下的事情,就可以放心交给 K8S 去自动完成了:它会通过控制器模式确保这个系统里的应用状态与我们期望定义做比对,使得应用的状态最终并且始终跟在 yaml 文件里描述的期望定义完全一致。
面对庞杂的概念和组件,从何下手?--突破口:了解 k8s 的 “语言”
所以我们需要关注的,就是应用部署期望状态的描述需要用到哪些对象,以及具体怎么去描述。
但 K8S 概念繁多,如何下手,这也是初学者的一个难点。
怎么解决呢?
破局:化零为整,从一个部署需求的演进开始:
就像前面提到对象是用来描述应用部署状态的,那我们不妨以一个实际部署需求的例子,循序渐进地来将应用部署期望状态和这些对象之间做一个投射。
即先划分环境部署域的问题和需求,再基于这些问题和需求去选择投射合适的 K8S 组件。
首先,希望服务部署在容器中,这很简单,需要注意的就是是否遵守了容器的单进程模型;
实际上,这个应用可能包含多个进程或业务单元,需要根据实际情况进一步分析他们之间的关系:
①.对于在传统虚拟机/物理机上需要统计部署的进程,它们可能需要通过共享内存通信,也可能需要共享本机的文件目录等,对于这类关系比较紧密的进程,可以当作多个独立的容器放在同一个 pod 中;
②.而对于一些关系没有那么紧密的进程,服务,则可以根据需求部署在不同的 pod 中;
接下来,希望 K8S 能够确保应用的实际状态与期望状态一致,这就需要用到控制器,需要根据服务的类型做控制器对象的选型,简单说几个常见的:
①.对于无状态服务,希望保持多个副本,且这些副本之间完全对等没有先后关系,可以用 deployment,本案例后面的介绍也基本是基于这个控制器对象;
②.对于有状态服务,多个副本之间如果重启,重新调度,希望维持其原有的主从、主备关系,那就需要用 statefulset;
③.还有一些服务只需要运行一次,像离线脚本,只需要确保它按照我们的预期运行完成即可,可以用 job;
④.定时任务,cronjob;
⑤.集群管理中可能有一些守护进程的需求,比如在集群的每个节点上都部署日志收集或者监控组件,就可以用 daemonset;
⑥.等等
⑦.除了服务,还可能会有数据挂载需求,也需要结合实际常见来选择具体类型的 volume 对象。
如是否有特殊用途?
从生命周期来看,这个数据是跟 pod 同生命周期的临时存储?还是需要持久化?
是否需要持久化到宿主机?
如果 pod 被调度到其他节点,其他宿主机能否有现成的数据?
最后,服务部署完,希望能够对外提供稳定的访问接口,无论 pod 实例发生过重新上线,还是重新调度,都希望对外暴漏的 ip 不变,就可以选择服务发现和流量接入相关的对象。
以上是基于一个简单的部署需求,实际情况会比这复杂的多。
但通过这个例子可以看到,看似杂乱无章的对象概念,其实都可以根据它可以解决的问题投射到具体的问题领域,也就是我们说的化零为整。每一类对象负责解决部署领域的一大类问题,而每一个对象则负责当前领域的细分问题。
我们可以从这个视角去双向投射:
了解一个对象,先去了解它能做什么,解决的是部署领域的什么问题;
同样,有一个特殊的部署需求,也可以有目的的去查有哪些对象可以支持。
关于案例背景就介绍到这里,接下来进入本次分享的重点:如何使用 K8S 对测试环境、测试服务部署进行改造。
如何与流水线结合:期望效果 - 充分信任,极简交互
环境部署作为流水线其中一个环节节点,需要遵循流水线设计原则之一:高内聚、低耦合。
如流水线任务自身与周边服务(如 git 仓库,容器云/其他环境部 署工具服务、自动化框架、自动化平台等)低耦合。
具体来说,由周边服务负责做好功能内聚和可靠性的容错检查并暴露相应的接口,流水线只需要遵循交互最简化原则调用其暴露的接口并信任其结果,根据其结果做出后续调度流转。
之前的做法是通过流水线环境部署节点脚本来做细节控制,经由 K8S 改造后,低耦合得以实现。
拿环境部署任务节点来说,需要由环境部署服务自行完成部署以及环境的自校验,流水线 负责对环境服务提供的状态做相应的流转调度。这里我们由公司统一的容器云平台负责完成 服务的编排定义、资源配额的管理、数据的挂载、网络通信、以及服务的存活检查、容错性 检查及重启方案,流水线任务只负责通过有限的开放接口与容器云平台交互,部署细节,流 水线都不需要关心,更不应该将许多设置性东西放在流水线脚本中
总体来说,期望效果:遵循高内聚,低耦合规范,将专业的事交给专业的领域来做。而流水线只需要充分信任它的结果,并根据结果做出后续的调度流转。
用下图来表示:
可以看到,通过这种方式,用户焦点变了(只需要把精力聚焦在应用部署的期望定义,然后在流水线中通过有限接口与容器云平台进行交互,进而根据容器云平台返回的结果做后续调度流转)。
实践案例
接下来以某业务实际部署场景及解决为例来说明。
实践 1.1 一个最简单的单模块部署场景
场景特点:
①.单模块:这是一个 C++ 服务,由 binary + conf + data 构成,通过 rpc 与下游交互;(个别带 kafka 等中间件)
②.data:data 比较大,约 300G+,每日从生产环境同步
部署要求:
①.尽量缩小镜像体积,减少 build/push/pull 时间和网络消耗,同时降 低失败概率;
②.考虑多人、多版本部署的复用性;
③.有健康检查;
④.模块部署后,希望能在集群内/外都能访问到;
⑤.考虑故障重启/重新调度时的必要数据恢复问题;
⑥.考虑故障重启/重新调度后的 ip 变化问题;
⑦.出问题日志可追查;
通过以上期望的分析和设定,可以明确的是:
①.整个模块运行在一个 pod,并通过 deployment 控制器对象管理;
②.Pod 需要绑定一个 service 对外提供稳定服务;
③.健康检查:可以通过设置就绪、存活探针来实现;
④.日志收集:也可以利用平台提供的能力,选择 sidecar 或者 daemonset 来完成;
利用 K8S 进行应用部署的定义可以参考下图:
这里主要针对这个场景解释一下容器编排、镜像分解、数据挂载这几个方面。
①.首先,为了缩小镜像体积。已经将编译和部署环节做了解耦;另外,对于部署镜像,仍然可以通过进一步分析,再解耦出相对固定和经常变化的部分。通过这样拆分,可以使得重新构建镜像时只针对常变的这一部分。在这个例子里,部署环境是不常变的,而部署包则是经常变化的。将他们分别做成两个镜像,代码发生变更,只有部署包镜像需要重新制作;
②.容器编排上,可以看到除了模块运行容器,还做了一个初始化容器,这个初始化容器的镜像就是前面提到的部署包镜像。初始化容器会先于其他容器启动,作用是负责给运行容器注入模块部署包。为什么要这么做?一方面是因为前面的镜像解耦,运行镜像里面没有包含部署包;另一方面,通过这样,可以确保不管 pod 怎么重启、调度,部署包都可以在容器启动时被自动注入进去。而如果通过挂载的方式,就必须保证宿主机上有对应的目录和文件。这样,pod 会先启动初始化容器,这个容器将部署包挂载到 pod 声明的卷,当模块运行容器启动时也挂载同一个卷,这样相对于在运行容器的某个目录下可以直接看到部署包,进而解压,启动即可。
③.再说数据挂载。这里的数据分成两种,分别对应不同的需求。
对于部署包来说,每次 pod 重建随初始化容器注入到运行容器即可,不需要做持久化存储,所以选择了 emptydir 这种方式。
而对于模块运行需要的大容量数据文件,因为太大重建困难所以必须要持久化,但显然不适合放在宿主机做持久化,因为 pod 一旦重新调度,新机器上不确保一定有。所以选择了远程持久化卷,这样还可以在多人、多 pod、多次共享。
实践 1.2 一个最简单的单模块部署场景:结合实际使用场景的衍生
结合实际情况,单模块部署可能又会衍生出多种场景,而我们需要考虑到多场景下的复用性,找出变与不变的部分。
实际会出现多版本并行测试,多人测试,多测试手段,这些都会导致部署的期望状态产生一些差异。
而通过进一步分析可以看到,这个差异都可以在 pod 对象的定义中找到控制点。
举例来说:
多版本测试时,不同版本之间的差异在于代码,进一步说,会产生不同的部署包,进而产生不同的部署包镜像,体现在 pod 定义里就是镜像 tag 变了。
我们可以复用同一个模版,针对镜像的 tag 实际运行时做一个替换。
再比如说,不同的测试用途,可能会有数据量方面的差异,对资源的占用也不同。而可以根据实际需求对模版中的容器 resources 字段做控制替换。
回归到流水线里面再看。
我们在拉取代码时,同时拉取了模块的 deployment 模版,运行时根据实际场景做响应字段的替换,替换值的来源一方面来自流水线参数,比如一些 name 字段,容器的资源限额等,另一方面可以在流水线运行过程中通过计算得到,比如镜像 tag。前面的构建部署包镜像时,会依据一定的规则计算得到镜像 tag,被放入环境变量,这里就可以获取到去替换。
最终替换后的模版拿去发布,得到一个新的部署实例。
通过这种方式,可以做到一个模版,通过运行时的控制满足多场景多样化的需求,从而提升了复用性。
实践 1.3 单模块部署 - 进阶场景(单模块 diff 测试)
接下来再看一个进阶场景。
场景特点:
第 1 点:这是一个有下游的业务模块;
第 2 点:考虑 diff 场景的丰富性,需要从生产环境引脱敏后的流量;
第 3 点:因为要做单模块测试,也是节省资源的角度,这里需要有一个 mockserver 代替被测模块的下游 C;
第 4 点:为了保证测试结果的有效性,mockserver 必须能像下游模块 C 那样基于当前的请求,给被测模块真实的响应;
第 5 点:为了解耦下游对基准环境、被测环境自身的影响,对于同一次请求,mockserver 需要给 2 个环境相同的响应;
第 6 点:使用 mockserver 强返回可能会掩盖 B 的处理逻辑缺陷,所以 diff 除了针对被测模块的响应,还需要比对被测模块给下游模块发出的请求;
第 7 点:也真是因为上面这一点,在流量录制时需要同时录下 A 发给 B 的请求,以及当时 C 给到 B 的响应,并将这个请求和响应做一个关联,后者给到 mockserver;
部署要求:
①.2 套环境:base + test
②.1 个 diff 工具
③.导流工具:tcpcopy + 自研流量插件 flowcopy
④.mockserver,充当下游服务,资源利用最小化
⑤.请求、响应流量的存储需求
流水线简易流转示意如下:
①.Base 环境部署流水线:
每日定时触发,拉取线上部署包 -> 构建镜像(base 环境部署包)-> k8s base deploy
②.词表同步流水线:
每日定时触发,同步线上大容量词表文件,创建 PVC
③.流量录制流水线:
每日定时触发,复制线上流量,同时录制配套的
下游响应,请求流量和响应分别创建 PVC
④.Diff 流水线:
代码更新触发,拉代码-> 编译打包 -> 构建镜像 (新版部署包)
-> k8s test deploy
-> k8s diff deploy -> 执行diff -> 比对,出报告
-> k8s mockserver deploy
实践 1.4 多模块/集成环境部署场景
场景特点:
①.联调环境:仅需少量数据,内存要求低
②.系统级仿真环境:流量、数据要无限接近生产环境,单模块需 200G+ 内存
③.公共环境 + feature 环境的组合:满足局部联调需求
部署要求:
①.小集群:满足日常联调测试
②.大集群:满足类仿真系统测试
③.灵活组装的组合环境:满足局部联调需求
④.解决服务间网络通信问题,以及 pod 重启、重新调度后 ip 可能变化的问题
实践 1.5 测试服务部署实践
场景特点:
①.数据驱动,用例与框架分离;
②.抽象各业务的通用部分:框架升级,希望给多项目复用;
③.相比框架,用例变更频繁,希望能够减少重复构建镜像的次数;
④.测试过程中随时可能调试、增加用例,通过调试的可以直接提交;
部署要求:
①.自动化框架单独做成相对固定的镜像,不升级不需要重复构建;
②.业务差异部分:用例通过挂载的方式运行时注入;
小结
本文主要介绍在环境部署方面的改造实践,当然,K8S 部署生态下,如何做质量保障,哪些地方可能会出现问题,哪些场景可能导致测试结果失效,我们也积累了一些实践,后续文章有机会再一一展开分享。