1 背景介绍
kubernetes原生调度器是基于单pod调度的,而大数据和机器学习场景中的一个任务往往需要并行运行多pod来完成,多个 pod 之间需要同步信息来执行复杂的并行算法,单个 pod 无法完成全部计算任务。
最出名的batch作业的例子应该是spark的 "driver-executor模式" 了。正是由于作业的这种属性,对作业调度平台也提出了相应的调度要求。比如spark任务需要同时运行driver和executor,TensorFlow任务需要同时运行ps和worker。

spark on kubernetes 场景,需要同时运行driver 和 executor
原生的调度器在处理batch任务时,作业之间可能造成死锁(两个任务都占用了部分资源,但两个任务都不能完全调度)。
Volcano正是针对batch任务调度而生的。目前volcanoVolcano是CNCF下唯一的基于Kubernetes的容器批量计算平台,已被广泛使用。
volcano-scheduler中的Gang调度算法,它实现了batch任务的子pod调度过程中 "全部调度" or "全都不调度" 的能力。避免了pod的任意调度造成的死锁和资源浪费。
2 架构
Volcano 核心组件主要包含三个:Admission、ControllerManager、Scheduler 。
Admission对Volcano CRD API提供校验能力
ControllerManager负责对Volcano CRD进行资源管理
Scheduler对任务提供丰富的调度能力。

volcano 架构
3 Volcano-CRD介绍
3.1 PodGroup
PodGroup定义一组强关联的pod。例如Tensorflow批处理场景中的ps-pod和worker-pod都属于同一个podgroup。
PodGroup.yaml示例:
apiVersion: scheduling.volcano.sh/v1beta1
kind: PodGroup
metadata:
name: test
namespace: default
spec:
minMember: 1 ## 被gang插件使用,表示该podgroup下最少需要运行的pod数量。如果集群资源不满足miniMember数量的pod运行,调度器将不会调度任何一个该podgroup内的pod。
minResources: ## minResources表示运行该podgroup所需要的最少资源。当集群可分配资源不满足minResources时,调度器将不会调度任何一个该podgroup内的pod。
cpu: "3"
memory: "2048Mi"
priorityClassName: high-prority
queue: default ## queue表示该podgroup所属的queue。queue必须提前已创建且状态为open。

podgroup在volcano调度处理中的位置
当各种工作负载提交后,会自动生成podgroup,podgroup的信息会被收集到volcano-scheduler的cache中作为调度这个作业所需要参考的信息。接着volcano-scheduler根据作业信息调度这个作业中的pod。

podgroup的生命周期
Pending表示该podgroup已经被创建,等待被调度。如果工作负载类型为vcjob,podgroup为pending状态时不会为其创建pod,减小apiserver的压力。
Inqueue表示该podgroup已经通过了调度器的校验并入队,即将为它分配资源。inqueue是一种处于pending和running之间的中间状态。
Running表示该podgroup至少有minMember个pod处于running状态。
Unknown表示该podgroup中部分pod处于running状态,但running pod的个数不足minMember数量。
Completed表示该podgroup中的全部pod都运行完成。

k8s原生资源结合podgroup

vcjob与podgroup的关系

tf-operator结合podgroup
自定义operator、k8s原生资源都可以与podgroup结合使用,并使用volcano的调度能力。
3.2 Queue
Queue划分集群中的计算资源,也是容纳PodGroup的队列。
PodGroup依据所属的Queue的资源,来计算是否有足够资源来运行PodGroup中的pod。(即Pending到Inqueue)
Queue.yaml示例:
apiVersion: scheduling.volcano.sh/v1beta1
kind: Queue
metadata:
name: default
spec:
reclaimable: true ## 表示该queue在资源使用量超过该queue所应得的资源份额时,是否允许其他queue回收该queue使用超额的资源,默认值为true。(需要开启reclaim action)
weight: 1 ## 软限制 表示该queue在集群资源划分中所占的相对比重,该queue应得资源总量为 (weight/total-weight) * total-resource。当queue中任务较多时可以超过weight限制,去借用其他空闲queue的部分资源。
capability: ## 硬限制 表示该queue内所有podgroup使用资源量之和的上限。
cpu: 4
memory: 4096Mi
quarantee: ## 硬限制 表示资源预留配置,即使queue空闲,也为其保留这些计算资源,这部分不许被其他queue共享。
resource:
cpu: 500m
memory: 1024Mi Queue资源分配示例:

