由浅入深理解Volcano-Scheduler调度器

https://ericamblog.oss-cn-shanghai.aliyuncs.com/all/understand%20-volcano-scheduler-from-zero-title.png

前言

很久前就想整理的一篇文章,结果鸽了很久,终于得闲来完成。

前段时间,由于公司项目的原因,第一次接触到调度功能的开发。初次接触时,云里雾里,很多东西都是在做的过程中逐渐串在一起后才恍然大悟。后来,基于这次开发经验,也在公司内部组织了一次技术分享,帮助内部的小伙伴上手调度开发。本篇文章主要是记录经验分享,如果其中存在错误,欢迎大家指正。

基础知识

1.device-plugin简介

Kubernetes 作为一个自动化容器编排系统,在调度 pod 的时候会根据容器需要的资源进行节点的选择,节点的选择会分为预选和优选阶段。预选阶段会根据所有节点上剩余的资源量与 pod 需要的资源量进行对比,选出能够满足需求的节点。通常情况下,这里的资源都会包括 CPU 和 Memory ,就像下面这样:

1
2
3
4
5
6
7
resources:
  requests:
    memory: "64Mi"
    cpu: "250m"
  limits:
    memory: "128Mi"
    cpu: "500m"

当涉及到AI作业时, pod 便会需要 GPU 资源,并借助 Kubernetes 的调度器将容器调度到有空余 GPU 资源的节点。 在 1.11 版本之前的 Kubernetes 中,提供了 alpha.kubernetes.io/nvidia-gpu 的资源名称来帮助我们根据 GPU 资源调度。但是这也带来了一些问题:

  • Kubernetes 需要维护 NVIDIA GPU 相关的代码,增加了维护成本。

  • NVIDIA GPU 方面的专家不一定熟悉 Kubernetes ,这不符合让最擅长的人做最擅长的事的原则。

  • 除了 NVIDIA GPU ,还会有其他的计算资源需要支持。

因此, Kubernetes 在 1.8 版本引入了 device plugin 机制,将第三方的计算资源通过插件的方式引入 Kubernetes ,并且由第三方厂商自行维护。 所以,device-plugin的作用简要概括便是:将计算卡的资源字段(gpu/mlu等)向kubelet注册,并实时对节点资源监听,完成pod申请资源时gpu相关资源的绑定。

不同任务调度组件
不同任务调度组件

2.device-plugin工作时序图

device-plugin工作时序图
device-plugin工作时序图

每个device-plugin都是通过daemonset的方式,以pod的形式运行在每个节点上。

device-plugin的核心工作流程很简单:

  • 通过unix socket接口与kubelet建立连接关系,然后向其注册资源名称(例如gpu等)
  • 然后通过ListAndWatch接口,扫描节点上的device资源,然后发送健康/不健康的device资源,令kubelet感知。
  • 用户任务创建pod时,kubelet根据节点状态进行pod调度,为其分配device。(例如一台节点还剩余5张gpu卡,kubelet为一个2卡任务分配0卡和3卡)
  • device-plugin接收到kubelet的分配请求,调用Allocate接口函数完成资源分配。

以上便是device-plugin的核心工作流程。万变不离其宗,虽然不同device-plugin实现的功能不同,但都是可以通过先定位核心流程然后再发散去理解的。

kubernetes - device plugin的rpc接口

kubernetes 提供了 rpc 接口,方便向 kubelet 注册硬件资源。 device-plugin 为了实现和 kubelet 的通信,需要实现接口。

每个device-plugin项目都会包含与Kubelet的连接注册函数,以及下述五个rpc接口函数。

通过其可以基本了解:

  • 注册资源字段

  • 如何持续监听节点上的device

  • device分配策略。

  • ……………

然后根据项目功能逐步了解其他函数设计。

device-plugin的rpc接口函数
device-plugin的rpc接口函数

Volcano-Scheduler调度器

kubernetes deault scheduler

kubernetes当然有默认的pod调度器,但是其并不适应AI作业任务需求。在多机训练任务中,一个AI作业可能需要同时创建上千个甚至上万个pod,而只有当所有pod当创建完成后,AI作业才能开始运行,而如果有几个pod创建失败,已经创建成功的pod就应该退出并释放资源,否则便会产生资源浪费的情况。因此Ai作业的pod调度应该遵循”All or nothing“的理念,即要不全部调度成功,否则应一个也不调度。这便是Volcano项目的由来(前身是kube-batch项目),接下来便来介绍Volcano的调度。

default scheduler
default scheduler

volcano scheduler流程图

上图中,需要注意的资源字段有:VolcanoJob、PodGroup、Pod。

Sheduler遍历所有待调度的VcJob,每次会话开始(OpenSession)的时候,按照定义的次序依次执行 enqueue等action,为每个Job找到一个最合适的节点。(action中执行的具体算法逻辑取决于注册的plugin中各函数的实现)。

例如图中,enqueue、allocate是每次会话中需要执行的函数,而在enqueue这些函数中,会执行一些子方法,这些子方法通过plungin注册在map类型中,提供调用。(如下图predicateFns)

因此需要注意的是,当设计一些调度算法时,需要保证只针对某种计算卡生效,否则便可能影响其他任务。

继续回说资源字段,Volcano引入了以下资源字段:Queue、PodGroup、VolcanoJob。

