前言

这篇帖子其实已经写好快两个月了,一直放在公司的 wiki 中做内部分享。 但因为今年的大会里我们组的超哥也专门去讲一些关于 k8s 和 CICD 的事情,内容很像所以一直没发到社区来。 所以既然大会已经结束那么今天就贴出来好了。 同时也很遗憾今年没能到场参加测试大会,感觉特别对不起大家,没帮上什么忙。我赶上了公司最忙的一段时间。 上周还发帖子说战斗已经告一段落,没想到从这周 3 开始又是每天半夜 3 点的节奏。

大规模持续集成

今天主要说一说 k8s 对 CI 能有怎样的支持。持续集成这个话题并不陌生,基本上大多数人都参与在其中。 我做面试官的时候,几乎绝大部分候选人都会说自己做过持续集成,实际上也差不多是这样。 找个虚拟机或者实体机,用 shell 写个部署环境的脚本,在 jenkins 上配置一下 pipeline,git 上一提交代码就触发 pipeline, 单测,编译,出包,部署,自动化测试一条龙就出来了。技术上并没什么门槛,属于懂点技术的都能做的范畴,更重要的可能还是在策略和思想上。大部分还没入门的同学感觉很神秘很高大上,大部分跨过这个门的同学感觉不过如此。 这也造就了一种情况,就是当我问一些候选人关于持续集成的问题的时候, 大部分会有两种反应,一种是觉得这玩意没什么好问的,说来说去就那么几个套路。 第二种是,口若悬河把持续集成吹的上天入地,实际一问其实他的场景特别简单。

持续集的原理确实属于说的烂大街的了。 属于业界很成熟的理念,算是大家的共识。 在很久以前它还是一个很高大上的概念,但自从 jenkins 普及了以后,实现难度已经到了一个很低的程度了。 我记得几年前的时候,我也属于觉得这玩意不过如此的。 但是我记得以前有个人跟我说过,你觉得简单只是因为你面对的场景还不够复杂。 是的, 如果你只是面对一个模块或者几个模块的持续集成, 面对一套测试环境甚至只面对一套测试环境的某几个模块,那确实是简单的。 但如果你面对的是一个有几十上百个模块 (每个模块都有独立的 repo) 组成的产品, 要维护数套甚至数十套测试环境,达到每日构建次数数以千计的时候。 你就不会觉得这玩意那么简单了。 这就是我曾经和现在正在面对的情况。 所以说在持续集成中,如果按技术难度分类的话,环境治理一定是 top 1。 当你面对如此之多的构建次数和测试环境的时候,当你面一套环境这么多个模块以及这么复杂的部署架构的时候。 想用单纯的 jenkins + shell 来搞定环境基本上就是痴人说梦了。 所以才有人说想玩微服务就要先搞定部署。

K8S 带来的环境治理

尤其是大家都知道我们公司是做机器学习的,为了能够保证机器学习平台的研发和测试效率,一个能够支撑大规模测试环境的基础设施是十分必要的。为什么这么说呢?

所以我们经过一段时间的讨论和实验,引入了 k8s+docker 来完成这个目标。首先 docker 它具有以下的优势

而 k8s 也有很多的优势:

那么 K8S 在实际的场景中能为我们带来什么呢? 我举几个场景吧。

横向扩展提升性能与容量

刚才我们说过面对大规模的持续集成与测试环境时。 面对的每日数以千计甚至数以万计的构建次数。上面还要运行着很多的测试环境。 所以一台机器搞这事铁定是没戏的。如果是以前,我们基本都是在 jenkins 上加入大量的 slave 节点并用 shell 脚本来维护。 但是当节点越来越多的时候,维护这些节点和环境的成本就越来越高了。 所以如果我们能让 k8s 来搞定这些事情,扩容与调度都交给 k8s 来做。 jenkins 只负责 pipline,这就解决了我们自己开发和维护这些节点程序的成本。k8s 的扩容是很简单的。并且它给你一个统一的接口,让你对所有节点和容器的操作只有一个入口。 不会像以前 shell 脚本那样满天飞的节点配置和逻辑判断。 而且 k8s 能够对自我管理这些节点,自动决定把容器调度到哪个合适的节点上,不必我们操心。

精准的按需调度

