前言
首先我们来了解一下什么是Coscheduling和Gang scheduling。Wikipedia对 Coscheduling的定义是“在并发系统中将多个相关联的进程调度到不同处理器上同时运行的策略”。在Coscheduling的场景中,最主要的原则是保证所有相关联的进程能够同时启动。防止部分进程的异常,导致整个关联进程组的阻塞。这种导致阻塞的部分异常进程,称之为“碎片(fragement)”。
在Coscheduling的具体实现过程中,根据是否允许“碎片”存在,可以细分为Explicit Coscheduling,Local Coscheduling和Implicit Coscheduling。 其中Explicit Coscheduling就是大家常听到的Gang Scheduling。Gang Scheduling要求完全不允许有“碎片”存在, 也就是“All or Nothing”。
我们将上述定义的概念对应到Kubernetes中,就可以理解Kubernetes调度系统支持批任务Coscheduling的含义了。 一个批任务(关联进程组)包括了N个Pod(进程),Kubernetes调度器负责将这N个Pod调度到M个节点(处理器)上同时运行。如果这个批任务需要部分Pod同时启动即可运行,我们称需启动Pod的最小数量为min-available。特别地,当min-available=N时,批任务要求满足Gang Scheduling。
为什么Kubernetes调度系统需要Coscheduling?
Kubernetes目前已经广泛的应用于在线服务编排,为了提升集群的的利用率和运行效率,我们希望将Kubernetes作为一个统一的管理平台来管理在线服务和离线作业。默认的调度器是以Pod为调度单元进行依次调度,不会考虑Pod之间的相互关系。但是很多数据计算类的离线作业具有组合调度的特点,即要求所有的子任务都能够成功创建后,整个作业才能正常运行。如果只有部分子任务启动的话,启动的子任务将持续等待剩余的子任务被调度。这正是Gang Scheduling的场景。
如下图所示,JobA需要4个Pod同时启动,才能正常运行。Kube-scheduler依次调度3个Pod并创建成功。到第4个Pod时,集群资源不足,则JobA的3个Pod处于空等的状态。但是它们已经占用了部分资源,如果第4个Pod不能及时启动的话,整个JobA无法成功运行,更糟糕的是导致集群资源浪费。
如果出现更坏的情况的话,如下图所示,集群其他的资源刚好被JobB的3个Pod所占用,同时在等待JobB的第4个Pod创建,此时整个集群就出现了死锁。
社区相关的方案
社区目前有Kube-batch以及基于Kube-batch衍生的Volcano 2个项目来解决上文中提到的痛点。实现的方式是通过开发新的调度器将Scheduler中的调度单元从Pod修改为PodGroup,以组的形式进行调度。使用方式是如果需要Coscheduling功能的Pod走新的调度器,其他的例如在线服务的Pod走Kube-scheduler进行调度。
这些方案虽然能够解决Coscheduling的问题,但是同样引入了新的问题。如大家所知,对于同一集群资源,调度器需要中心化。但如果同时存在两个调度器的话,有可能会出现决策冲突,例如分别将同一块资源分配给两个不同的Pod,导致某个Pod调度到节点后因为资源不足,导致无法创建的问题。解决的方式只能是通过标签的形式将节点强行的划分开来,或者部署多个集群。这种方式通过同一个Kubernetes集群来同时运行在线服务和离线作业,势必会导致整体集群资源的浪费以及运维成本的增加。再者,Volcano运行需要启动定制的MutatingAdmissionWebhook和ValidatingAdmissionWebhook。这些Webhooks本身存在单点风险,一旦出现故障,将影响集群内所有pod的创建。另外,多运行一套调度器,本身也会带来维护上的复杂性,以及与上游Kube-scheduler接口兼容上的不确定性。
基于Scheduling Framework的方案
本系列第一篇《进击的Kubernetes调度系统 (一):Scheduling Framework》介绍了Kubernetes Scheduling Framework的架构原理和开发方法。在此基础上,我们扩展实现了Coscheduling调度插件,帮助Kubernetes原生调度器支持批作业调度,同时避免上述方案存在的问题。Scheduling framework的内容在前一篇文章详细介绍,欢迎大家翻阅。
Kubernetes负责Kube-scheduler的小组sig-scheduler为了更好的管理调度相关的Plugin,新建了项目scheduler-plugins管理不同场景的Plugin。我们基于scheduling framework实现的Coscheduling Plugin成为该项目的第一个官方插件,下面我将详细的介绍Coscheduling Plugin的实现和使用方式。
技术方案
总体架构
PodGroup
我们通过label的形式来定义PodGroup的概念,拥有同样label的Pod同属于一个PodGroup。min-available是用来标识该PodGroup的作业能够正式运行时所需要的最小副本数。
labels:
pod-group.scheduling.sigs.k8s.io/name: tf-smoke-gpu
pod-group.scheduling.sigs.k8s.io/min-available: "2"
备注: 要求属于同一个PodGroup的Pod必须保持相同的优先级
Permit
Framework的Permit插件提供了延迟绑定的功能,即Pod进入到Permit阶段时,用户可以自定义条件来允许Pod通过、拒绝Pod通过以及让Pod等待状态(可设置超时时间)。Permit的延迟绑定的功能,刚好可以让属于同一个PodGruop的Pod调度到这个节点时,进行等待,等待积累的Pod数目满足足够的数目时,再统一运行同一个PodGruop的所有Pod进行绑定并创建。
举个实际的例子,当JobA调度时,需要4个Pod同时启动,才能正常运行。但此时集群仅能满足3个Pod创建,此时与Default Scheduler不同的是,并不是直接将3个Pod调度并创建。而是通过Framework的Permit机制进行等待。
此时当集群中有空闲资源被释放后,JobA的中Pod所需要的资源均可以满足。
则JobA的4个Pod被一起调度创建出来,正常运行任务。
QueueSort
由于Default Scheduler的队列并不能感知PodGroup的信息,所以Pod在出队时处于无序性(针对PodGroup而言)。如下图所示,a和b表示两个不同的PodGroup,两个PodGroup的Pod在进入队列时,由于创建的时间交错导致在队列中以交错的顺序排列。
当一个新的Pod创建后,入队后,无法跟与其相同的PodGroup的Pod排列在一起,只能继续以混乱的形式交错排列。
这种无序性就会导致如果PodGroupA在Permit阶段处于等待状态,此时PodGroupB的Pod调度完成后也处于等待状态,相互占有资源使得PodGroupA和PodGroupB均无法正常调度。这种情况即是把死锁现象出现的位置从Node节点移动到Permit阶段,无法解决前文提到的问题。
针对如上所示的问题,我们通过实现QueueSort插件, 保证在队列中属于同一个PodGroup的Pod能够排列在一起。我们通过定义QueueSort所用的Less方法,作用于Pod在入队后排队的顺序:
func Less(podA *PodInfo, podB *PodInfo) bool
首先,继承了默认的基于优先级的比较方式,高优先级的Pod会排在低优先级的Pod之前。
然后,如果两个Pod的优先级相同,我们定义了新的排队逻辑来支持PodGroup的排序。
- 如果两个Pod都是regularPod(普通的Pod),则谁先创建谁在队列里边排在前边。
- 如果两个Pod一个是regularPod,另一个是pgPod(属于某个PodGroup的Pod), 我们比较的是regularPod的创建时间和pgPod所属PodGroup的创建时间,则谁先创建谁在队列里边排在前边。
- 如果两个Pod都是pgPod,我们比较两个PodGroup的创建时间,则谁先创建谁在队列里边排在前边。同时有可能两个PodGroup的创建时间相同,我们引入了自增Id,使得PodGroup的Id谁小谁排在前边(此处的目的是为了区分不同的PodGroup)。
通过如上的排队策略,我们实现属于同一个PodGroup的Pod能够同一个PodGroup的Pod能够排列在一起。
当一个新的Pod创建后,入队后,会跟与其相同的PodGroup的Pod排列在一起。
Prefilter
为了减少无效的调度操作,提升调度的性能,我们在Prefilter阶段增加一个过滤条件,当一个Pod调度时,会计算该Pod所属PodGroup的Pod的Sum(包括Running状态的),如果Sum小于min-available时,则肯定无法满足min-available的要求,则直接在Prefilter阶段拒绝掉,不再进入调度的主流程。
UnReserve
如果某个Pod在Permit阶段等待超时了,则会进入到UnReserve阶段,我们会直接拒绝掉所有跟Pod属于同一个PodGroup的Pod,避免剩余的Pod进行长时间的无效等待。
Coscheduling试用
安装部署
用户既可以在自己搭建的Kubernetes集群中,也可以在任一个公有云提供的标准Kubernetes服务中来试用Coscheduling。需要注意的是集群版本1.16+, 以及拥有更新集群master的权限。
本文将使用 阿里云容器服务 ACK 提供的Kubernetes集群来进行测试。
前提条件
- 支持Kubernetes 1.16以上版本
- 选择创建ACK提供的标准专有集群(Dedicated cluster)
- 保证集群节点可以访问公网
- helm v3:ACK在master节点上默认安装helm,请确认版本是否为helm v3,如果是helm v2请升级值helm v3,安装helm v3请参考helm v3 安装。
部署Coscheduling
我们已经将Coscheduling插件和原生调度器代码统一构建成新的容器镜像。并提供了一个helm chart包 ack-coscheduling来自动安装。它会启动一个任务,自动用Coscheduling scheduler替换集群默认安装的原生scheduler,并且会修改scheduler的相关Config文件,使scheduling framework正确地加载Coscheduling插件。完成试用后,用户可通过下文提示的卸载功能恢复集群默认scheduler及相关配置。
下载helm chart包,执行命令安装
$ wget http://kubeflow.oss-cn-beijing.aliyuncs.com/ack-coscheduling.tar.gz
$ tar zxvf ack-coscheduling.tar.gz
$ helm install ack-coscheduling -n kube-system ./ack-coscheduling
NAME: ack-coscheduling
LAST DEPLOYED: Mon Apr 13 16:03:57 2020
NAMESPACE: kube-system
STATUS: deployed
REVISION: 1
TEST SUITE: None
验证Coscheduling
在Master节点上,使用helm命令验证是否安装成功。
$ helm get manifest ack-coscheduling -n kube-system | kubectl get -n kube-system -f -
NAME COMPLETIONS DURATION AGE
scheduler-update-clusterrole 1/1 8s 35s
scheduler-update 3/1 of 3 8s 35s
### 卸载Coscheduling
通过helm卸载,将kube-scheduler的版本及配置回滚到集群默认的状态。
$ helm uninstall ack-coscheduling -n kube-system
使用方式
使用Coscheduling时,只需要在创建任务的yaml描述中配置pod-group.scheduling.sigs.k8s.io/name和pod-group.scheduling.sigs.k8s.io/min-available这两个label即可。
labels:
pod-group.scheduling.sigs.k8s.io/name: tf-smoke-gpu
pod-group.scheduling.sigs.k8s.io/min-available: "3"
pod-group.scheduling.sigs.k8s.io/name:用于表示PodGroup的Name
pod-group.scheduling.sigs.k8s.io/min-available: 用于表示当前集群资源至少满足min-available个pod启动时,才能整体调度该任务
备注: 属于同一个PodGroup的Pod必须保持相同的优先级
Demo展示
接下来我们通过运行Tensorflow的分布式训练作业来演示Coscheduling的效果。当前测试集群有4个GPU卡
- 通过Kubeflow的Arena工具在已有Kubernetes集群中部署Tensorflow作业运行环境。
Arena是基于Kubernetes的机器学习系统开源社区Kubeflow中的子项目之一。Arena用命令行和SDK的形式支持了机器学习任务的主要生命周期管理(包括环境安装,数据准备,到模型开发,模型训练,模型预测等),有效提升了数据科学家工作效率。
git clone https://github.com/kubeflow/arena.git
kubectl create ns arena-system
kubectl create -f arena/kubernetes-artifacts/jobmon/jobmon-role.yaml
kubectl create -f arena/kubernetes-artifacts/tf-operator/tf-crd.yaml
kubectl create -f arena/kubernetes-artifacts/tf-operator/tf-operator.yaml
检查是否部署成功
$ kubectl get pods -n arena-system
NAME READY STATUS RESTARTS AGE
tf-job-dashboard-56cf48874f-gwlhv 1/1 Running 0 54s
tf-job-operator-66494d88fd-snm9m 1/1 Running 0 54s
- 用户向集群中提交Tensorflow作业(TFJob),如下示例,包含1个Parameter Server pod和4个Worker pod,每个Worker需要2个GPU。配置了pod group,需要至少5个pod启动后才能运行。
apiVersion: "kubeflow.org/v1"
kind: "TFJob"
metadata:
name: "tf-smoke-gpu"
spec:
tfReplicaSpecs:
PS:
replicas: 1
template:
metadata:
creationTimestamp: null
labels:
pod-group.scheduling.sigs.k8s.io/name: tf-smoke-gpu
pod-group.scheduling.sigs.k8s.io/min-available: "5"
spec:
containers:
- args:
- python
- tf_cnn_benchmarks.py
- --batch_size=32
- --model=resnet50
- --variable_update=parameter_server
- --flush_stdout=true
- --num_gpus=1
- --local_parameter_device=cpu
- --device=cpu
- --data_format=NHWC
image: registry.cn-hangzhou.aliyuncs.com/kubeflow-images-public/tf-benchmarks-cpu:v20171202-bdab599-dirty-284af3
name: tensorflow
ports:
- containerPort: 2222
name: tfjob-port
resources:
limits:
cpu: '1'
workingDir: /opt/tf-benchmarks/scripts/tf_cnn_benchmarks
restartPolicy: OnFailure
Worker:
replicas: 4
template:
metadata:
creationTimestamp: null
labels:
pod-group.scheduling.sigs.k8s.io/name: tf-smoke-gpu
pod-group.scheduling.sigs.k8s.io/min-available: "5"
spec:
containers:
- args:
- python
- tf_cnn_benchmarks.py
- --batch_size=32
- --model=resnet50
- --variable_update=parameter_server
- --flush_stdout=true
- --num_gpus=1
- --local_parameter_device=cpu
- --device=gpu
- --data_format=NHWC
image: registry.cn-hangzhou.aliyuncs.com/kubeflow-images-public/tf-benchmarks-gpu:v20171202-bdab599-dirty-284af3
name: tensorflow
ports:
- containerPort: 2222
name: tfjob-port
resources:
limits:
nvidia.com/gpu: 2
workingDir: /opt/tf-benchmarks/scripts/tf_cnn_benchmarks
restartPolicy: OnFailure
- 当用户不使用Coscheduling功能时
删除上述TFJob yaml中的pod-group.scheduling.sigs.k8s.io/name和pod-group.scheduling.sigs.k8s.io/min-available标签,表示该任务不使用Coscheduling。创建任务后,集群资源只能满足2个Worker启动,剩余两个处于Pending状态。
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
tf-smoke-gpu-ps-0 1/1 Running 0 6m43s
tf-smoke-gpu-worker-0 1/1 Running 0 6m43s
tf-smoke-gpu-worker-1 1/1 Running 0 6m43s
tf-smoke-gpu-worker-2 0/1 Pending 0 6m43s
tf-smoke-gpu-worker-3 0/1 Pending 0 6m43s
查看其中正在运行的Worker的日志,都处于等待剩余那两个Worker启动的状态。此时,4个GPU都被占用却没有实际执行任务。
$ kubectl logs -f tf-smoke-gpu-worker-0
INFO|2020-05-19T07:02:18|/opt/launcher.py|27| 2020-05-19 07:02:18.199696: I tensorflow/core/distributed_runtime/master.cc:221] CreateSession still waiting for response from worker: /job:worker/replica:0/task:3
INFO|2020-05-19T07:02:28|/opt/launcher.py|27| 2020-05-19 07:02:28.199798: I tensorflow/core/distributed_runtime/master.cc:221] CreateSession still waiting for response from worker: /job:worker/replica:0/task:2
- 当用户使用Coscheduling功能时
添加pod-group相关标签后创建任务,因为集群的资源无法满足用户设定的min-available要求,则PodGroup无法正常调度,所有的Pod一直处于Pending状态。
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
tf-smoke-gpu-ps-0 0/1 Pending 0 43s
tf-smoke-gpu-worker-0 0/1 Pending 0 43s
tf-smoke-gpu-worker-1 0/1 Pending 0 43s
tf-smoke-gpu-worker-2 0/1 Pending 0 43s
tf-smoke-gpu-worker-3 0/1 Pending 0 43s
此时,如果通过集群扩容,新增4个GPU卡,资源能满足用户设定的min-available要求,则PodGroup正常调度,4个Worker开始运行
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
tf-smoke-gpu-ps-0 1/1 Running 0 3m16s
tf-smoke-gpu-worker-0 1/1 Running 0 3m16s
tf-smoke-gpu-worker-1 1/1 Running 0 3m16s
tf-smoke-gpu-worker-2 1/1 Running 0 3m16s
tf-smoke-gpu-worker-3 1/1 Running 0 3m16s
查看其中一个Worker的日志,显示训练任务已经开始
$ kubectl logs -f tf-smoke-gpu-worker-0
INFO|2020-05-19T07:15:24|/opt/launcher.py|27| Running warm up
INFO|2020-05-19T07:21:04|/opt/launcher.py|27| Done warm up
INFO|2020-05-19T07:21:04|/opt/launcher.py|27| Step Img/sec loss
INFO|2020-05-19T07:21:05|/opt/launcher.py|27| 1 images/sec: 31.6 +/- 0.0 (jitter = 0.0) 8.318
INFO|2020-05-19T07:21:15|/opt/launcher.py|27| 10 images/sec: 31.1 +/- 0.4 (jitter = 0.7) 8.343
INFO|2020-05-19T07:21:25|/opt/launcher.py|27| 20 images/sec: 31.5 +/- 0.3 (jitter = 0.7) 8.142
后续工作
利用Kubernetes Scheduling Framework的机制实现了Coscheduling,解决了AI、数据计算类的批任务需要组合调度,同时减少资源浪费的问题。从而提升集群整体资源利用率。
我们将在本系列接下来的文章中详细介绍更多针对批任务的调度策略,如Capacity Scheduling,多队列管理等特性,以及在Scheduling Framework中的设计与实现。敬请期待。
来源:oschina
链接:https://my.oschina.net/u/1464083/blog/4367953