前言
为什么Kubernetes需要Coscheduling功能?
Kubernetes目前已经广泛的应用于在线服务编排,为了提升集群的的利用率和运行效率,我们希望将Kubernetes作为一个统一的管理平台来管理在线服务和离线作业。但是默认的调度器是以Pod为调度单元进行依次调度,不会考虑Pod之间的相互关系。但是很多数据计算类的作业具有All-or-Nothing特点,要求所有的任务都成功创建后才能正常运行,如果只是部分任务启动的话,启动的任务将持续等待剩余的任务被调度。
如下图所示,JobA需要4个Pod同时启动,才能正常运行。Kube-scheduler依次调度3个Pod并创建,到第4个Pod时,集群资源不足,则JobA的3个Pod处于空等的状态,导致集群资源浪费。
如果出现更坏的情况的话,如下图所示,集群其他的资源刚好被JobB的3个Pod所占用,同时在等待JobB的第4个Pod创建,此时整个集群就出现了死锁。
社区相关的方案
社区目前有Kube-batch以及基于Kube-batch衍生的Vocalno 2个项目来解决上文中提到的痛点。实现的方式是通过开发新的调度器将Scheduler中的调度单元从Pod修改为PodGroup,以组的形式进行调度。使用方式是如果需要Coscheduling功能的Pod走新的调度器,其他的例如在线服务的Pod走Kube-scheduler进行调度。
这种情况虽然能够有效的解决Coscheduling的问题,但是同样引入了新的问题。如大家所知,对于同一集群资源,调度器需要中心化。但如果同时存在两个调度器的话,有可能会出现决策冲突,例如分别将同一块资源分配给两个不同的Pod,导致某个Pod调度到节点后因为资源不足,导致无法创建的问题。解决的方式只能是通过标签的形式将节点强行的划分开来,或者部署多个集群。这种方式通过同一个Kubernetes集群来同时运行在线服务和离线作业,势必会导致整体集群资源的浪费以及运维成本的增加。
Scheduler Framework
上文中两难的处境,使得大量数据计算类的任务无法迁移到Kubernetes上。与此同时,越来越多的特性被添加到Kubernetes scheduler中,使得scheduler的代码不断变大,逻辑也更复杂。系统越复杂,则问题的定位和修复就越困难。同时原有通过extender的方式扩展scheduler的方式存在扩展点有限、无法与scheduler共用Cache以及性能较差等问题。
为了解决如上问题,使scheduler扩展性更好、代码更简洁,社区提出Scheduler Framework的机制。
Scheduler Framework定义了一组扩展点,用户可以通过实现扩展点所定义的接口来定制自己的插件,并且将插件注册扩展点。Scheduler Framework在执行调度流程时,运行到相应的扩展点时,将调用用户注册的插件。
我们基于Kube-scheduler framework基础上扩展实现Coscheduling的能力,解决原生调度器对于All-or-Nothing作业调度的问题。
scheduler-plugins
Kubernetes负责Kube-scheduler的小组sig-scheduler为了更好的管理调度相关的Plugin,新建了项目scheduler-plugins管理不同场景的Plugin。我们实现的Coscheduling的Plugin将会成为该项目的第一个插件。目前Kep已经merge,代码在Review阶段(https://github.com/kubernetes-sigs/scheduler-plugins/pull/4)。
技术方案
总体架构
PodGroup
我们通过label的形式来定义PodGroup的概念,拥有同样label的Pod同属于一个PodGroup。min-available是用来标识该PodGroup的作业能够正式运行时所需要的最小副本数。
labels:
pod-group.scheduling.sigs.k8s.io/name: nginx
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 1.16版本
- 集群节点可以访问公网
- helm v3:ACK在master节点上默认安装helm,请确认版本是否为helm v3,如果是helm v2请升级值helm v3,安装helm v3请参考helm v3 安装。
部署Coscheduling
安装ack-coscheduling时,会用增强后的调度器替换原生的Scheduler以支持Coscheduling功能,并且会修改Scheduler的相关Config文件。用户可通过下文提示的卸载功能恢复集群及相关配置。
下载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时,在创建的Pod处通过label的形式配置好min-available和name
labels:
pod-group.scheduling.sigs.k8s.io/name: nginx
pod-group.scheduling.sigs.k8s.io/min-available: "3"
name:用于表示PodGroup的Name
min-available: 用于表示当前集群资源至少满足min-available个pod时,才能统一调度创建
备注: 要求属于同一个PodGroup的Pod必须保持相同的优先级
Demo展示
当前集群资源为10核, 仅足够满足如下3个Nginx pod运行,我们把minAvailable的值设置为3
apiVersion: apps/v1
kind: ReplicaSet
metadata:
name: nginx
labels:
app: nginx
spec:
replicas: 6
selector:
matchLabels:
app: nginx
template:
metadata:
name: nginx
labels:
app: nginx
pod-group.scheduling.sigs.k8s.io/name: nginx
pod-group.scheduling.sigs.k8s.io/min-available: "3"
spec:
containers:
- name: nginx
image: nginx
resources:
limits:
cpu: 3000m
memory: 500Mi
requests:
cpu: 3000m
memory: 500Mi
集群中运行3个Nginx Pod
# kubectl get pods
NAME READY STATUS RESTARTS AGE
nginx-4jw2m 0/1 Pending 0 55s
nginx-4mn52 1/1 Running 0 55s
nginx-c9gv8 1/1 Running 0 55s
nginx-frm24 0/1 Pending 0 55s
nginx-hsflk 0/1 Pending 0 55s
nginx-qtj5f 1/1 Running 0 55s
如果此时minAvailable设置为4,则因为资源不满足minAvailable的要求,所有的Nginx Pod均处于Pending状态
# kubectl get pods
NAME READY STATUS RESTARTS AGE
nginx-4vqrk 0/1 Pending 0 3s
nginx-bw9nn 0/1 Pending 0 3s
nginx-gnjsv 0/1 Pending 0 3s
nginx-hqhhz 0/1 Pending 0 3s
nginx-n47r7 0/1 Pending 0 3s
nginx-n7vtq 0/1 Pending 0 3s
未来展望
通过Scheduler Framework的不断优化,我们相信未来会释放更大的潜力,用户可以通过插件的扩展就可以满足自身的需求,再也不用陷入自定义调度器的困境中。同时为了更好的支持数据计算类型的任务迁移到Kubernetes平台中,我们也在努力将数据计算类型中常用Capacity Scheduling、Dominant Resource Fairness和多队列管理等特性,通过Scheduler Framework的机制来融入到原生的Kube-scheduler的中,敬请期待。