在一个复杂的场景中,我们不仅需要集群的调度系统能按照当前节点的资源使用情况进行调度,也就是尽量把任务分配到较为空闲的节点上进行负载均衡。 我们还需要更精准的按需调度。 K8S 中有一种策略叫 node selector, 我们通过给集群打上不同的 label 给节点分类,在提交任务的时候可以选择不同类别的节点进行运行。 比如我的数据库是需要存放数据的,而这些数据只存在那一个特定的节点上, 这样我们能通过这样的机制把数据库容器调度到固定的节点上。 当然我们还可以有更强大的调度。比如 GPU 资源再哪里都是稀缺的,我们并不希望大量的普通任务调度到拥有 GPU 的节点上,免得它们把资源占满后会导致真正有 GPU 需求的任务无法调度上来。 k8s 有一种策略叫污点,任务除非在特别指定的情况下,否则是无法调度到这个加了这个污点的 GPU 节点上的。 但这样也有问题,因为这个策略太排他了,实际上如果我的 GPU 任务没有那么多,站不满资源。 那这些剩下的资源还不能被其他任务调度,这个资源利用率是无法忍受的。 所以 k8s 也有第三种调度,叫亲和性和反亲和性。 这个有点复杂,但是它能达到一种效果。 就是它可以优先把普通任务调度到其他节点上,其他的节点不够用的时候,才把这些任务调度到 GPU 节点上。 这样能最大程度的保证其他任务不会影响 GPU 任务还能最大化增加资源率用率。

资源治理

上一点说按需调度的时候,也讲过资源利用率的问题。 我们面临大规模的测试环境的时候, 资源治理是一个非常重要的问题。 公司不是土豪, 我们的资源永远是有限的, 在有限的资源中支撑更多的环境是我们的目标。 我们总在计算着一个节点上能抗多少任务,什么类型的任务。服务起少了,资源利用率低,服务起多了,可能直接把节点撑爆了。 这是一个挺两难的事。所以 k8s 中有 quota 的概念,是一个非常重要的特性。首先启动 k8s 的时候可以设置为系统预留多少资源,这些资源是 k8s 不会使用的,专门留给 linux 系统,以免过多的使用撑爆集群。而且 k8s 把每一个资源都分为两种申请方式。 request 和 limit, request 代表着预留资源,也就是说如果我们为一个设置为 request memory 为 2G。 那么 k8s 就会为它预留 2 个 G 的资源,即便这个任务只用了 1 个 G,但其他任务也无法使用另外一个 G 的资源。 这种策略保证了服务绝对拥有启动服务的最小资源,因为只要申请了,系统就会为你预留这些资源使用。 但是这样的策略有个缺点,就是我们一般无法准确预估出一个服务会用到多少资源,也许他只用了申请资源的一半。 所以我们还有一个中申请资源叫 limit, 他跟 requeset 配合着使用,request 是系统预留的资源,是给任务独占的资源,即便任务根本是用不了这么多的资源,但他也会预留出这些资源给他使用,给一个任务设置过多的这种预留资源肯定是不利于资源利用率的, 但是预留资源少了,也可能造成其他任务抢占资源导致本任务无法运行。 所以 request 一般都是设置成需要运行任务的最小资源。 但是最小资源不代表着他永远都使用这么小的资源,在服务的高峰期的时候他会使用更多的资源。 所以我们有 limit 这种资源申请的方式, 如果说 request 设置的是资源的下限,是任务申请的最小资源, 而 limit 就是任务申请资源的上限,代表着不论如何,任务使用的资源都不可以超过此上限,超过了就会被 k8s kill 掉。确保一个任务不会超出它的预期占用过多的资源。 这样, request 和 limit 相互配合,就会产生更弹性的资源治理方式。 尤其由于他们这两种申请资源的方式存在,我们的系统才会拥有超卖的能力。 超卖是一个在集群管理中常见的词汇。 意思是一个服务本来申请了固定的资源保持平时的开销 (request 方式),但是在服务高峰期的时候,准许分配给他更多的资源,也就是超卖给他更多的资源来抗住高峰期的压力 (limit 方式)。这样就给了我们更加弹性的资源利用方式。

健康检查

之前说过 k8s 有自动化运维的能力, 其中一个重要的能力就是健康检查和故障恢复。 一般来说,在我们面对数量庞大的部署实例的时候, 最头疼的就是如果环境不稳定,比如节点故障,或者进程本身故障的时候,我们该如何维护这些部署实例。 在部署实例还比较少的时候这些都不是问题,我们人肉维护都是可以接受的。 但是当我们有微服务的时候,有多套测试环境的时候,数以百计的部署实例会让所有维护人员都疯掉。 尤其是有些服务的故障可能是很临时性的,比如仅仅是进程 oom 了,或者部署的当前节点发生了什么故障。 并不是进程本身的问题。可能只需要重启或者换个节点重新部署就能解决的问题。那这时候 k8s 的调度能力就起到了很大的作用。 首先 k8s 能够自动监控每个节点的健康状况, 如果 A 节点发生了故障,k8s 会监控到 A 节点是不健康的状态, 那么原来启动在 A 节点上的服务,都会自动的漂移到其他的可用节点上重新启动,来保证我们环境的可用性。 这是节点上的健康检查 。 同时我们也有用服务的健康检查, 在 k8s 中,没启动一个服务都可以为它配置相应的健康检查策略,包括资源的和进程的本身。当任务服务出现异常退出的时候,k8s 都会自动的检测到并在合适的节点上重启该服务,这其中不会有任何的人工操作。这种故障恢复能力,是在我们面对大规模测试环境中要拥有的重要能力。

