GangScheduling
概览
Koord-dscheduler 提供了 Gang Scheduling 满足 All-or-Nothing 调度需求。用户可以声明最小资源集合数(resource-collection-minimum),只有当已经完成调度资源数(assigned-resources)超过前面声明当前最小资源集合数才能触发节点绑定。
同时提供 Strict
和 NonStrict
两个参数用于控制 resource-accumulation-process ,区别于其他社区方案将提供 two-level Gang 描述用于更好匹配真实场景。
动机
在 AI 场景中很多任务都需要使用 Gang scheduling,社区已经有很多相关实现,比如 Coscheduling
、 Vocalno
,设计过程中我们从社区项目中得到了很多灵感。
竞品对标
Coscheduling
Coscheduling
主要通过实现新型队列排序(queue-sort)接口以及其他方法将一组 Gang pod 尽量有序的出队。 举个🌰 ,我们有 10 个任务需要进行 Gang 调度,前面 5 个任务已经调度成功,此时第 6 个任务调度失败,Coscheduling
将会回滚前面 5 个已经完成调度的任务,同时会跳过后面 4 个待调度中的任务。Coscheduling
会简单得使用一个全局间隔时间作为 Gang 调度周期。该设计会带来两个问题:- 问题一,如果配置间隔太长会带来无效等待,如果太短会带来无效调度。
- 问题二,如果待调度分组任务很多,此时大概率会出现周期内无法完成调度,出现调度超时的情况。
对于上面的场景,我们的设计中称为
Strict
,此场景下调度会严格按照既定配置的周期时间进行工作。有些任务需要复杂的 Gang 要求。例如,一个任务有几个规则,每个规则都有几个 pod 以及自身的 Gang 条件,任务也需要不同的规则来组成不同的 GangGroups。 一个 GangGroup 中的所有 pod 只有在 GangGroup 中的所有规则都满足 Gang 条件后才触发绑定过程。上游标准的
Coscheduling
不能满足这个需求。
目标
定义 Gang 调度配置。
提供调度器插件实现 Gang 调度。
非目标/未来工作
- 提供使用
NonStrict
解决 Gang 资源死锁问题的能力。
方案
核心概念
Strict / NonStrict
Strict
模式,如果其中一个 pod 调度失败,当前调度周期内,其他已经调度成功的 pod 将会被取消调度,同时正在调度中的 pod 将会在 PreFilter 阶段被拒绝调度。
NonStrict
模式,如果其中一个 pod 调度失败,并不会影响其他 pod 参与调度,会继续累计已经被调度的 pod 直到符合 Gang 调度条件。此模式对于 pod 比较多的情况比较友好,但是会增加不同 Gang 调度之间资源死锁的风险。
举个🌰 ,如果当前资源配额为 10,此时用户提交三组 Gang 调度任务 pod 数都为 5,由于各种条件限制,Gang 调度 1/2/3 任务分别调度起来 pod 数量为 3/3/4, 此时当前资源组配额已经耗尽,不会有新的 pod 完成调度,三组 Gang 调度任务就会一直出于等待状态,这就是上面说到到资源死锁情况,目前还没有解决这个问题。
GangGroup
GangGroup
,有些任务需要复杂的 Gang 要求。例如,一个任务有几个规则,每个规则都有几个 pod 以及自身的 Gang 条件,任务也需要不同的规则来组成不同的 GangGroups。
一个 GangGroup 中的所有 pod 只有在 GangGroup 中的所有规则都满足 Gang 条件后才触发绑定过程。GangGroup
则允许我们将不同 Gangs 进行聚合。
After Gang
注意⚠️,如果满足 Gang 调度资源积累条件,随后一些 pod 在 binding 阶段失败,或者一些已经绑定的 pod 被抢占或者重新调度,这种情况下 Gang 的约束在资源重新分配过程中是否依然有效?
答案:应该有效。因为 Gang 的设计初衷要求所有 pod 需要同时被拉起,如果只有其中一些 pod 被拉起,那么后续操作继续执行 Gang 调度策略将失去意义。因此,一旦 Gang 策略已经满足,后续所有的资源分配将不受 Gang 规则约束,后续将使用默认调度进行 pod 调度。
WaitTime
WaitTime
自第一个 pod 进入 permit 阶段依赖的最大等待时间。如果 WaitTime
已经超时,调度器将会回滚所有已经调度完成的 pod,并且更新所有 pod annotation gang.scheduling.koordinator.sh/timeout=true
,调度器将不会再调度这些 pod。用户需要注意这种情况并及时删除此类 pod。
API
定义
我们设计的初衷是优化以及增强社区原有的 PodGroup
能力,所以我们的 PodGroup
定义会兼容社区设计。我们会提供通过使用更新 annotation 方式使用 Gang 调度特性。
CRD 方式
用户可以使用社区 PodGroup
CRD 声明 Gang:
type PodGroup struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec PodGroupSpec `json:"spec,omitempty"`
Status PodGroupStatus `json:"status,omitempty"`
}
type PodGroupSpec struct {
MinMember int32 `json:"minMember,omitempty"`
MinResources *v1.ResourceList `json:"minResources,omitempty"`
ScheduleTimeoutSeconds *int32 `json:"scheduleTimeoutSeconds,omitempty"`
}
Pod 需要添加 label pod-group.scheduling.sigs.k8s.io
来关联 PodGroup
配置。
同时,我们也可以使用以下可选配置:
gang.scheduling.koordinator.sh/total-number
gang.scheduling.koordinator.sh/mode
gang.scheduling.koordinator.sh/groups
gang.scheduling.koordinator.sh/name
配置 Gang 调度器名称, 名称需要符合 RFC 1123 规范。gang.scheduling.koordinator.sh/total-number
当前配置仅作用于Strict
模式, 详情请参考Data-Structure
部分。默认与gang.scheduling.koordinator.sh/min-available
一致。gang.scheduling.koordinator.sh/mode
选项Strict
或者NonStrict
。 默认配置为Strict
。gang.scheduling.koordinator.sh/groups
用于配置 GangGroups 名称。默认为空,表示不需要与其他资源合并到 GangGroups,同一个 GangGroups 的 Gangs 可以来自于不同的 namespace。
PodGroup
annotation 可以包含 gang.scheduling.koordinator.sh/total-number
, gang.scheduling.koordinator.sh/mode
, gang.scheduling.koordinator.sh/gang-groups
。
示例
基础 Gang 调度配置如下:
apiVersion: v1alpha1
kind: PodGroup
metadata:
creationTimestamp: "2022-07-11T18:26:33Z"
name: gang-a
namespace: default
spec:
minMember: 5
minResources:
cpu: "5"
memory: "2048Mi"
scheduleTimeoutSeconds: 600
创建一个任务包含两个策略:A 和 B,每个策略包含一些 pod。PodA 属于 roleA,PodB 属于 roleB。roleA、roleB 归属于同一个 GangGroup,示例如下:
apiVersion: v1alpha1
kind: PodGroup
metadata:
creationTimestamp: "2022-07-11T18:26:33Z"
name: gang-a
namespace: namespaceA
annotations:
gang.scheduling.koordinator.sh/total-number: 5
gang.scheduling.koordinator.sh/mode: Strict
gang.scheduling.koordinator.sh/groups: ["namespaceA/gang-a", "namespaceB/gang-b"]
spec:
minMember: 5
minResources:
cpu: "5"
memory: "2048Mi"
scheduleTimeoutSeconds: 600
注意:如果用户使用 CRD way
,需要集群管理员提前将 PodGroup 策略部署到集群,否则会出现带有 Gang 配置的 Pod 进行调度时,找不到对应的 Gang 策略 PodGroup 配置。
此外,从调度的角度来看,调度应该处理 Gang CRD 和 Pod 之间的任务顺序问题。 例如,如果 Pod 在 Gang CRD 之前到达调度,我们必须构建一个假 Gang 数据结构
临时收集所有相关的 Pod,需要暂停 Pod 的调度,直到从真正的 Gang CRD 解析配置。
Annotation 方式
gang.scheduling.koordinator.sh/name
gang.scheduling.koordinator.sh/min-available
以上配置为必填,同时我们兼容社区 annotation pod-group.scheduling.sigs.k8s.io
, pod-group.scheduling.sigs.k8s.io/name
以及 pod-group.scheduling.sigs.k8s.io/min-available
。
此外,我们还支持以下可选配置:
gang.scheduling.koordinator.sh/waiting-time
gang.scheduling.koordinator.sh/total-number
gang.scheduling.koordinator.sh/mode
gang.scheduling.koordinator.sh/groups
gang.scheduling.koordinator.sh/waiting-time
自第一个 pod 进入 permit 阶段依赖的最大等待时间。默认值可以在全局配置中设置。gang.scheduling.koordinator.sh/total-number
当前配置仅作用于Strict
模式, 详情请参考Data-Structure
部分。默认与gang.scheduling.koordinator.sh/min-available
一致。gang.scheduling.koordinator.sh/mode
选项Strict
或者NonStrict
。 默认配置为Strict
。gang.scheduling.koordinator.sh/groups
用于配置 GangGroups 名称。默认为空,表示不需要与其他资源合并到 GangGroups,同一个 GangGroups 的 Gangs 可以来自于不同的 namespace。
注意⚠️,如果同时通过 CRD 和 annotation 方式进行配置,该 annotation 配置将会覆盖 CRD 配置。同时, GangGroup 名称格式为 " gangNamespace" + "/" + "gangName "
示例
基础 Gang 调度配置如下:
metadata:
annotations:
gang.scheduling.koordinator.sh/name: gang-a
gang.scheduling.koordinator.sh/min-available: 5
创建一个任务包含两个策略:A 和 B,每个策略包含一些 Pod。PodA 属于 roleA,PodB 属于 roleB。roleA、roleB 归属于同一个 GangGroup,示例如下:
metadata:
annotations:
gang.scheduling.koordinator.sh/name: gang-a
gang.scheduling.koordinator.sh/waiting-time: 3600s
gang.scheduling.koordinator.sh/min-available: 5
gang.scheduling.koordinator.sh/total-number: 5
gang.scheduling.koordinator.sh/mode: Strict
gang.scheduling.koordinator.sh/groups: ["namespaceA/gang-a", "namespaceB/gang-b"]
metadata:
annotations:
gang.scheduling.koordinator.sh/name: gang-b
gang.scheduling.koordinator.sh/waiting-time: 3600s
gang.scheduling.koordinator.sh/min-available: 5
gang.scheduling.koordinator.sh/total-number: 5
gang.scheduling.koordinator.sh/mode: Strict
gang.scheduling.koordinator.sh/groups: ["namespaceA/gang-a", "namespaceB/gang-b"]
创建一个任务包含两个策略:A 和 B,每个策略包含一些 Pod。PodA 属于 roleA,PodB 属于 roleB。roleA、roleB 归属于不同 GangGroup,示例如下:
metadata:
annotations:
gang.scheduling.koordinator.sh/name: gang-a
gang.scheduling.koordinator.sh/waiting-time: 3600s
gang.scheduling.koordinator.sh/min-available: 5
gang.scheduling.koordinator.sh/total-number: 5
gang.scheduling.koordinator.sh/mode: Strict
gang.scheduling.koordinator.sh/groups: ""
metadata:
annotations:
gang.scheduling.koordinator.sh/name: gang-b
gang.scheduling.koordinator.sh/waiting-time: 3600s
gang.scheduling.koordinator.sh/min-available: 5
gang.scheduling.koordinator.sh/total-number: 5
gang.scheduling.koordinator.sh/mode: Strict
gang.scheduling.koordinator.sh/groups: ""
详细设计
QueueSortPlugin
我们单独设计调度器插件用于实现 QueueSort
拓展点,这样就可以将队列排序逻辑集成到所有插件,并且只需要注册一次。
当前方案中,我们实现 Less 方法汇总属于相同 Gang 的 pod。具体排序规则为:
- 比较两个 pod 的优先级配置,优先级越高的 pod 优先入队。
- 比较两个 pod 的创建时间戳,如果 pod 归属于同一个 Gang 配置,我们比较 Gang 配置创建时间,谁先创建则优先入队。
- 比较 pod 的 namespace,如果 pod 归属某一个 Gang 配置,则比较 Gang 名称。
type QueueSortPlugin interface{
QueueSort(*QueuedPodInfo, *QueuedPodInfo) bool
}
GangSchedulingPlugin
Data-Structure
Gang
type Gang struct {
Name string
WaitTime time.Duration
Mode string //Strict or NonStrict
GangGroup []string
MinRequiredNumber int
TotalChildrenNum int
Children map[string]*PodInfo
BoundChildren map[string]*PodInfo
WaitingForBindChildren map[string]*PodInfo
ResourceSatisfied bool
ScheduleCycle int
ScheduleCycleValid bool
ChildrenScheduleRoundMap map[string]int
}
Gang,用于记录 Gang 调度状态到调度器缓存。
Children
,用于记录归属于当前 Gang 的 pod 列表。BoundChildren
,WaitingForBindChildren
用于记录已经出于 binding 状态的 pod,用于检查 pod 是否已经通过 permit 阶段。ResourceSatisfied
,用于标记当前 pod 是否通过调度 Permit 阶段,如果通过则为 true。该字段主要用于判断当前 Gang 调度是否满足条件。scheduleCycle
,childrenScheduleRoundMap
,前面两个字段主要用于控制 Gang 调度周期。举个🌰 ,调度伊始
scheduleCycle
字段为 1,childrenScheduleRoundMap
中所有 pod 值为 0。 所有 pod 进入 PreFilter 阶段时,将会判断childrenScheduleRoundMap
中 pod 值是否小于scheduleCycle
值; 如果上一步校验通过,则将childrenScheduleRoundMap
值设置为scheduleCycle
的值,并通过当前校验; 反之则说明当前 pod 在本轮调度周期内已经完成调度,需要拒绝本次调度。 根据totalChildrenNum
字段,当所有 pod 都通过 PreFilter 阶段,说明当前调度周期所有 pod 已经完成调度,scheduleCycle
需要累加 1,说明开启新一轮调度周期。scheduleCycleValid
,当前 Gang 中任意 pod 在 Filter 阶段失败,scheduleCycleValid 将设置为 true,只有所有 pod 全部通过 Filter 阶段,该字段才会设置为 true。scheduleCycleValid=false
此场景下所有 pod 将不会进行调度,同时所有调度中都 pod 将被在 PreFilter 阶段被拒绝,当新一轮调度周期开启时,scheduleCycleValid
才会被设置为 true。
注意⚠️ ,scheduleCycle\scheduleCycleValid\childrenScheduleRoundMap
仅作用于 Strict
模式。
GangPlugin
在调度器框架 Plugin 结构提基础上,增加 gangCache 用于缓存 Gang 信息。
type GangPlugin struct {
frameworkHandler framework.Handle
gangClient gangClient.Interface
podLister listerv1.PodLister
snapshotSharedLister framework.SharedLister
gangCache map[string]*Gang
}
当启动 kubernetes 调度器时,我们仅需要将我们当逻辑挂载到以下 4 个扩展点:
var(
_ framework.PreFilterPlugin = &GangScheduling{}
_ framework.PostFilterPlugin = &GangScheduling{}
_ framework.PermitPlugin = &GangScheduling{}
_ framework.ReservePlugin = &Coscheduling{}
)
type GangScheduling interface{
ActiveGang(pod *corev1.Pod, state *framework.CycleState)
PreFilter(context.Context, *corev1.Pod) error
PostFilter(ctx context.Context, state *CycleState, pod *v1.Pod, filteredNodeStatusMap NodeToStatusMap) (*PostFilterResult, *Status)
Permit(context.Context, *corev1.Pod) Status
Unreserve(ctx context.Context, state *framework.CycleState, pod *v1.Pod, nodeName string)
}
PreFilter
NonStrict
模式,我们仅处理 步骤一和二:
校验 Gang 下包含所有 pod 是否符合最小数,如果不符合则拒绝当前 pod。
校验 Gang 是否超时,如果超时则拒绝当前 pod。
校验 Gang scheduleCycleValid 字段是否为 true,如果为 false 则拒绝当前 pod。
尝试更新
scheduleCycle
,scheduleCycleValid
,childrenScheduleRoundMap
字段。
PostFilter
到达当前阶段说明 pod 没有通过 Filter 校验,操作如下:
如果
Strict
模式,设置scheduleCycleValid
字段为 false,同时释放所有已经完成调度的 pod。如果
NonStrict
模式则不做任何操作。
Permit
到达当前阶段说明 pod 已经通过 Filter 校验,调度器插件将会计算 GangGroup 下所有 Gang 已经完成调度 pod 数量是否满足 Gang 最小值。
- 如果 Gang 不符合 bind 条件,我们会将 pod 状态修改为 "Wait" 并配置超时时间,同时 bind 协程一直保持等待直到超时或者通过校验。
随后,我们会执行
ActiveGang
操作,该操作会将归属于 Gang 的 pod 从schedulableQueue
或者backoffQueue
队列中迁移到activeQueue
队列, 如此操作之后,pod 将会被尽快尽享调度。
注意⚠️ ,社区调度器中,调度周期最长不能超过 15 分钟,我们则需要通过改写 RunPermitPlugins 将调度周期配置超过 15 分钟。
- 如果 Gang 符合 bind 条件,我们将等待中 pod 状态修改为 "Success",此时 bind 协程将结束等待并执行后续操作,并将 Gang 对象中
ResourceSatisfied
设置为 true。
Un-reserve
如果 permit 阶段超时且 binding 阶段失败,此时调度阶段将会流转到 un-reserve 阶段,我们通过 Gang 对象中 ResourceSatisfied
值判断,如果此时值为 true 说明 binding 阶段失败,反之则说明 Gang 超时。
如果 permit 阶段超时,我们将在所有 Gang 下所有 pod annotation 中增加
gang.scheduling.koordinator.sh/timeout=true
,同时释放所有已经调度成功的 pod。 此时,Gang 下所有 pod 将永远不会再进行调度,用户需要手动处理 permit 超时问题。如果 binding 阶段失败,Gang 资源累计操作将会结束,随后会回滚所有失败的 pod 。
Init
我们将 watch pod 事件,并根据事件类型持续更新 Gang。
未解问题
可选性
用户可以根据具体场景选择使用 Gang Strict
或者 NonStrict
模式。