kubernetes之Gang scheduling
此篇为《Gang scheduling in Kubernetes》文档翻译,个人翻译。
动机
Kubernetes
目前已经成为主流的容器平台编排方案,在服务与存储层面已经取得成功性的大规模应用了,并且原生k8s
支持Spark
。同时社区也在致力于将ML
机器学习框架运行在k8s
之上,比如kubeflow/tf-operator,在整合kube-arbitrator
期间,我们发现以下需求合并到default-scheduling
是更好的措施。
定义
Gang Scheduling
:任务实例要么全部或全部不执行。如果没有足够的资源来调度所有的pod
,那么改任务包含的pod
一个都不被调度,即All-or-none
模式。如果任务中有任何的pod
运行失败或未被调度,那么所有的pod
必须能够优雅的终止退出
使用场景
- 以
pod group
的方式调度pod
,all-or-nothing
模式(TensorFlow
,MPI
)
最初的需求来自于TesnsorFlow
(以及MPI
):运行Tensorflow/MPI
任务,一个任务重的所有task
单元必须保证能够一起启动,否则不启动其中任何一个task
。如果资源充足到满足运行所有的task
那么一切运行正常,但是在绝大部分情况下,尤其是在不具备任何保障的环境下,这种理想环境是不存在的。最坏的场景是由于”资源死锁”导致所有的任务都处于pending
状态:每个任务只启动了部分task
,该任务还在等他其他的task
启动运行。此种问题在联邦域或者跨域场景下会变的更加糟糕,具体细节可详见 - 以
pod group
的方式调度pod
,最小满足模式(Spark
)
与Tensorflow/MPI
不同的是,Spark
不需要所有的task(driver/executors)
都启动:driver
是必要的,但是对于excutors
来说是多多益善的(但是至少得有一个)。在此场景下,必须要求scheduler
能够保障“最小可用资源”(gang-scheduling
),另外其他task
还是能够以默认调度策略进行调度。 - 同一个
pod group
中支持不同pod
模板(Tensorflow/Spark/PMI
)。对于Tensorflow/Spark
任务,tasks
的镜像可能会不同。例如in tensorflow job
,master
和worker
就使用不同的镜像。spark
下的场景也类似,driver
和executor
也可能不尽相同。这种情况下就要求k8s
在gang-scheduling
调度模式中能够在同一pod group
中根据不同的pod
模板做出相应调度。
PS:目前来说,Spark on Kubernetes首先启动运行driver
,然后driver
再启动其对应的executors
,相关的讨论细节请详见
4. pod group
支持pod
顺序/优先级启动(Spark
)
由于#2(min availiable != desire
),#3(不同的tasks
),tasks
必须按序启动。拿Spark
举例,minAvailable=2
,desire=4
(其中包括driver
)。如果没有足够的资源去运行所有的4个tasks
,scheduler
能够确保driver
以及其对应的一个executor
能够启动运行,而不是两个exectors
(minAvaiable=2
)
5. 对其他特性具备可扩展性,e.g. IndexedJob(MPI)
gang-scheduling
能够满足不少工作场景,但是对于某些场景来说还不够完善,比如MPI
。这就要求gang-scheduling
对于一些特性的场景具备可扩展性。
开放问/答
gang-scheduling
只支持batch
工作需求?
答案是不,尽管大多数batch
工作需求是需要这样的特性,但是对于scheduler
来说,gang-scheduling
是一种“捆绑式,all-or-nothing
模式”,scheduler
并不清楚pod
中运行的是啥。gang-scheduler
必须具备其他特性以支持除batch
工作需求的其他场景。gang-scheduling
如何支持其他框架,比如Flink
,Storm
?[k82cn]
:我更倾向在资源规划阶段利用好资源可规划的这个阶段来达到scheduler
对这些框架的支持,资源规划其实是一个必不可少的共性阶段,在此之后提供给用户可配置能力来自定义使用。e.g.
(一种可自定义的tensorflow
控制器)1.gang-scheduling
:任务中的pods
将不会启动,除非有足够的资源满足2.job group
:框架(e.g. TensorFlow,Spark
)需要一组jobs
共同工作才能保证正常运行,比如Spark
中的driver
和worker
,TensorFlow
中的master
和workers
目标
- 定义管理或者调度批量工作场景的方式.比如
ML
- 在默认调度器中支持
gang-scheduling
Non-Goal
- 通过
Application API Object
原生支持job group
功能设计
默认调度器是在Pod
级别进行调度。当声明了新的Kind
,为了对scheduler
无任何变化,就必须将scheduler
从controller-manager
解耦出来。对于批量任务处理,支持Pods
(比如Job vs Tasks
)之间的关系的特性又是必要的,e.g. gang-shceduler
。如下的设计就是满足管理具备关联关系的pods
,并且gang-schedluing
能够工作其上。如下示例规范的定义的新的Kind
:QueueJob
就保证了能够管理pods
之间关系。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59type QueueJob struct {
metav1.TypeMeta
metav1.ObjectMeta
// cron job 的行为定义包括 minAvailable.
Spec QueueJobSpec
// 当前QueueJob状态
Status QueueJobStatus
}
// QueueJobSpec 描述 job 执行情况以及何时真正运行
type QueueJobSpec struct {
// 定义 QueueJob 的优先级。
//"system-node-critical" 和 "system-cluster-critical"是最高的优先级。
// 其他任何命名则必须创建一个用于此名称的PriorityClass 对象
//如果未定义,那么QueueJob的优先级将是默认值,如果未提供默认值的话将会是0。
// +optional
PriorityClassName string
// 优先级属性值。许多时候系统组件通过此属性来发现QueueJob的优先级。
// 开启优先级准入接入特性之后,它将阻止用户配置此属性,准入控制器将使用PriorityClassName来配置此属性。
// 参数值越大优先级越高
// +optional
Priority *int32
// 期望副本数
Replicas int32
// 可运行状态最少Pod数, 默认为nil.
MinAvailable *int32
// Pods控制器,如下合法属性值:
// k8s;
// customized
Controller string
// pod描述模板
Template PodTemplateSpec
}
// QueueJobStatus represents the current state of a QueueJob.
type QueueJobStatus struct {
// The number of actively running pods.
// +optional
Running int32
// The number of pods which reached phase Succeeded.
// +optional
Succeeded int32
// The number of pods which reached phase Failed.
// +optional
Failed int32
// The minimal available pods to run for this QueueJob
// +optional
MinAvailable int32
}
开放讨论:
Scheduler
默认调度器也会对QueueJob
进行监控,根据OwnerReference
和selector
在调度器的pending
缓存中“填入”Pods
。由于QueueJob
中的pods
是被批量调度的,存入pending queue
的是QueueJob
而不是其中的pods
。
开放讨论:
- 一种选择是使用不同的
queue
去存放不同的QueueJob
,scheduler
则必须根据资源使用情况进行速率处理- 另一种选择是在
multi-scheduler feature
中构建一个单独的库来实现此特性。
在scheduler loop
中,getNext
将获取下一个对象进行调度。scheduleOne
和scheduleBatch
分别作为调度Pod
和QueueJob
的使用依据。
scheduleOne
从FIFO/PriQueue
中获取pod
之后,scheduler
将调用scheduleOne
方法调度该pod
。当选择主机节点时触发抢占式多任务处理,scheduler
将尝试避免给QueueJob
中的pod
进行优先处理,类似于PDB(PodDisruptionBudget)
。若pod
被驱逐,QueueJob
的控制器将管理器生命周期,比如kill
掉整个的QueueJob(for MPI)
,或者重新创建那些被kill
掉的pods
以便再次调度(for Spark
)。
scheduleBatch
如果一个QueueJob
从FIFO/PriQueue
中获取,其中的pdos
将执行scheduleBatch
进行批量处理,辅助方法将对QueueJob
进行调度。scheduleBatch
也拥有三个主要阶段:
选取主机节点,#TODO「assume」,绑定。
选取主机节点
在此阶段,scheduler
将为批量任务中的pods
选择匹配QueueJob
中.spec.minAvailable
属性要求的节点。Running
和Pending
状态的pods
数量都被统计作为.spec.minAvailable
的考量。
pods
总数 <.spec.minAvailable
根据ResourceQuota
,QueueJobController
也许不会创建足够的(.spec.minAvailable
)pods
。该QueueJob
会被记录在backlog
中直到足够的pods
被创建。运行状态的
pods
<.spec.minAvailable
如果QueueJob
内没有足够的running
状态的pods
(包括#TODO「assumed」但没有启动的),scheduler
将尝试批量调度.spec.minAvailable-Running pods
数量的pods
:
a. 调用调度算法来为每个pod
获取distHost
,如果获取distHost
失败,scheduler
将会尝试选择一个pod
进行抢占式调度,#TODO the pod eviction will be triggered in batch until all pods got distHost or preemption candidate
b. 如果所有的pods
都获取到distHost
,将继续批量的将这些pods
绑定。
c. 否则,遗忘它们。Running pods
>.spec.minAvailable
如果QueueJob
已经有足够的running pods
,那些pending
状态的pods
将会一个个的被调度。如果支持QueueJob
抢占式调度的话则会调用scheduleOne
方法。
总之,当调度pod
失败,pods
的状况将会被更新.QueueJob
对应的控制器也将会管理QueueJob
的生命周期。
抢占式调度
gang-scheduling
支持QueueJob
级别的抢占式调度。在选取主机节点的阶段,如果无法将pod
调度到主机节点上,那么scheduler
将会尝试选取某个pod
进行抢占式调度。schedule
将不会触发驱逐直到所有的pod
获得disHost
或者具备优先抢占的“权利”。如果抢占式候选者不为空,则会批量的对pod
进行驱逐,同时#TODO「en-queue the QueueJob」。
在下个scheduling loop
时,QueueJob
将会被调度。
* 如果驱逐失败了,当再次调度QueueJob
时,scheduler
将会再次触发对pod
的驱逐。
* scheduler
不会绑定QueueJob
的pods
除非所有的pods
已经终止退出了,同时原本那些被pod
抢占的资源将会被释放,以供待调度的QueueJob
使用。只有优先级较高的pod
才能够使用空闲资源,并且触发QueueJob
抢占式调度。
资源“饥饿”vs
空闲
由于QueueJob
是批量的关联多个pods
,达到QueueJob
对资源需求“满意”是常见的场景需求,常见的两种如下:
- 保持资源空闲除非搞优先级的
QueueJbo
获得足够的资源 - “回填”对于以及满足它们资源的那些低优先级/同等优先级的
QueueJob
假想
在选取主机节点阶段,scheduler
将假设pod
已经获取到distHost
或者成为优先候选者。如若发生错误异常,将忽略pods
包括那些QueueJob
中已经被消费调度的。pods
的状态也会得到及时的更新。
绑定
过了“消费”pods
阶段,pods
已经具备被绑定的前提。pod
将会被一个个的被绑定。如果其中任何一个失败了,scheduler
会及时更新pod
状态,会取消之后涉及到的pod
的绑定不包括已经完成绑定了的。剩下的工作就是QueueJob Controller
去管理它们的生命周期了,比如 对于MPI
任务来说,就是kill
掉整个的QueueJob
,而对于Spark
任务来说仅仅是重新创建那些失败的worker pod
。
Controller
QueueJobController
如若.spec.Controller
声明为k8s
,那么QueueJobController
将会按如下管理QueueJob
的生命周期:Pod/QueueJob
创建QueueJobController
会创建pods
达到.spec.minAvailable
的数量,并等待kube-scheduler
批量的将这些pods
进行调度。
#TODO
#待继续更新