k8s 还有很多有助于维护我们测试环境的特性,比如驱逐策略,负载均衡,弹性伸缩,init container 等等。这里就不多做介绍了。

下面介绍一下 k8s 最简单的玩法。

监控:官方的解决方案是 Heapster+grafana+InfluxDB。 可以在 github 上找到现成的镜像使用。我们一开始也是使用这种方式。后来我们慢慢演进到了使用 filebeat 来收集容器日志,灌入 es 中做更精细的日志监控的机制。最近我们已经迁移到另外的一套架构了,不过监控这块不是我负责的,我就不说了。

上面这些都是偏运维的东西,我就不再细讲下去了,

总之我们通过 docker+k8s 就有用了很强大的测试服务能力。 一般我们使用这种机制做一下 3 种事情。

这些是我们对于测试的基础 PAAS 平台的架构设计, 对于像机器学习这种复杂度如此之高的系统来说, 我们对这方面有着很高的要求。尤其是在后期我们的产品引入微服务的架构以后,它的复杂度到达了一种空前恐怖的程度。 所以这就需要我们的测试人员能够支撑住这样的基础设施。

CICD

一个高度工程化的团队离不开 CICD,但是面对机器学习平台和微服务架构,我们的 CICD 又有了不一样的难度。 之前说过我们在微服务架构下每日大量的构建次数对 CICD 的性能和自动化流程的要求是很高的。这里我们使用的是 k8s+bamboo+jenkins 的架构。 当然了,所有的构建都是基于 k8s 来做的,使用 k8s 的分布式调度来做横向扩展,解决环境对性能的要求。 利用 bamboo 和 jenkins 的 pipeline 衔接整个 CICD 自动化流程。整个的流程如图:

通过这样的 cicd 流程加上 k8s 强大的调度能力,我们支撑着每日千量级别的构建次数的同时,保证了最快的响应速度。 托了 k8s 的自动化运维能力,我们也能够用最少的人去维护这样一个庞大的系统。

机器学习平台下的自动化测试

我们说机器学习平台已经是一个产品了。 所以一个产品该有的东西都会有,我们测试人员关心的 UI 和 API 都会有。 该有的模块也都会有, 比如用户,权限,quota,项目,计划,监控,数据管理等等等。 所以除了机器学习特有的一些特性外,他跟我们普通的测试策略也比较像。 构建测试体系的时候,也符合金字塔原理。 但在这里我们的 CICD 流程中就有了
另一个挑战,那就是测试运行的速度。做机器学习的研发也好,测试也好。 都要面对一个无法避免的问题。 那就是任务运行很慢。 在大数据的情况下,一个模型要用几个小时甚至几天的情况也是不奇怪的。即便我们用一个很小的数据走通业务流程,一般也需要数分钟甚至数十分钟。 所以如果我们像以前一样,使用一个浏览器运行的话,在海量的 case 面前,我们的自动化测试会跑到天荒地老的。 所以在我们早期的时候就会使用多浏览器的方式来为我们的自动化测试加速。 我们使用的技术栈是 java+testng+selenide。 testng 的并发测试模式大家可以了解一下,调整并发的线程数我们就可以在一台机器上启动多个浏览器进行测试。

通过类似上面这样的配置,我们把测试分成了两个组并且并发的去测试他们。 这样在初期的时候还是可以接受的。 但是随着 case 慢慢的增长, 算法越来越多。 我们又出现了一个问题, 那就是单台机器已经无法支撑更多的浏览器了。我们说架构扩容有纵向扩展和横向扩展两种,纵向扩展是通过提高节点的硬件配置来提高性能,但是即便是再优秀的机器它的纵向扩展也是有极限的。 而横向扩展是通过增加节点的方式分担压力。那么目前我们面对的就是在纵向扩展遇到极限的时候,引入横向扩展的能力。 而 testng 本身是不支持跨越多台机器进行分布式执行的。 所以后来我们引入了 selenium gird。 这是一种基于 selenium 的分布式 UI 自动化测试解决方案。 它通过启动一个 grid hub 作为 master 节点,负责接收测试请求以及分发任务。 再通过启动多个 node 节点向 hub 进行注册。 这样 grid hub 会接收到我们的测试请求,并适当的把测试任务均匀的发送到各个 node 上去。 在测试结束后汇总结果返回给 testng。 它的架构是下面这个样子的。

