本文作者为霍格沃兹测试开发学社特约讲师乔巴
K8S目前是业界容器编排领域的事实标准,是几乎所有云原生架构的首选。目前随着云原生架构越来越流行,测试开发人员需要掌握K8S技术栈已经成为越来越迫切的需求。
Kubernetes 开源于 2014 年,是谷歌 10 多年大规模容器管理系统 Borg 的开源版本。Kubernetes 这个单词在首字母 K 和尾字母 s 之间有 8 个字母,因此称为 K8S。这种称谓方式和 i18n(internationalization)是一致的,如果做过本地化国际化的人应该对 i18n 这样的叫法很熟悉。对于一个刚刚接触容器的初学者来说,搞清楚容器编排是什么,搞清楚 K8S 是什么是一件非常不容易的事情,编排二字赋予了它非常多的意义。
大多数人理解 K8S 是容器集群的管理技术,这个描述是不完整的,如果 K8S 仅仅是一个管理多台节点上容器的管理软件的话,那么业界直接称呼为容器集群就好了。而不是像现在这样称其为容器编排领域的事实标准,谷歌和 Linux 也不会为了它一起创办了 CNCF 云原生基金会。所以 K8S 除了是一个容器集群管理软件外它还提供了针对容器的网络,调度,权限,资源,安全,硬件等管理和设计的能力。 接下来通过 2 个案例来带大家体验一下其中的奥妙。
在实际介绍 K8S 的容器编排实例前需要先了解一下 K8S 中最基本的资源类型--POD。可以说 POD 是 K8S 中最重要的资源,其他一切的资源都是围绕着 POD 并为其提供服务的。用一句话说明 POD 的定义:POD 是由多个容器组成的逻辑概念,这些容器共同配合对外提供服务, 同时 POD 也是 K8S 中最小的调度单位,POD 中的容器必须调度在同一台机器上不可分割。这么说比较抽象,用一个实例来展示一下 POD 到底是什么。通过下载并配置 jenkins 中 K8S 的插件来打通两者之间的通信,使得 jenkins 在运行 pipeline 时可以动态的在 K8S 中创建 POD 并在其中一个容器中通过 jnlp 动态的创建并向 jenkins 注册 slave 节点(容器), 后续这个 pipeline 中所有的任务都将在这个 POD 中的容器中执行。通过这样的机制实现了更强大的 jenkins pipeline 的高可用和负载均衡架构。从此实现了在 K8S 中可以动态创建 jenkins 的 slave 节点运行任务的能力, 并在任务结束后回收这些资源。
yaml
apiVersion: "v1"
kind: "Pod"
metadata:
name: "sdk-test-109-hpf67-tr47k-95sch"
spec:
containers:
- command:
- "cat"
image: "registry.gaofei.com/qa/python3"
name: "python3"
tty: true
volumeMounts:
- mountPath: "/home/jenkins/agent"
name: "workspace-volume"
readOnly: false
image: "registry.gaofei.com/tester_jenkins_slave:v1"
name: "jnlp"
volumeMounts:
- mountPath: "/home/jenkins/agent"
name: "workspace-volume"
readOnly: false
volumes:
- emptyDir:
medium: ""
name: "workspace-volume"
上面是 jenkins 动态创建 POD 的配置文件,这其中为了更方便说明我删除了很多其他干扰项,只留下了最需要关注的部分。可以看到 containers 字段中定义两个容器。其中名字为 jnlp 的容器是由 jenkins 提供用来与 jenkins 建立通信并注册 slave 节点用的。对 jenkins slave 节点配置比较熟悉的人对此应该并不陌生,除了 jnlp 外 jenkins 还支持 ssh 等协议形式的 slave 通信机制。
另外一个名字为 python3 的容器使用的就是官方提供的 python3 镜像,它的任务是用来执行测试任务。也就是说在这个 POD 中分工是明确的,jnlp 容器负责注册 jenkins slave 节点并与之保持通信。而 python3 容器拥有 python 的执行环境所以可以在获取代码后运行诸如 pytest 这样的测试任务。实际上如果需要可以定义更多的容器,比如要测试一款 python sdk 的兼容性的时候, 可以再定义一个 python2.6 的容器,这样在 pipeline 中可以通过切换不同的容器达到切换运行环境的目的以便测试 sdk 在 python3 和 python2 上的兼容性。
下面我贴一下 jenkins pipeline 中的定义,还是照例删减了其他干扰项。
groovy
pipeline{
parameters {
choice(name: 'PLATFORM_FILTER', choices: ['python352', 'python368', 'python376','all'], description: '选择测试的 python 版本')
}
agent{
kubernetes{
yaml """
apiVersion: v1
kind: Pod
metadata:
labels:
qa: python3
spec:
containers:
- name: python352
image: python:3.5.2
command:
- cat
tty: true
- name: python368
image: python:3.6.8
command:
- cat
tty: true
- name: python376
image: python:3.7.6
command:
- cat
tty: true
- name: jnlp
image: registry.gaofie.com/tester_jenkins_slave:v1
"""
}
}
stages{
stage('sdk 兼容性测试'){
matrix {
when { anyOf {
expression { params.PLATFORM_FILTER == 'all' }
} }
axes {
axis {
name 'PLATFORM'
values 'python352', 'python368','python376'
}
}
stages{
stage('兼容性测试开始 '){
steps{
container("${PLATFORM}"){
echo "Testing planform ${PLATFORM}"
sh """
pip3 install -i http://pypi.xxx.com/4paradigm/dev/ --trusted-host pypi.xxx.com 'sdk[builtin-operators]'
pip3 install -r requirements.txt
cd test
python3 -m pytest -n 5
"""
}
}
}
}
}
}
}
}
通过上面的 Pipeline 的配置可以看到通过 container 指令,可以在 pipeline 中任意的切换容器(运行环境)来完成 Python 的兼容性测试。这里可能有人可能会问运行环境可以通过切换容器来完成,但是各个容器之间是怎么共享文件和代码的呢?毕竟要执行测试必须先获取代码, 那这些容器是怎么获取代码执行测试的,又是通过什么方式合并每个容器中的测试报告的呢?这个问题可以抽象成一个 POD 中的容器是怎么共享文件的。在学习 Docker 的时候知道在启动容器的时候可以通过-v 这个参数来将容器中的某个目录或文件挂载到宿主机上, 而在 POD 中的玩法也类似。回到上面 Jenkins 启动的 POD 的定义中来:
yaml
image: "registry.gaofei.com/tester_jenkins_slave:v1"
name: "jnlp"
volumeMounts:
- mountPath: "/home/jenkins/agent"
name: "workspace-volume"
readOnly: false
volumes:
- emptyDir:
medium: ""
name: "workspace-volume"
上面是 POD 中关于数据卷的一段定义, 可以看到 jenkins 创建的 POD 定义中自动添加了一个临时的共享目录,而 POD 中所有的容器都会挂载这个目录。通过这样的形式达到了所有容器共享文件的目的。
而这个目录就是 Jenkins 的 Workspace。相信熟悉 Jenkins 的人对此目录不会感到陌生。
实际上多个容器间的合作不仅可以共享目录,也可以共享网络或者进程名称空间。还记得学习 Docker 的时候使用的 container 网络模式么, 实际上 POD 中的容器都是默认通过 container 模式将网络连接在一起的,很多软件应用比如 mock server,流量复制,service mesh 都是通过在 POD 中额外定义一个 proxy 容器劫持业务容器的网络。
而如果你想使用 jvm-sandbox 这种字节码注入工具的话还可以通过打开 POD 中 shareProcessNamespace 这个参数来共享进程名称空间,使得 jvm-sandbox 容器中可以看到业务容器的进程并以 jvm-attach 的方式进行字节码注入。而这种通过启动多个容器互相协作配合的玩法有一个专业名词叫"side car"。
所以回过头来看看什么是 POD,什么是容器编排?从这里的角度看 POD 是容器之间的一种协作模式,多个容器组成一个 POD,而一个 POD 提供了多种机制,包括但不限于共享和限制目录,网络,进程,资源等机制来让容器之间的协作更加顺畅, 而这也是容器编排的表现之一, 不仅仅是运行, 而是多个容器配合在一起更好的运行。
初学者容易误以为容器的任务只在于部署行为--将软件在容器中部署以提供持续的服务。但其实容器也同样大量的被应用于批处理程序的运行上。比如测试行为是典型的批处理任务范畴, 它不提供持续稳定的服务, 它只是一段特定的程序,而一但这段测试程序结束后就应该销毁一切,包括执行环境和所占用的资源,容器对比于传统的虚拟机的优势也在于除了容器更加的轻量级外, 容器的创建和销毁都很方便,通过 K8S 的能力可以很方便的在需要时创建,结束时销毁回收资源以达到更好的资源利用率(就如上篇文章中介绍的 Jenkins 与 K8S 打通后的运作模式)。而现在准备的测试案例会更加特殊, 它需要重复运行 N 次,因为本次执行的是稳定性测试(也有人叫它浸泡测试或者长期高压测试),这种测试类型的特殊之处就在于它的目的是验证被测系统在长期的高压下是否仍能够提供稳定的服务。所以它的测试方式是长期的(1 天,1 周甚至更长时间)不间断的运行自动化测试。而自动化测试的数量是有限的,它不可能持续的运行那么长时间,所以才需要重复运行。在不改造测试框架的前提下 K8S 能通过什么样的方式来帮助完成这个测试需求。首先看一段 K8S 提交任务的配置文件。
yaml
apiVersion: batch/v1
kind: Job
metadata:
name: stable-test
spec:
template:
spec:
containers:
- name: stable
image: registry.gaofei.com/stable_test
imagePullPolicy: Always
volumeMounts:
- name: stableconfig
mountPath: '/home/work/configs'
readOnly: false
restartPolicy: Never
volumes:
- name: 'stableconfig'
configMap:
name: stableconfig
backoffLimit: 4
parallelism: 1
completions: 1000
上面定义的是向 K8S 提交一个 job 类型的也即是批处理程序请求的配置文件, 将这个配置文件保存为 yaml 文件后就可以通过 kubectl 命令行将任务提交到 K8S 集群中运行了, job 会帮助创建相应的 POD 来完成任务。虽然我已经对这段配置做了一定程度的删减,但仍然有不少的字段类型容易让新手眼花缭乱。不过本次案例只需关注几个重点的地方,第一个是在文件中的 template 字段, 它代表了 POD 的模板, job 通过此模板来动态的创建 POD,它定义了本次执行测试的运行环境, 也就是测试是在 POD 中的容器中执行的。K8S 会根据用户填写的内容来启动 POD。第二个需要注意的地方是配置中最下面的 3 个字段:
backoffLimit:可容忍的失败次数。稳定性测试是要长期执行的,而任何长期执行的任务都无法保证在运行过程中 100% 的不出问题,有些时候网络卡顿或者公司内的一些基础设施的临时中断都可能造成测试的失败。所以 K8S 会在任务失败时尝试进行重试(当整个节点出现异常时,K8S 可以将容器调度到其他节点上重试执行,拥有更好的容错能力),而这个字段可以理解为重试的次数
parallelism:并行的数量。如果你的批处理任务需要并发能力,那么 K8S 会按照这个字段的数字同时启动多个容器来并发的执行。由于大部分的测试并发能力来源于测试框架而不是外部软件, 所以本次测试在这里填写为 1 就可以。
completions:任务成功执行 N 次后结束任务。即便是像稳定性测试这种需要长期运行的测试类型,它也有结束测试的时候。所以把这个参数设定为 1000 代表当测试重复运行了 1000 次后就结束本次的批处理任务。
注意:每次测试运行结束后,K8S 会销毁当前的容器,并启动一个一模一样的新容器来执行新的任务。也就是在的案例里如果不出意外的话,前后会启动 1000 个容器来完成本次的稳定性测试。通过这样一个案例的讲解可以体会一下相比于原生的 Docker 容器,K8S 带来了多少额外的能力。在 K8S 中容器只不过是程序的运行时环境而已,除了程序能运行起来,K8S 更关注的是程序怎样更好的运行。通过上面针对配置文件最后 3 个字段的讲解可以看出来 K8S 在尝试帮助用户解决更复杂的程序运行问题。在本案例中如果不使用 K8S,用户需要编写自己的模块来控制测试用例的重复执行,并发,容错和重试机制,也就是说用户需要自己编写代码来对测试用例进行"编排"。在传统的容器场景中,很多人都会把容器当做一个小型的虚拟机来使用–只要程序能在容器里跑起来就可以了。这种模式并不具备"编排"的思维能力,真实的企业场景下要求的不仅仅是把程序跑起来就可以了,还关心容器调度到什么节点,什么时候触发和结束任务,当任务出现异常时要如何处理,容器和容器之前如何配合以便完成更大的任务等等。这便是 K8S 提供的"容器编排"了。希望读者可以用心体会"容器编排"这 4 个字的含义。
接下来再看一下,如果希望任务能够定时触发该怎么办呢?K8S 中同样提供了 CronJob 类型的任务,可以看到在 schedule 字段中可以填写 cron 表达式来定时启动容器完成的批处理任务。
yaml
apiVersion: batch/v1beta1
kind: CronJob
metadata:
name: k8scleaner
spec:
schedule: '0 * */1 * *'
jobTemplate:
spec:
template:
spec:
containers:
- name: k8scleaner
image: reg.gaofei.com/qa/k8s-cleaner:v2
imagePullPolicy: Always
restartPolicy: Never
实际上,目前看到的编排能力仍然是 K8S 的冰山一角,K8S 目前已经成为了分布式计算平台,支持很多大数据和机器学习的计算框架比如 Spark 和 Flink。下面是将 Spark 任务调度到 K8S 中执行的 Demo。
bash
./bin/spark-submit \
--deploy-mode cluster \
--class org.apache.spark.examples.SparkPi \
--master k8s://https://172.27.130.144:6443 \
--kubernetes-namespace spark-cluster \
--conf spark.kubernetes.authenticate.driver.serviceAccountName=spark \
--conf spark.executor.instances=5 \
--conf spark.app.name=spark-pi \
--conf spark.kubernetes.driver.docker.image=kubespark/spark-driver:v2.2.0-kubernetes-0.5.0 \
--conf spark.kubernetes.executor.docker.image=kubespark/spark-executor:v2.2.0-kubernetes-0.5.0 \
local:///opt/spark/examples/jars/spark-examples_2.11-2.2.0-k8s-0.5.0.jar
熟悉大数据领域的人都知道 Hadoop 是分布式计算领域中最流行的调度平台。提交的 Spark 任务都会被调度到 Hadoop 集群中进行调度,运行。但是 K8S 也同样具备这样的能力,通过下载支持 K8S 的 Spark 安装包就可以使用 spark-submit 命令将任务提交到 K8S 上以容器的形态执行,在参数中可以指定使用多少个 executor,每个 executor 申请多少资源等等。这便是 K8S 的魅力,如果你深入了解 K8S 会发现更多有趣又好用的功能。
总结
实际上除了上面讲的能力外,K8S 还包含了非常多的容器编排能力,尤其对于在线服务的编排能力上尤为强大, 但这部分内容留待后续讲解。最后附上一个最简单的 K8S 流程图帮助大家理解。毕竟 K8S 还是一个集群管理软件,上述说明的所有案例在提交给 K8S 后, K8S 都会按照自己的调度策略将 POD 调度到一个合适的节点上执行。