资源被多个queue分配
3.3 Volcano Job
简称vcjob,是Volcano自定义的Job资源类型。区别于Kubernetes Job,vcjob提供了更多高级功能,如可指定调度器、支持最小运行pod数、 支持task、支持生命周期管理、支持指定队列、支持优先级调度等。
volcano job有很多值得学习的设计,但笔者建议在实际落地时使用自己的job-operator,而不是使用volcano job,理由如下:
各团队有自己独特的需求,如果对volcano job代码进行修改,则在后续更新volcano版本时难以处理。
volcano-job 是对底层 queue podgroup scheduler功能的整理集成,开发自己的crd也能与podgroup结合,实现这些功能。
3.4 Queue、PodGroup 和 VolcanoJob 的关系
Queue 是一个 PodGroup 队列,PodGroup 是一组强关联的 Pod 集合。
VolcanoJob 对应的下一级资源是 PodGroup,VolcanoJob 控制器会根据 VolcanoJob 的具体配置去创建相应的 PodGroup 出来。就好比 Deployment 的下一级资源是 ReplicaSet 一样。
PodGroup 最终会被当做一个整体被 Volcano Scheduler 调度。在调度的过程中,Volcano 还用到了 Queue 来实现 PodGroup 的排队、优先级控制等逻辑。也正因为 Volcano-Scheduler 处理的对象是 PodGroup,我们才能如上面所述跳过 volcanoJob 来使用 volcano。
4 Volcano-Scheduler
Volcano Scheduler是负责Pod调度的组件,内部处理流程如下图:

volcano-scheduler 工作流
Cache 缓存了集群中Node和Pod信息,并根据PodGroup的信息重新构建 Job (PodGroup) 和 Task (Pod) 的关系。由于在分布式系统中很难保证信息的同步,因此调度器经常以某一时间点的集群快照进行调度;并保证每个调度周期的决定是一致的。在每个调度周期中,Volcano 通过以下几个步骤派发作业:
在每个调度周期都会创建一个Session对象,用来存储当前调度周期的所需的数据,例如,Cache 的一个快照。当前的调度器中仅创建了一个Session,并由一个调度线程执行;后续将会根据需要创建多个Session,并为每个Session分配一个线程进行调度;并由Cache来解决调度冲突。
在每个调度周期中,会按配置文件中的action顺序执行action,根据配置文件中的plugin在每个action中执行调度插件。最后CloseSession负责清理中间数据。
action定义了调度各环节中需要执行的动作,类似于原生调度器中的拓展点。
plugin根据不同场景提供了action 中算法的具体实现细节。
volcano的配置文件决定volcano执行哪些调度动作和调度插件。
# cat volcano-scheduler.conf
actions: "enqueue, allocate, backfill"
tiers:
- plugins:
- name: priority
- name: gang
- name: conformance
- plugins:
- name: drf
- name: predicates
- name: proportion
- name: nodeorder
- name: binpack 需要注意的是,配置文件中 action所写的顺序,就是执行顺序。Volcano本身不会对action顺序的合理性进行检查。
5 Action和Plugin介绍 & 实战场景配置
5.1 enqueue action
判断集群中的资源是否满足podgroup的最小资源量,如果不满足,则先不让podgroup进入queue,也不创建podgroup中的pod。起到限流的作用,提高调度性能。