•Queue 用于管理和优先级排序任务。它允许用户根据业务需求或优先级,将作业分组到不同的队列中。

•PodGroup 一组相关的 Pod 集合。这主要解决了 Kubernetes 原生调度器中单个 Pod 调度的限制。

•VolcanoJob 是 Volcano 中的一个核心概念,它扩展了 Kubernetes 的 Job 资源。

正是有了PodGroup,Volcano才具备了批量调度的能力,真正实现“all or nothing”的理念。

Vc-Sheduler资源字段详解

Volcano Controller监听到Volcano Job创建,然后创建PodGroup,Pod。资源的创建属于Controller内容,在这里略过,仅探讨在Scheduler中如何使用PodGroup和Pod。

当Informer监听到podgroup创建后,开始注册podgroup信息。

需要注意的是,代码中的PodGroup字段是对podgroup资源的上层封装。

继续深入探索setPodGroup函数。

上图中,getJobID返回字符串(%s/%s),填充值为podGroup的namespace和name。然后将podGroup继续封装成JobInfo。所以在后续插件函数设计时,*api.JobInfo声明的VcJob其实是PodGroup字段的包装。

查看实际任务podgroup的yaml字段,以及在代码中podgroup CR的类型字段,都无法寻找到pod信息。那pod是如何podgroup建立联系的呢?

回来细看NewJobInfo这个函数,看看PodGroup是如何被进一步封装的。

这里Uid便是JobInfo的身份标识,值便是podgroup的namespace和name。

因此pod便可以通过UID获取到所属的podgroup,而podgroup也能通过此管理组内的所有pod。

我们查看一下实际任务的pod信息,如下图所示:

可以看到,podGroup信息存储在pod的annotation中。因此每个podGroup便能知道自己组内的Pod有哪些,而每个pod也会知道自己所属的podGroup。

上面讲完了podGroup被监听创建时的处理逻辑,我们进一步来探讨pod的创建处理逻辑。

pod创建时,信息是被封装进TaskInfo字段,然后记录在SchedulerCache中。后续在插件开发时,*api.TaskInfo类型的VcTask对应的其实是pod。

在创建TaskInfo时,jobID即JobInfo(podgroup)的UID,存储在pod的annotation中。

经过前面的介绍,可以总结出以下几个重要的注意点:

  • volcano代码中的job对应的是podgroup资源。
  • volcano代码中的task对应的是pod资源
  • volcano中通过在pod annotation中注入podgroup信息,从而形成批次概念。
  • volcano代码中主要针对job操作,其实是针对podgroup。

Vc-Sheduler Plugin探索

在Scheduler初始化时,便会根据配置文件获取actions和plugins。

默认加载如下,

当然也可以自定义指定,

action和plugin都是先向framework注册,主要是把自己的方法向Session注册。

然后在每次Session开启时,根据配置里设置执行action,action里会使用插件注册的方法

在action调用函数时,会将所有plugin注册的该函数以此执行。

因此需要特别注意的一点是,在设计插件函数时,必须只针对该计算卡生效

Plugins开发经验

针对不同计算卡的调度功能以插件的形式向 volcano 注册

framework.RegisterPluginBuilder(mlu.PluginName, xxx.New)

在插件开发中,需要完成以下插件在每次 OnSessionOpen 时调用功能的开发:

(1)初始化函数 在初始化函数中,主要初始化待候选的 node 节点、待候选的 job (此 job 非 kubernetes 层面的 job ) 等。

(2)job 校验函数 通过初始化函数加入 SHandle.Jobs 的job,在完成入队后,才会执行 JobVaild 校验。 在初始化函数中添加候选 job 时,一般是判断其申请资源是否是计算卡系列(例如昇腾系列),而在 jobValid 阶段判断其是否符合具体卡型的申请要求(例如 Ascend910b 卡)。

(3)入队校验函数 在 volcano 当前的项目逻辑中,当 podGroup 存在 MinResources 申请时,才会执行入队校验函数。 入队校验函数一般是校验job申请的资源能否得到满足,满足则能入队。而不入队的 job 等待下次重新调 度。 需要特别注意的是,在函数中需要首先将非该计算卡任务跳过入队,否则一些任务会出现一直等待的情况。(例如 cpu 任务)

(4)优选打分函数 优选打分主要是针对 sHandle 初始化并完成预选阶段的Node,函数返回map列表,代表每个节点的打分 情况。但是节点的打分并不仅仅是通过优选打分,还有其他考量。因此当想指定调度节点时,需要在优选阶段返回的 map 中将得分打的较高(权重大)。

(5)Allocate函数Allocate 函数完成对每个task任务调度,主要是对 task 所对应的 pod 写入 annotation 。

分享个开发过程中遇到的实际问题。

当并发情况提任务时,可能会出现在device-plugin绑定计算卡时出现绑定失败的情况,报错为UnexpectedAdmissionError Pod Allocate failed due to rpc err code:device is inused

原因就是volcano调度和device-plugin分配这个链路中存在延时性,即可能device-plugin还未完成绑定完device,volcano便已再次完成了调度。

针对这个问题,我选择的解决方法是:执行 device-plugin 原生绑定逻辑(不通过 annotation 分配) 更新 pod annotation 和 cm ,并在延时1s后通过 docker inspect 再次更新 configmap ,防止 cm 初次更新失败时产生不一致性。

0%