通过这样的架构,我们的自动化测试就拥有了横向扩展的能力。 支持更多的浏览器帮助我们进行并发测试。当然大家可以看到在这个图里我们各个节点上都带有 docker 的标记, 为了方便管理和节省资源。 我们也使用容器管理的方式来建设自动化设施。 我们讲 grid hub 制作成镜像,可以运行在 k8s 中当做一个服务。 然后将不同的浏览器制作成不同的镜像。 比如把 chrome 和 Firefox 做成不同的 node 镜像,也通过 k8s 启动服务,注册到 grid hub 中。 当然大家可以发现我们在上面的这个图里在 IE 的镜像那里画一个叉,因为目前 docker 还是无法制作 ie 的镜像的,这涉及到 docker 的原理,docker 使用的是宿主机的内核也就是 linux 系统的内核。 还是无法驱动 windows 的 GUI 的。 所以这部分没有办法,只能够通过在外面启动虚拟机的方式接入 ie 浏览器了。 不过其他两种浏览器还是可以很简单的使用的。 并且通过 k8s 的自动分片的功能,我们可以很方便的启动多个 node 来支持自动化测试。 k8s 强大的调度能力也会在资源上做权衡,做到资源利用最大化,而不会把这些 node 都启动到一个节点上。 同时健康检查机器可以监控到每个 node 和 hub 的状态,如果出现异常会自动的找到可用的节点重新部署。 资源管理能力也可以限制和申请适当的资源,很方便实现的超卖功能。 这样通过 k8s 的自动化运维的能力,我们可以很好的管理我们的浏览器集群。 当然也许会有同学问都是使用 docker 启动的浏览器,那么我们怎么样能够实时的看到浏览器在发生什么呢?以前使用 windows 的时候我们是可以通过界面看到发生的所有事情,方便调试。 其实我们在这里也是可以做到的。 我们在一个 node 镜像中都安装 vnc 服务,并向外暴露端口。 我们在自己的本机上使用 vnc viewer 就可以看到浏览器上发生的一切。

这时候我们的整体工作流程就是下面这个样子的了。

这里我们有两种做法, 一种是 jenkins 直接调用 k8s 的 job,job 执行自动化测试,执行完毕后 jenkins 拉取 report。 另一种做法是直接利用的 jenkins 的 slave 机制,把 slave 直接用 k8s 中的容器的形式启动起来。 这样 j8s 中的一个容器就是 jenkins 的 slave 了。 如果为了简单,我们可以选择后者,利用 k8s 的自动化运维能力,提供稳定的服务。 在 jenkins 上的 job 上绑定测试的 repo,jenkings 触发测试的时候,jenkins 的 salve 会到 git 上拉取代码,并执行自动化测试, 由于我们的 slave 就是启动在 k8s 里的容器,所以他们的访问是无障碍的,甚至完全不需要端口映射和 7 层路由。 slave 的测试请求会发送到同样是启动在 k8s 中的 grid hub 上,gird hub 会把任务发送到同样在 k8s 中作为容器启动的 node 上面去执行。 之后将测试结果一层层的返回,汇总到 jenkins 的 job 中。 这里使用 k8s 的优势就是完全的自动化运维管理,出现任何异常 k8s 都会帮我们处理,这比搭建多个虚拟机来管理要方便便捷很多。 通过这样的架构,在我们的公司的日常测试中使用 40 个浏览器并发测试的方式,可以在 30 分钟内结束战斗。 极大的提升了测试效率。这里在安利一波 selenide,这是一个 github 上面的开源项目, 基于 selenium 做了一层封装。 可以很好的支持我们的这种架构。我们只需要两行代码,就可以对接这种分布式架构了。 如下:

上面的第一个配置指定使用的浏览器类型,第二个配置指定 grid hub 的地址。 这样就可以让我们的代码测试代码迁移到这个分布式架构上了。 其他的任何代码都不需要改变。 之后的工作只需要在 testng 的配置文件中指定并发数量就可以了。

尾声

今年测试开发大会我特意推荐了我们公司的两个同事去分享 topic, 一个是机器学习算法架构的测试,一个就是 k8s 下的 CICD。 可惜算法架构测试的那位同事掉链子了,而推荐的另一位蚂蚁金服的同学分享这方面的内容因为公司层面规则的问题也无法分享了, 我打算明年自己申请一个 topic 分享来弥补这方面的缺失。不过好在超哥的 k8s 的 topic 还是保留了下来。 我一直认为 k8s 将会是一种趋势,前年 k8s 击败了 mesos 和 swarm 成为容器编排界的扛把子。去年 tensorflow on k8s 开始流行, 就在前不久 spark 宣布支持 k8s 调度平台, google 宣布 tensorflow 将天然支持 k8s 调度。 在微服务和机器学习大行其道的今天,我觉得及早掌握 k8s 技能对于测试人员来说将会是很重要的一步。将来会有越来越多的应用和框架运行在 k8s 中, 而拥有了 k8s 能力的 QA,将会抢占一定的先机。


↙↙↙阅读原文可查看相关链接,并与作者交流