enqueue action
enqueue action 主要插件有:
overcommit:当集群总资源经过1.2倍放大后,空闲资源仍不足以调度作业时,拒绝作业通过enqueue action。放大1.2倍,因为volcano支持抢占,可以让稍微超过当前资源空闲量的任务调度,然后才能发生抢占。
sla插件:表示podgroup停留在 pending 状态不被调度的最长等待时间。配置在configmap中对经由volcano调度的全部作业均生效。当作业等待时间达到sla-waiting-time时,SLA 插件强制作业通过enqueue action,并将podgroup的状态改为inqueue 。同时 SLA 插件在allocate action中通过预调度的方式为该作业锁定空闲资源,即使该作业还不能成功运行,从而避免空闲资源被后续作业占用
配置文件示例:
actions: "enqueue, allocate, backfill"
tiers:
- plugins:
- name: sla
arguments:
sla-waiting-time: 30m10s ## 设置最大等待时长30分10秒
- name: overcommit
arguments:
ovecommit-factor: 1.2 ## 放大倍数,默认为1.2
5.2 Allocate action
负责通过一系列的预选和优选算法筛选出最适合的节点。action在配置文件中必填,否则不能调度成功。
allocate action 主要插件有:
nodeorder插件:它集成了原生kubernetes调度器的打分算法,能够让用户根据自身需求组合出最适合的打分算法(维度)集合,并且可以设置各个算法权重。 把weight设置为0可以关闭某维度的计算。
predicate插件:过滤不符合要求的Node。predicate插件还能用于解决GPU节点因CPU等其他维度资源过度使用引起GPU作业饥饿但GPU资源空闲浪费的问题,即gpu节点上的mem被普通微服务占用了,当使用gpu的ai训练作业想要调度到这个节点时发现mem不够,从而无法调度。用户设置gpu为主导资源,并可为它配置配套资源维度的预留比例(如GPU:CPU:Memory=1:4:32)。调度器在工作时将会时刻保持GPU节点上GPU、CPU、Memory的空闲资源比例不低于该设定值,因此任何时刻符合该比例需求的GPU作业均可调度到该节点,而不会引起GPU浪费。
binpack插件:尽量把已用的节点填满(尽量不往空白节点分配)。这种调度算法能够尽可能减小节点内的碎片,在空闲的机器上为申请了更大资源请求的Pod预留足够的资源空间,使集群下空闲资源得到最大化的利用。binpack在优选时生效(predicate插件后),Volcano-scheduler在计算binpack算法时,会考虑Pod请求的各种资源,每种资源在节点分值计算过程中的权重取决于配置文件中的设置。同时不同的插件在计算节点分数时,也需要分配不同的权重,scheduler也为binpack插件设置了分数权重。
配置文件示例:
actions: "enqueue, allocate, backfill"
tiers:
- plugins:
- name: gang ## gang插件没有参数,开启即可。 podgroup的minMember依赖于gang插件
- name: predicates
arguments:
predicate.ProportionalEnable: true
predicate.resources: nvidia.com/gpu
predicate.resources.nvidia.com/gpu.cpu: 8
predicate.resources.nvidia.com/gpu.memory: 8
- name: nodeorder
arguments:
leastrequested.weight: 1
mostrequested.weight: 0
nodeaffinity.weight: 1
podaffinity.weight: 1
balancedresource.weight: 1
tainttoleration.weight: 1
imagelocality.weight: 2
- plugins:
- name: binpack
arguments:
binpack.weight: 10 ## binpack插件的权重(allocate action有许多计算插件,可以把某个插件权重调大,作为主要计算标准)
binpack.cpu: 2 ## binpack计算时,cpu所占权重较高
binpack.memory: 1 ## binpack计算时,mem所占权重较低

配置示例中binpack配置的计算说明
5.3 Preempt action 和 Recliam action
Preempt action 用于为“高”优先级的Pending作业选取一个或多个“低”优先级的作业进行驱逐。Preempt 用于同一个Queue中podgroup之间的抢占,或同一podgroup下pod之间的抢占。
recliam action 用于跨queue的资源占用之后,把本queue资源拿回的抢占。
5.4 Backfill action
backfill action 一般放在actions中的最后,是调度流程中的回填步骤,用于处理待调度Pod列表中没有指明资源申请量的Pod。
backoff 的计算很简单只需判断 亲和性、污点、是否满足Gang调度等基础条件。
如果不在配置文件里明确开启backfill,那么volcano不会调度没有资源限制的pod,pod处于pending状态并显示 all nodes are unavailable。