1.引言
Kubernetes 最初源于谷歌内部的 Borg,提供了面向应用的容器集群部署和管理系统。 Kubernetes 具备完善的集群管理能力,包括多层次的安全防护和准入机制、多租户应用支撑能力、透明的服务注册和服务发现机制、内建负载均衡器、故障发现和自我修复能力、服务滚动升级和在线扩容、可扩展的资源自动调度机制、多粒度的资源配额管理能力。
Kubernetes 调度器是 Kubernetes 系统的核心组件之一,负责整个集群资源的调度。调度器的主要作用是根据特定的调度算法和策略,将 Pod 调度到最优的工作节点上面去,从而更加合理、更加充分地利用集群的资源。
k8s调度总览
在 Kubernetes 中,每个 Pod 都可能需要在特定的硬件资源上运行,例如特定的 CPU 和内存数量,或者特定的存储和网络配置。调度器需要将这些需求与集群的可用资源进行匹配,并选择最适合的节点来运行 Pod。
这个过程在我们看来好像比较简单,但在实际的生产环境中,需要考虑的问题就有很多了:
如何保证全部的节点调度的公平性?要知道并不是所有节点资源配置一定都是一样的
如何保证每个节点都能被分配资源?
集群资源如何能够被高效利用?
集群资源如何才能被最大化使用?
如何保证 Pod 调度的性能和效率?
用户是否可以根据自己的实际需求定制自己的调度策略?
等等。。。
2.k8s默认调度
2.2 简介
k8s原生的调度主要分为以下几个部分:
预选过程,过滤掉不满足条件的节点,这个过程称为
Predicates
(过滤)优选过程,对通过的节点按照优先级排序,称之为
Priorities
(打分)择优先级最高的节点,如果中间任何一步骤有错误,就直接返回错误
Predicates
阶段首先遍历全部节点,过滤掉不满足条件的节点,属于强制性
规则,这一阶段输出的所有满足要求的节点将被记录并作为第二阶段的输入,如果所有的节点都不满足条件,那么 Pod 将会一直处于 Pending 状态,直到有节点满足条件,在这期间调度器会不断的重试。
所以我们在部署应用的时候,如果发现有 Pod 一直处于 Pending 状态,那么就是没有满足调度条件的节点,这个时候可以去检查下节点资源是否可用。
Pending图
Priorities
阶段即再次对节点进行筛选,如果有多个节点都满足条件的话,那么系统会按照节点的优先级(priorites
)大小对节点进行排序,最后选择优先级最高的节点来部署 Pod 应用。
调度器监听 API Server 查看到还未被调度(bind)的 Pod 列表,循环遍历地为每个 Pod 尝试分配节点,这个分配过程就是我们上面提到的两个阶段。
首先,客户端通过 API Server 的 REST API 或者 kubectl 工具创建 Pod 资源
API Server 收到用户请求后,存储相关数据到 etcd 数据库中。
调度器监听 API Server 查看到还未被调度(bind)的 Pod 列表,循环遍历地为每个 Pod 尝试分配节点,调度器还要负责对调度器缓存 Scheduler Cache 进行更新,并以这个 cache 为参考信息,来提高整个调度流程的性能。
预选阶段(Predicates),过滤节点,调度器用一组规则过滤掉不符合要求的 Node 节点,比如 Pod 设置了资源的 request,那么可用资源比 Pod 需要的资源少的主机显然就会被过滤掉
优选阶段(Priorities),为节点的优先级打分,将上一阶段过滤出来的 Node 列表进行打分,调度器会考虑一些整体的优化策略,比如把 Deployment 控制的多个 Pod 副本尽量分布到不同的主机上,使用最低负载的主机等等策略
经过上面的阶段过滤后选择打分最高的 Node 节点和 Pod 进行
binding
操作( 赋值pod 的 spec.NodeName),然后将结果存储到 etcd 中 最后被选择出来的 Node 节点对应的 kubelet 去执行创建 Pod 的相关操作(当然也是 watch APIServer 发现的)。
K8s 自带的的资源调度器,有一个明显的特点是:依次调度每个容器。但在 AI 训练或者大数据,这种必须多个容器并行执行的情况下,容器依次调度是无法满足需要的,因为这些计算任务包含的容器们想要的是,要么同时都成功,要么就都别执行。
(1)总体资源需求<集群资源的时候, K8s 自带调度器可以完全胜任,(场景1)。
(2)总体资源需求>集群资源的时候,K8s 自带调度器会因为随机依次调度容器,使得部分容器无法调度,从而导致业务占着资源又不能开始计算,死锁着浪费资源(场景2)。
很显然在当前需求算力膨胀的年代 场景2是常态化。
2.1局限性
K8s 默认调度器策略在小规模集群下有着优异表现,但是随着业务量级的增加以及业务种类的多样性变化,默认调度策略则逐渐显露出局限性:调度维度较少,无并发,存在性能瓶颈,以及调度器越来越复杂。
高峰期的节点负载不均匀
默认调度器,参考的是 workload 的 request 值,如果我们针对 request 设置的过高,会带来资源浪费;过低则有可能带来高峰期 CPU 不均衡差异严重的情况;使用亲和策略虽然可以一定程度避免这种,但是需要频繁填充大量的策略,维护成本就会非常大。而且服务的 request 往往不能体现服务真实的负载,带来差异误差。而这种差异误差,会在高峰时体现到节点负载不均上。
调度维度多样化
随着业务越来越多样,需要加入更多的调度维度,比如日志。由于采集器不可能无限速率采集日志且日志采集是基于节点维度。需要平衡日志采集速率,各个节点差异不可过大。部分服务 CPU 使用量一般但是日志输出量很大,而日志并不属于默认调度器决策的一环,所以当这些日志量很大的多个服务 pod 在同一个节点上时,该机器上的日志上报就有可能出现部分延迟。
大批量服务扩缩带来的调度时延
随着业务复杂度进一步上升,在高峰时段出现,会有大量定时任务和集中大量弹性扩缩,大批量(上千 POD)同时调度导致调度时延上涨,这两者对调度时间比较敏感,尤其对于定时任务来说,调度延时的上涨会被明显感知到,原因是 K8s 调度 pod 本身是对集群资源的分配,反应在调度流程上则是预选和打分阶段是顺序进行的。如此一来,当集群规模大到一定程度时,大批量更新就会出现可感知的 pod 调度延迟。
在 Kubernetes 1.12 版本之前,kube-scheduler 会检查集群中所有节点的可调度性,并且给可调度节点打分。Kubernetes 1.12 版本添加了一个新的功能,允许调度器在找到一定数量的可调度节点之后就停止继续寻找可调度节点。该功能能提高调度器在大规模集群下的调度性能,这个数值是集群规模的百分比,这个百分比通过 percentageOfNodesToScore
参数来进行配置,其值的范围在 1 到 100 之间,最大值就是 100%,如果设置为 0 就代表没有提供这个参数配置。Kubernetes 1.14 版本又加入了一个特性,在该参数没有被用户配置的情况下,调度器会根据集群的规模自动设置一个集群比例,然后通过这个比例筛选一定数量的可调度节点进入打分阶段。该特性使用线性公式计算出集群比例,比如100个节点的集群下会取 50%,在 5000节点的集群下取 10%,这个自动设置的参数的最低值是 5%,换句话说,调度器至少会对集群中 5% 的节点进行打分,除非用户将该参数设置的低于 5。
可以看出机器学习、批处理任务和流式任务等工作负载的运行从 Kubernetes 诞生第一天起到今天都不是它的强项
2.2 自定义调度器
一般来说,我们有4种扩展 Kubernetes 调度器的方法。
一种方法就是直接 clone 官方的 kube-scheduler 源代码,在合适的位置直接修改代码,然后重新编译运行修改后的程序,当然这种方法是最不建议使用的,也不实用,因为需要花费大量额外的精力来和上游的调度程序更改保持一致。
第二种方法就是和默认的调度程序一起运行独立的调度程序,默认的调度器和我们自定义的调度器可以通过 Pod 的
spec.schedulerName
来覆盖各自的 Pod,默认是使用 default 默认的调度器,但是多个调度程序共存的情况下也比较麻烦,比如当多个调度器将 Pod 调度到同一个节点的时候,可能会遇到一些问题,因为很有可能两个调度器都同时将两个 Pod 调度到同一个节点上去,但是很有可能其中一个 Pod 运行后其实资源就消耗完了,并且维护一个高质量的自定义调度程序也不是很容易的,因为我们需要全面了解默认的调度程序,整体 Kubernetes 的架构知识以及各种 Kubernetes API 对象的各种关系或限制。第三种方法是调度器扩展程序,这个方案目前是一个可行的方案,可以和上游调度程序兼容,所谓的调度器扩展程序其实就是一个可配置的 Webhook 而已,里面包含
过滤器
和优先级
两个端点,分别对应调度周期中的两个主要阶段(过滤和打分)。第四种方法是通过调度框架(Scheduling Framework),Kubernetes v1.15 版本中引入了可插拔架构的调度框架,使得定制调度器这个任务变得更加的容易。调库框架向现有的调度器中添加了一组插件化的 API,该 API 在保持调度程序“核心”简单且易于维护的同时,使得大部分的调度功能以插件的形式存在,而且在我们现在的 v1.16 版本中上面的
调度器扩展程序
也已经被废弃了,所以以后调度框架才是自定义调度器的核心方式。
调度框架
调度框架定义了一组扩展点,用户可以实现扩展点定义的接口来定义自己的调度逻辑(我们称之为扩展),并将扩展注册到扩展点上,调度框架在执行调度工作流时,遇到对应的扩展点时,将调用用户注册的扩展。调度框架在预留扩展点时,都是有特定的目的,有些扩展点上的扩展可以改变调度程序的决策方法,有些扩展点上的扩展只是发送一个通知。
1.QueueSort
扩展用于对 Pod 的待调度队列进行排序,以决定先调度哪个 Pod,QueueSort
扩展本质上只需要实现一个方法 Less(Pod1, Pod2)
用于比较两个 Pod 谁更优先获得调度即可,同一时间点只能有一个 QueueSort
插件生效。
2.Pre-filter
扩展用于对 Pod 的信息进行预处理,或者检查一些集群或 Pod 必须满足的前提条件,如果 pre-filter
返回了 error,则调度过程终止。
3.Filter
扩展用于排除那些不能运行该 Pod 的节点,对于每一个节点,调度器将按顺序执行 filter
扩展;如果任何一个 filter
将节点标记为不可选,则余下的 filter
扩展将不会被执行。调度器可以同时对多个节点执行 filter
扩展。
4.Post-filter
是一个通知类型的扩展点,调用该扩展的参数是 filter
阶段结束后被筛选为可选节点的节点列表,可以在扩展中使用这些信息更新内部状态,或者产生日志或 metrics 信息。
5.Scoring
扩展用于为所有可选节点进行打分,调度器将针对每一个节点调用 Soring
扩展,评分结果是一个范围内的整数。在 normalize scoring
阶段,调度器将会把每个 scoring
扩展对具体某个节点的评分结果和该扩展的权重合并起来,作为最终评分结果。
6.Normalize scoring
扩展在调度器对节点进行最终排序之前修改每个节点的评分结果,注册到该扩展点的扩展在被调用时,将获得同一个插件中的 scoring
扩展的评分结果作为参数,调度框架每执行一次调度,都将调用所有插件中的一个 normalize scoring
扩展一次。
7.Reserve
是一个通知性质的扩展点,有状态的插件可以使用该扩展点来获得节点上为 Pod 预留的资源,该事件发生在调度器将 Pod 绑定到节点之前,目的是避免调度器在等待 Pod 与节点绑定的过程中调度新的 Pod 到节点上时,发生实际使用资源超出可用资源的情况。(因为绑定 Pod 到节点上是异步发生的)。这是调度过程的最后一个步骤,Pod 进入 reserved 状态以后,要么在绑定失败时触发 Unreserve 扩展,要么在绑定成功时,由 Post-bind 扩展结束绑定过程
8.Permit
扩展用于阻止或者延迟 Pod 与节点的绑定。Permit 扩展可以做下面三件事中的一项:
approve(批准):当所有的 permit 扩展都 approve 了 Pod 与节点的绑定,调度器将继续执行绑定过程
deny(拒绝):如果任何一个 permit 扩展 deny 了 Pod 与节点的绑定,Pod 将被放回到待调度队列,此时将触发
Unreserve
扩展wait(等待):如果一个 permit 扩展返回了 wait,则 Pod 将保持在 permit 阶段,直到被其他扩展 approve,如果超时事件发生,wait 状态变成 deny,Pod 将被放回到待调度队列,此时将触发 Unreserve 扩展
9.Pre-bind
扩展用于在 Pod 绑定之前执行某些逻辑。例如,pre-bind 扩展可以将一个基于网络的数据卷挂载到节点上,以便 Pod 可以使用。如果任何一个 pre-bind
扩展返回错误,Pod 将被放回到待调度队列,此时将触发 Unreserve 扩展。
10.Bind
扩展用于将 Pod 绑定到节点上。
11.Post-bind
是一个通知性质的扩展。
12.Unreserve
是一个通知性质的扩展,如果为 Pod 预留了资源,Pod 又在被绑定过程中被拒绝绑定,则 unreserve 扩展将被调用。Unreserve 扩展应该释放已经为 Pod 预留的节点上的计算资源。在一个插件中,reserve 扩展和 unreserve 扩展应该成对出现。
源码 pkg/scheduler/framework/v1alpha1/interface.go
3.volcano调度
3.1 简介
针对云原生场景下的高性能应用场景,华为云容器团队推出了 Volcano 项目。Volcano 是基于 Kubernetes 构建的一个通用批量计算系统,它弥补了 Kubernetes 在“高性能应用”方面的不足,支持 TensorFlow、Spark、MindSpore 等多个领域框架,帮助用户通过 Kubernetes 构建统一的容器平台。Volcano 作为容器调度系统,不仅包括了作业调度,还包含了作业生命周期管理、多集群调度、命令行、数据管理、作业视图及硬件加速等功能。
调度算法的多样性
调度性能的高效性
无缝对接主流计算框架
对异构设备的支持
同时,Volcano继承了Kubernetes接口的设计风格和核心概念。用户可以在充分享受Volcano的高效性和便利性的同时不用改 变任何以前使用Kubernetes的习惯
Volcano Scheduler是负责Pod调度的组件,它由一系列action和plugin组成。action定义了调度各环节中需要执行的动作;plugin根据不同场景提供了action 中算法的具体实现细节。Volcano scheduler具有高度的可扩展性,您可以根据需要实现自己的action和plugin。
Scheduler支持动态配置和加载。左边为apiserver,右边为整个Scheduler。apiserver里有Job、Pod、Pod Group;Scheduler分为三部分,第一层为Cache,中间层为整个调度的过程,右边是以插件形式存在的调度算法。
Cache会将apiserver里创建的Pod、Pod Group这些信息存储并加工为Jobinfors。中间层的OpenSession会从Cache里拉取Pod、Pod Group,同时将右边的算法插件一起获取,从而运行它的调度工作。
Volcano scheduler的工作流程如下:
客户端提交的Job被scheduler观察到并缓存起来。
周期性的开启会话,一个调度周期开始。
将没有被调度的Job发送到会话的待调度队列中。
遍历所有的待调度Job,按照定义的次序依次执行enqueue、allocate、preempt、reclaim、backfill等动作,为每个Job找到一个最合适的节点。将该Job 绑定到这个节点。action中执行的具体算法逻辑取决于注册的plugin中各函数的实现。
关闭本次会话。
比如 gang插件的实现源码
同时 volcano也增加了容器的运行状态
蓝色部分为K8s自带的状态;绿色部分是session级别的状态;黄色部分的状态是放在Cache内的
3.2 核心组件
Queue
queue是容纳一组podgroup的队列,也是该组podgroup获取集群资源的划分依据。用户可以通过queue分割集群资源,达到平衡任务优先级和集群资源的目的。Queue是集群级别,它可以跨越多个namespace,不同的namespace可以共用同一个queue,不同的pod也可以共享同一个queue下的资源。Queue根据自身配置和集群现状分得资源配额,其下的Job根据优先级逐个获取queue的资源,当queue下的资源被占满后,其下未获取到集群资源的Pod将无法再继续被调度,直到queue下已经在运行的Pod结束并释放资源。
apiVersion: scheduling.volcano.sh/v1beta1
kind: Queue
metadata:
creationTimestamp: "2020-08-10T11:54:36Z"
generation: 1
name: default
resourceVersion: "559"
selfLink: /apis/scheduling.volcano.sh/v1beta1/queues/default
uid: 14082e4c-bef6-4248-a414-1e06d8352bf0
spec:
reclaimable: true
weight: 1
capability:
cpu: "4"
memory: "4096Mi"
status:
state: Open
weight
weight表示该queue在集群资源划分中所占的相对比重,该queue应得资源总量为 (weight/total-weight) * total-resource。其中, total-weight表示所有的queue的weight总和,total-resource表示集群的资源总量。weight是一个软约束,取值范围为[1, 2^31-1]
volcano启动后,会默认创建名为default的queue,weight为1。后续下发的job,若未指定queue,默认属于default queue。且可临时使用全部的资源
capability
capability表示该queue内所有podgroup使用资源量之和的上限,它是一个硬约束 (pod申请资源超过限制 pending)
reclaimable
reclaimable表示该queue在资源使用量超过该queue所应得的资源份额时,是否允许其他queue回收该queue使用超额的资源,默认值为true
PodGroup
podgroup是一组强关联pod的集合,它是volcano-scheduler调度过程中的一个单位,主要用于批处理工作负载场景,比如Tensorflow中的一组ps和worker。它是volcano自定义资源类型。
apiVersion: scheduling.volcano.sh/v1beta1
kind: PodGroup
metadata:
creationTimestamp: "2020-08-11T12:28:55Z"
generation: 5
name: test
namespace: default
ownerReferences:
- apiVersion: batch.volcano.sh/v1alpha1
blockOwnerDeletion: true
controller: true
kind: Job
name: test
uid: 028ecfe8-0ff9-477d-836c-ac5676491a38
resourceVersion: "109074"
selfLink: /apis/scheduling.volcano.sh/v1beta1/namespaces/default/podgroups/job-1
uid: eb2508f5-3349-439c-b94d-4ac23afd71ff
spec:
minMember: 1
minResources:
cpu: "3"
memory: "2048Mi"
priorityClassName: high-prority
queue: default
status:
conditions:
- lastTransitionTime: "2020-08-11T12:28:57Z"
message: '1/0 tasks in gang unschedulable: pod group is not ready, 1 minAvailable.'
reason: NotEnoughResources
status: "True"
transitionID: 77d5be3f-6169-4f86-8e65-0bdc621ce983
type: Unschedulable
- lastTransitionTime: "2020-08-11T12:29:02Z"
reason: tasks in gang are ready to be scheduled
status: "True"
transitionID: 54514401-5c90-4b11-840d-90c1cda93096
type: Scheduled
phase: Running
running: 1
minMember
minMember表示该podgroup下最少需要运行的pod或任务数量。如果集群资源不满足miniMember数量任务的运行需求,调度器将不会调度任何一个该podgroup 内的任务。
在某些场景下,可能会只需要某个任务的子任务运行达到一定的数量,即可认为本次任务可以运行,如机器学习训练。这种情况下适合使用minMember字段。
queue
queue表示该podgroup所属的queue。queue必须提前已创建且状态为open。
priorityClassName
priorityClassName表示该podgroup的优先级,用于调度器为该queue中所有podgroup进行调度时进行排序。system-node-critical和system-cluster-critical是2个预留的值,表示最高优先级。不特别指定时,默认使用default优先级或zero优先级。
minResources
minResources表示运行该podgroup所需要的最少资源。当集群可分配资源不满足minResources时,调度器将不会调度任何一个该podgroup内的任务。
phase
phase表示该podgroup当前的状态。
conditions
conditions表示该podgroup的具体状态日志,包含了podgroup生命周期中的关键事件。
running
running表示该podgroup中当前处于running状态的pod或任务的数量。
succeed
succeed表示该podgroup中当前处于succeed状态的pod或任务的数量。
failed
failed表示该podgroup中当前处于failed状态的pod或任务的数量。
VolcanoJob
Volcano Job,简称vcjob,是Volcano自定义的Job资源类型。区别于Kubernetes Job,vcjob提供了更多高级功能,如可指定调度器、支持最小运行pod数、 支持task、支持生命周期管理、支持指定队列、支持优先级调度等。Volcano Job更加适用于机器学习、大数据、科学计算等高性能计算场景。
apiVersion: batch.volcano.sh/v1alpha1
kind: Job
metadata:
name: test-job
spec:
minAvailable: 3
schedulerName: volcano
priorityClassName: high-priority
policies:
- event: PodEvicted
action: RestartJob
plugins:
ssh: []
env: []
svc: []
maxRetry: 5
queue: default
volumes:
- mountPath: "/myinput"
- mountPath: "/myoutput"
volumeClaimName: "testvolumeclaimname"
volumeClaim:
accessModes: [ "ReadWriteOnce" ]
storageClassName: "my-storage-class"
resources:
requests:
storage: 1Gi
tasks:
- replicas: 6
name: "default-nginx"
template:
metadata:
name: web
spec:
containers:
- image: nginx
imagePullPolicy: IfNotPresent
name: nginx
resources:
requests:
cpu: "1"
restartPolicy: OnFailure
schedulerName
schedulerName表示该job的pod所使用的调度器,默认值为volcano,也可指定为default-scheduler。它也是tasks.template.spec.schedulerName的默认值。
minAvailable
minAvailable表示运行该job所要运行的最少pod数量。只有当job中处于running状态的pod数量不小于minAvailable时,才认为该job运行正常。
volumes
volumes表示该job的挂卷配置。volumes配置遵从kubernetes volumes配置要求。
tasks.replicas
tasks.replicas表示某个task pod的副本数。
tasks.template
tasks.template表示某个task pod的具体配置定义。
tasks.policies
tasks.policies表示某个task的生命周期策略。
policies
policies表示job中所有task的默认生命周期策略,在tasks.policies不配置时使用该策略。
plugins
plugins表示该job在调度过程中使用的插件。
queue
queue表示该job所属的队列。
priorityClassName
priorityClassName表示该job优先级,在抢占调度和优先级排序中生效。
maxRetry
maxRetry表示当该job可以进行的最大重启次数。
调度action
1.enqueue
Enqueue action负责通过一系列的过滤算法筛选出符合要求的待调度任务并将它们送入待调度队列。经过这个action,任务的状态将由pending变为inqueue。
Enqueue action是调度流程中的准备阶段,只有当集群资源满足作业调度的最小资源请求,作业状态才可由”pending”变为”enqueue”。这样在AI/MPI/HPC这样的集群资源可能不足的高负荷的场景下。过滤掉集群空闲资源不能满足的作业,从而避免这些不可能调度的job造成后面的allocate、preempt动作空跑,影响调度器性能
2.allocate
Allocate action负责通过一系列的预选和优选算法筛选出最适合的节点。
Allocate action遵循commit机制,当一个Pod的调度请求得到满足后,最终并不一定会为该Pod执行绑定动作,这一步骤还取决于Pod所在Job的gang约束是否得到满足。只有Pod所在Job的gang约束得到满足,Pod才可以被调度,否则,Pod不能够被调度。
在集群混合业务场景中,Allocate的预选部分能够将特定的业务(AI、大数据、HPC、科学计算)按照所在namespace快速筛选、分类,对特定的业务进行快速、集中的调度。在Tensorflow、MPI等复杂计算场景中,单个作业中会有多个任务,Allocate action会遍历job下的多个task分配优选,为每个task找到最合适的node。
3.preempt
Preempt action负责根据优先级规则为同一队列中高优先级任务执行抢占调度。
用于处理高优先级pod的调度问题。当集群比较繁忙,集群下已经没有空闲资源可供新Pod使用,此时,如果有更高优先级的Pod下发到集群中,那么volcano-scheduler会尝试驱逐这个集群中已经处于运行中的并且优先级比待调度Pod低的Pod,希望通过驱逐低优先级的Pod,使更高优先级的Pod得以调度。当然考虑到驱逐将可能对已经处于运行中的任务有破坏性的影响,对于一个Pod是否可以驱逐其他的Pod,或者一个Pod是否可以被其他的pod驱逐都有严格的限制。比如在一个Pod是否可以驱逐其他Pod的约束中,只有当Pod驱逐了其他Pod后,这个Pod所在Job的gang约束可以得到满足,Pod才可以驱逐其他Pod。
Queue内job抢占:一个公司中多个部门共用一个集群,每个部门可以映射成一个Queue,不同部门之间的资源不能互相抢占,这种机制能够很好的保证部门资源的隔离性。多业务类型混合场景中,基于Queue的机制满足了一类业务对于某一类资源的集中诉求,也能够兼顾集群的弹性。例如,AI业务组成的queue对集群GPU占比90%,其余图像类处理的业务组成的queue占集群GPU10%。前者占用了集群绝大部分GPU资源但是依然有一小部分资源可以处理其余类型的业务。
Job内task抢占:同一Job下通常可以有多个task,例如复杂的AI应用场景中,tf-job内部需要设置一个ps和多个worker,Preempt action就支持这种场景下多个worker之间的抢占。
4.reclaim
Reclaim action负责当一个新的任务进入待调度队列,但集群资源已不能满足该任务所在队列的要求时,根据队列权重回收队列应得资源。
queue在瓜分集群资源时,只会考虑现有集群下有任务在运行或待调度的queue。当集群中现有的queue瓜分完集群资源后,集群下新增了queue,这个queue将希望得到集群资源。集群资源划分需要打破原来的形势,建立新的分割形势。当这个新加入的queue分割到集群配额后,部分原有queue的配额将可能会降低。然而,新queue分到配额后,并不表明queue下的Pod可以正常调度了,因为queue在此时分到的配额只是使用集群资源的上限,并不是使用集群的担保。假如此时旧有queue下的Pod已经占尽了集群资源,尽管此时这些queue下pod的资源使用量已经大于queue分得的配额,但是因为这些Pod已经处于运行中,并不会主动释放资源。这时候,新的queue虽然有配额,但是苦于集群下没有资源,queue下的Pod仍然无法调度。这个时候就需要reclaim action在不同的queue之间做资源均衡。“reclaim” action尝试驱逐那些资源使用量已经大于配额的queue下的Pod,并把这部分资源分配给资源使用量还没有得到满足的queue。同样在Pod驱逐过程中,对于是否可以驱逐和是否可以被驱逐都有严格的定义。
5.backfill
backfill action负责将处于pending状态的任务尽可能的调度下去以保证节点资源的最大化利用。
在一个集群中,主要资源被“胖业务”占用,例如AI模型的训练。Backfill action让集群可以快速调度诸如单次AI模型识别、小数据量通信的“小作业” 。Backfill能够提高集群吞吐量,提高资源利用率。
调度插件
Gang
Gang调度策略是volcano-scheduler的核心调度算法之一,它满足了调度过程中的“All or nothing”的调度需求,避免Pod的任意调度导致集群资源的浪费。具体算法是,观察Job下的Pod已调度数量是否满足了最小运行数量,当Job的最小运行数量得到满足时,为Job下的所有Pod执行调度动作,否则,不执行。
基于容器组概念的Gang调度算法十分适合需要多进程协作的场景。AI场景往往包含复杂的流程,Data Ingestion、Data Analysts、Data Splitting、Trainer、Serving、Logging等,需要一组容器进行协同工作,就很适合基于容器组的Gang调度策略。MPI计算框架下的多线程并行计算通信场景,由于需要主从进程协同工作,也非常适合使用Gang调度策略。容器组下的容器高度相关也可能存在资源争抢,整体调度分配,能够有效解决死锁。
在集群资源不足的场景下,gang的调度策略对于集群资源的利用率的提升是非常明显的。
Binpack
binpack调度算法的目标是尽量把已有的节点填满(尽量不往空白节点分配)。具体实现上,binpack调度算法是给可以投递的节点打分,分数越高表示节点的资源利用率越高。binpack算法能够尽可能填满节点,将应用负载靠拢在部分节点,这非常有利于K8S集群节点的自动扩缩容功能。
binpack算法对能够尽可能填满节点的小作业有利。例如大数据场景下的单次查询作业、电商秒杀场景订单生成、AI场景的单次识别作业以及互联网高并发的服务场景等。这种调度算法能够尽可能减小节点内的碎片,在空闲的机器上为申请了更大资源请求的Pod预留足够的资源空间,使集群下空闲资源得到最大化的利用。
Priority
Priority plugin提供了job、task排序的实现,以及计算牺牲作业的函数preemptableFn。job的排序根据priorityClassName,task的排序依次根据priorityClassName、createTime、id。
当集群运行了多个Job,但资源不足,并且每个Job下有不等数量的Pod等待被调度的时候,如果使用Kubernetes默认调度器,那么最终,具有更多Pod数量的Job将分得更多的集群资源。在这种情况下,volcano-scheduler提供算法支持不同的Job以fair-share的形式共享集群资源。
Priority plugin能够让用户自定义job、task优先级,根据自己的需求在不同层次来定制调度策略。根据job的priorityClassName在应用层面进行优先级排序,例如集群中有金融场景、物联网监控场景等需要较高实时性的应用,Priority plugin能够保证其优先得到调度。
DRF
DRF调度算法的全称是Dominant Resource Fairness,是基于容器组Dominant Resource的调度算法。volcano-scheduler观察每个Job请求的主导资源,并将其作为对集群资源使用的一种度量,根据Job的主导资源,计算Job的share值,在调度的过程中,具有较低share值的Job将具有更高的调度优先级。这样能够满足更多的作业,不会因为一个胖业务,饿死大批小业务。DRF调度算法能够确保在多种类型资源共存的环境下,尽可能满足分配的公平原则。
DRF调度算法优先考虑集群中业务的吞吐量,适用单次AI训练、单次大数据计算以及查询等批处理小业务场景。
Proportion
Proportion调度算法是使用queue的概念,用来控制集群总资源的分配比例。每一个queue分配到的集群资源比例是一定的。举例来说,有3个团队,共享一个集群上的资源池:A团队最多使用总集群的40%,B团队最多使用30%,C团队最多使用30%。如果投递的作业量超过团队最大可用资源,就需要排队。
Task-topology
Task-topology算法是一种根据Job内task之间亲和性和反亲和性配置计算task优先级和Node优先级的算法。通过在Job内配置task之间的亲和性和反亲和性策略,并使用task-topology算法,可优先将具有亲和性配置的task调度到同一个节点上,将具有反亲和性配置的Pod调度到不同的节点上。
3.3 应用场景
场景一:弹性调度
推理作业A(优先级高)抢占训练作业B(优先级低)保证SLA
空闲资源增多后调度训练作业B
场景二:CPU拓扑感知调度
当节点运行许多CPU绑定的Pod时,工作负载可以迁移到不同的CPU核心,这取决于Pod是否被限制以及调度时哪些CPU核心可用。许多工作负载对此迁移不敏感,因此在没有任何干预的情况下工作正常。但是,在CPU缓存亲和性和调度延迟显著影响工作负载性能的工作负载中,如果CPU是从不同的NUMA节点分配的,会导致额外的延迟。因此kubelet允许使用拓扑管理器(Topology Manager)替代CPU管理策略来确定节点的分配。
避免将Pod调度到NUMA拓扑不匹配的节点。
将Pod调度到NUMA拓扑的最佳节点
场景三:在离线作业混部 (EasyDLOCR平台)
在线服务有峰谷,资源请求根据峰值设置
有些用户不知道其服务的资源使用情况,是否有请求过多的资源
高分配率,低利用率,(在线作业往往按照最高的峰值来分配资源),CPU平均利用率小于15%
场景四:为Spark、tensorflow、paddle提供批量调度
PS-worker模型:Parameter Server执行模型相关业务,Work Server训练相关业务,推理计算、梯度计算等[1]。
4.volcano实践
直接运行
kubectl apply -f
https://raw.githubusercontent.com/volcano-sh/volcano/master/installer/volcano-development.yaml
会在集群内生成默认的volcano空间
kubectl get all -n volcano-system
4.1 deployment
Queue
apiVersion: scheduling.volcano.sh/v1beta1
kind: Queue
metadata:
name: test
spec:
weight: 1
reclaimable: false
capability:
cpu: 2
deployment 指定scheduling.volcano.sh/queue-name: test
apiVersion: apps/v1
kind: Deployment
metadata:
name: deploy-with-volcano
labels:
app: nginx
spec:
replicas: 1
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
annotations:
#create test queue and use this annotation
#to make deployment be scheduled into test queue
scheduling.volcano.sh/queue-name: test
spec:
# set spec.schedulerName to 'volcano' instead of
# 'default-scheduler' for deployment.
schedulerName: volcano
containers:
- name: nginx
image: nginx:latest
ports:
- containerPort: 80
resources:
requests:
cpu: 300m
查看队列的状态
查看podgroup的状态
查看pod状态
4.2 MindSpore-cpu
apiVersion: batch.volcano.sh/v1alpha1
kind: Job
metadata:
name: mindspore-cpu
spec:
minAvailable: 1
schedulerName: volcano
policies:
- event: PodEvicted
action: RestartJob
plugins:
ssh: []
env: []
svc: []
maxRetry: 5
queue: default
tasks:
- replicas: 8
name: "pod"
template:
spec:
containers:
- command: ["/bin/bash", "-c", "python /tmp/lenet.py"]
image: lyd911/mindspore-cpu-example:0.2.0
imagePullPolicy: IfNotPresent
name: mindspore-cpu-job
resources:
limits:
cpu: "1"
requests:
cpu: "1"
restartPolicy: OnFailure
4.3 MindSpore-gpu
apiVersion: batch.volcano.sh/v1alpha1
---
因为使用集群为实验集群,没办法安装gpu插件,这里没有做gpu、gpu虚拟化的验证 ,详细的gpu使用样例可以参考https://github.com/volcano-sh/devices
apiVersion: v1
kind: Job
metadata:
name: mindspore-gpu
spec:
minAvailable: 3
schedulerName: volcano
plugins:
ssh: []
svc: []
tasks:
- replicas: 1
name: mpimaster
template:
spec:
containers:
- command:
- /bin/bash
- -c
- |
mkdir -p /var/run/sshd; /usr/sbin/sshd;
MPI_HOST=`cat /etc/volcano/mpiworker.host | tr "\n" ","`;
sleep 10;
mpiexec --allow-run-as-root --host ${MPI_HOST} -np 2 --prefix /usr/local/openmpi-3.1.5 python /tmp/gpu-test.py;
sleep 3600;
image: lyd911/mindspore-gpu-example:0.2.0
name: mpimaster
ports:
- containerPort: 22
name: mpijob-port
workingDir: /home
restartPolicy: OnFailure
- replicas: 2
name: mpiworker
template:
spec:
containers:
- command:
- /bin/bash
- -c
- |
mkdir -p /var/run/sshd; /usr/sbin/sshd -D;
image: lyd911/mindspore-gpu-example:0.2.0
name: mpiworker
resources:
limits:
nvidia.com/gpu: "1"
ports:
- containerPort: 22
name: mpijob-port
workingDir: /home
restartPolicy: OnFailure
kind: Pod
metadata:
name: gpu-pod1
spec:
schedulerName: volcano
containers:
- name: cuda-container
image: nvidia/cuda:9.0-devel
resources:
limits:
volcano.sh/gpu-memory: 1024 # requesting 1024MB GPU memory
---
apiVersion: v1
kind: Pod
metadata:
name: gpu-pod2
spec:
schedulerName: volcano
containers:
- name: cuda-container
image: nvidia/cuda:9.0-devel
resources:
limits:
volcano.sh/gpu-memory: 1024 # requesting 1024MB GPU memory
https://github.com/volcano-sh/volcano/tree/master/example