上篇我们分析了 hami-device-plugin-nvidia,知道了 HAMi 的 NVIDIA device plugin 工作原理。

本文为 HAMi 原理分析的第二篇,分析 hami-scheduler 实现原理。

为了实现基于 vGPU 的调度,HAMi 实现了自己的 Scheduler:hami-scheduler,除了基础调度逻辑之外,还有 spread & binpark 等 高级调度策略。

主要包括以下几个问题:

  • 1)Pod 是如何使用到 hami-scheduler,创建 Pod 时我们未指定 SchedulerName 默认会使用 default-scheduler 进行调度才对

  • 2)hami-scheduler 逻辑,spread & binpark 等 高级调度策略是如何实现的

由于内容比较多,拆分为了 hami-webhook、 hami-scheduler 以及 Spread&Binpack 调度策略三篇文章,本篇我们主要解决第一个问题。

以下分析基于 HAMi v2.4.0

1. hami-scheduler 启动命令

hami-scheduler 具体包括两个组件:

  • hami-webhook

  • hami-scheduler

虽然是两个组件,实际上代码是放在一起的,cmd/scheduler/main.go 为启动文件:

这里也是用 corba 库实现的一个命令行工具。

var (
sher *scheduler.Scheduler
tlsKeyFile string
tlsCertFile string
rootCmd = &cobra.Command{
Use: "scheduler",
Short: "kubernetes vgpu scheduler",
Run: func(cmd *cobra.Command, args []string) {
start()
},
}
) func main() {
if err := rootCmd.Execute(); err != nil {
klog.Fatal(err)
}
}

最终启动的 start 方法如下:

func start() {
device.InitDevices()
sher = scheduler.NewScheduler()
sher.Start()
defer sher.Stop() // start monitor metrics
go sher.RegisterFromNodeAnnotations()
go initMetrics(config.MetricsBindAddress) // start http server
router := httprouter.New()
router.POST("/filter", routes.PredicateRoute(sher))
router.POST("/bind", routes.Bind(sher))
router.POST("/webhook", routes.WebHookRoute())
router.GET("/healthz", routes.HealthzRoute())
klog.Info("listen on ", config.HTTPBind)
if len(tlsCertFile) == 0 || len(tlsKeyFile) == 0 {
if err := http.ListenAndServe(config.HTTPBind, router); err != nil {
klog.Fatal("Listen and Serve error, ", err)
}
} else {
if err := http.ListenAndServeTLS(config.HTTPBind, tlsCertFile, tlsKeyFile, router); err != nil {
klog.Fatal("Listen and Serve error, ", err)
}
}
}

开始初始化了一下 Device

这个后续 Webhook 会用到,等会再看

device.InitDevices()

然后启动了 Scheduler

sher = scheduler.NewScheduler()
sher.Start()
defer sher.Stop()

接着启动了一个 Goroutine 来从之前 device plugin 添加到 Node 对象上的 Annotations 中不断解析拿到具体的 GPU 信息

go sher.RegisterFromNodeAnnotations()

最后则是启动了一个 HTTP 服务

router := httprouter.New()
router.POST("/filter", routes.PredicateRoute(sher))
router.POST("/bind", routes.Bind(sher))
router.POST("/webhook", routes.WebHookRoute())
router.GET("/healthz", routes.HealthzRoute())

其中

  • /webhook 就是 Webhook 组件
  • /filter/bind 则是 Scheduler 组件
  • /healthz 则用作健康检查。

接下来在通过源码分析 Webhook 以及 Scheduler 各自的实现。

2. hami-webhook

这里的 Webhook 是一个 Mutating Webhook,主要是为 Scheduler 服务的。

核心功能是:根据 Pod Resource 字段中的 ResourceName 判断该 Pod 是否使用了 HAMi vGPU,如果是则修改 Pod 的 SchedulerName 为 hami-scheduler,让 hami-scheduler 进行调度,否则不做处理。

MutatingWebhookConfiguration 配置

为了让 Webhook 生效,HAMi 部署时会创建MutatingWebhookConfiguration 对象,具体内容如下:

root@test:~# kubectl -n kube-system get MutatingWebhookConfiguration vgpu-hami-webhook -oyaml
apiVersion: admissionregistration.k8s.io/v1
kind: MutatingWebhookConfiguration
metadata:
annotations:
meta.helm.sh/release-name: vgpu
meta.helm.sh/release-namespace: kube-system
labels:
app.kubernetes.io/managed-by: Helm
name: vgpu-hami-webhook
webhooks:
- admissionReviewVersions:
- v1beta1
clientConfig:
caBundle: xxx
service:
name: vgpu-hami-scheduler
namespace: kube-system
path: /webhook
port: 443
failurePolicy: Ignore
matchPolicy: Equivalent
name: vgpu.hami.io
namespaceSelector:
matchExpressions:
- key: hami.io/webhook
operator: NotIn
values:
- ignore
objectSelector:
matchExpressions:
- key: hami.io/webhook
operator: NotIn
values:
- ignore
reinvocationPolicy: Never
rules:
- apiGroups:
- ""
apiVersions:
- v1
operations:
- CREATE
resources:
- pods
scope: '*'
sideEffects: None
timeoutSeconds: 10

具体效果是在创建 Pod 时,kube-apiserver 会调用该 service 对应的 webhook,这样就注入了我们的自定义逻辑。

关注的对象为 Pod 的 CREATE 事件:

  rules:
- apiGroups:
- ""
apiVersions:
- v1
operations:
- CREATE
resources:
- pods
scope: '*'

但是不包括以下对象

  namespaceSelector:
matchExpressions:
- key: hami.io/webhook
operator: NotIn
values:
- ignore
objectSelector:
matchExpressions:
- key: hami.io/webhook
operator: NotIn
values:
- ignore

即:namespace 或者 资源对象上带 hami.io/webhook=ignore label 的都不走该 Webhook 逻辑。

请求的 Webhook 为

    service:
name: vgpu-hami-scheduler
namespace: kube-system
path: /webhook
port: 443

即:对于满足条件的 Pod 的 CREATE 时,kube-apiserver 会调用该 service 指定的服务,也就是我们的 hami-webhook。

接下来就开始分析 hami-webhook 具体做了什么。

源码分析

这个 Webhook 的具体实现如下:

// pkg/scheduler/webhook.go#L52
func (h *webhook) Handle(_ context.Context, req admission.Request) admission.Response {
pod := &corev1.Pod{}
err := h.decoder.Decode(req, pod)
if err != nil {
klog.Errorf("Failed to decode request: %v", err)
return admission.Errored(http.StatusBadRequest, err)
}
if len(pod.Spec.Containers) == 0 {
klog.Warningf(template+" - Denying admission as pod has no containers", req.Namespace, req.Name, req.UID)
return admission.Denied("pod has no containers")
}
klog.Infof(template, req.Namespace, req.Name, req.UID)
hasResource := false
for idx, ctr := range pod.Spec.Containers {
c := &pod.Spec.Containers[idx]
if ctr.SecurityContext != nil {
if ctr.SecurityContext.Privileged != nil && *ctr.SecurityContext.Privileged {
klog.Warningf(template+" - Denying admission as container %s is privileged", req.Namespace, req.Name, req.UID, c.Name)
continue
}
}
for _, val := range device.GetDevices() {
found, err := val.MutateAdmission(c)
if err != nil {
klog.Errorf("validating pod failed:%s", err.Error())
return admission.Errored(http.StatusInternalServerError, err)
}
hasResource = hasResource || found
}
} if !hasResource {
klog.Infof(template+" - Allowing admission for pod: no resource found", req.Namespace, req.Name, req.UID)
//return admission.Allowed("no resource found")
} else if len(config.SchedulerName) > 0 {
pod.Spec.SchedulerName = config.SchedulerName
}
marshaledPod, err := json.Marshal(pod)
if err != nil {
klog.Errorf(template+" - Failed to marshal pod, error: %v", req.Namespace, req.Name, req.UID, err)
return admission.Errored(http.StatusInternalServerError, err)
}
return admission.PatchResponseFromRaw(req.Object.Raw, marshaledPod)
}

逻辑比较简单:

  • 1)判断 Pod 是否需要使用 HAMi-Scheduler 进行调度

  • 2)需要的话就修改 Pod 的 SchedulerName 字段为 hami-scheduler(名字可配置)

至此,核心部分就是如何判断该 Pod 是否需要使用 hami-scheduler 进行调度呢?

如何判断是否使用 hami-scheduler

Webhook 中主要根据 Pod 是否申请 vGPU 资源来确定,不过也有一些特殊逻辑。

特权模式 Pod

首先对于特权模式的 Pod,HAMi 是直接忽略的

if ctr.SecurityContext != nil {
if ctr.SecurityContext.Privileged != nil && *ctr.SecurityContext.Privileged {
klog.Warningf(template+" - Denying admission as container %s is privileged", req.Namespace, req.Name, req.UID, c.Name)
continue
}
}

因为开启特权模式之后,Pod 可以访问宿主机上的所有设备,再做限制也没意义了,因此这里直接忽略。

具体判断逻辑

然后根据 Pod 中的 Resource 来判断是否需要使用 hami-scheduler 进行调度:

for _, val := range device.GetDevices() {
found, err := val.MutateAdmission(c)
if err != nil {
klog.Errorf("validating pod failed:%s", err.Error())
return admission.Errored(http.StatusInternalServerError, err)
}
hasResource = hasResource || found
}

如果 Pod Resource 中有申请 HAMi 这边支持的 vGPU 资源则,那么就需要使用 HAMi-Scheduler 进行调度。

而那些 Device 是 HAMi 支持的呢,就是之前 start 中初始化的:

var devices map[string]Devices

func GetDevices() map[string]Devices {
return devices
} func InitDevices() {
devices = make(map[string]Devices)
DevicesToHandle = []string{}
devices[cambricon.CambriconMLUDevice] = cambricon.InitMLUDevice()
devices[nvidia.NvidiaGPUDevice] = nvidia.InitNvidiaDevice()
devices[hygon.HygonDCUDevice] = hygon.InitDCUDevice()
devices[iluvatar.IluvatarGPUDevice] = iluvatar.InitIluvatarDevice()
//devices[d.AscendDevice] = d.InitDevice()
//devices[ascend.Ascend310PName] = ascend.InitAscend310P()
DevicesToHandle = append(DevicesToHandle, nvidia.NvidiaGPUCommonWord)
DevicesToHandle = append(DevicesToHandle, cambricon.CambriconMLUCommonWord)
DevicesToHandle = append(DevicesToHandle, hygon.HygonDCUCommonWord)
DevicesToHandle = append(DevicesToHandle, iluvatar.IluvatarGPUCommonWord)
//DevicesToHandle = append(DevicesToHandle, d.AscendDevice)
//DevicesToHandle = append(DevicesToHandle, ascend.Ascend310PName)
for _, dev := range ascend.InitDevices() {
devices[dev.CommonWord()] = dev
DevicesToHandle = append(DevicesToHandle, dev.CommonWord())
}
}

devices 是一个全局变量, InitDevices 则是在初始化该变量,供 Webhook 中使用,包括 NVIDIA、海光、天数、昇腾等等。

这里以 NVIDIA 为例说明 HAMi 是如何判断一个 Pod 是否需要自己来调度的,MutateAdmission 具体实现如下:

func (dev *NvidiaGPUDevices) MutateAdmission(ctr *corev1.Container) (bool, error) {
/*gpu related */
priority, ok := ctr.Resources.Limits[corev1.ResourceName(ResourcePriority)]
if ok {
ctr.Env = append(ctr.Env, corev1.EnvVar{
Name: api.TaskPriority,
Value: fmt.Sprint(priority.Value()),
})
} _, resourceNameOK := ctr.Resources.Limits[corev1.ResourceName(ResourceName)]
if resourceNameOK {
return resourceNameOK, nil
} _, resourceCoresOK := ctr.Resources.Limits[corev1.ResourceName(ResourceCores)]
_, resourceMemOK := ctr.Resources.Limits[corev1.ResourceName(ResourceMem)]
_, resourceMemPercentageOK := ctr.Resources.Limits[corev1.ResourceName(ResourceMemPercentage)] if resourceCoresOK || resourceMemOK || resourceMemPercentageOK {
if config.DefaultResourceNum > 0 {
ctr.Resources.Limits[corev1.ResourceName(ResourceName)] = *resource.NewQuantity(int64(config.DefaultResourceNum), resource.BinarySI)
resourceNameOK = true
}
} if !resourceNameOK && OverwriteEnv {
ctr.Env = append(ctr.Env, corev1.EnvVar{
Name: "NVIDIA_VISIBLE_DEVICES",
Value: "none",
})
}
return resourceNameOK, nil
}

首先判断如果 Pod 申请的 Resource 中有对应的 ResourceName 就直接返回 true

_, resourceNameOK := ctr.Resources.Limits[corev1.ResourceName(ResourceName)]
if resourceNameOK {
return resourceNameOK, nil
}

NVIDIA GPU 对应的 ResourceName 为:

fs.StringVar(&ResourceName, "resource-name", "nvidia.com/gpu", "resource name")

如果 Pod Resource 中申请了这个资源,就需要由 HAMi 进行调度,其他几个 Resource 也是一样的就不细看了。

HAMi 会支持 NVIDIA、天数、华为、寒武纪、海光等厂家的 GPU,默认 ResourceName 为:nvidia.com/gpu、iluvatar.ai/vgpu、hygon.com/dcunum、cambricon.com/mlu、huawei.com/Ascend310 等等

使用这些 ResourceName 时都会有 HAMi-Scheduler 进行调度。

ps:这些 ResourceName 都是可以在对应 device plugin 中进行配置的。

如果没有直接申请nvidia.com/gpu ,但是申请了 gpucore、gpumem 等资源,同时 Webhook 配置的 DefaultResourceNum 大于 0 也会返回 true,并自动添加上 nvidia.com/gpu 资源的申请。

_, resourceCoresOK := ctr.Resources.Limits[corev1.ResourceName(ResourceCores)]
_, resourceMemOK := ctr.Resources.Limits[corev1.ResourceName(ResourceMem)]
_, resourceMemPercentageOK := ctr.Resources.Limits[corev1.ResourceName(ResourceMemPercentage)] if resourceCoresOK || resourceMemOK || resourceMemPercentageOK {
if config.DefaultResourceNum > 0 {
ctr.Resources.Limits[corev1.ResourceName(ResourceName)] = *resource.NewQuantity(int64(config.DefaultResourceNum), resource.BinarySI)
resourceNameOK = true
}
}

修改 SchedulerName

对于上述满足条件的 Pod,需要由 HAMi-Scheduler 进行调度,Webhook 中会将 Pod 的 spec.schedulerName 改成 hami-scheduler。

具体如下:

if !hasResource {
klog.Infof(template+" - Allowing admission for pod: no resource found", req.Namespace, req.Name, req.UID)
//return admission.Allowed("no resource found")
} else if len(config.SchedulerName) > 0 {
pod.Spec.SchedulerName = config.SchedulerName
}

这样该 Pod 就会由 HAMi-Scheduler 进行调度了,接下来就是 HAMi-Scheduler 开始工作了。

这里也有一个特殊逻辑:如果创建时直接指定了 nodeName,那 Webhook 就会直接拒绝,因为指定 nodeName 说明 Pod 都不需要调度了,会直接到指定节点启动,但是没经过调度,可能该节点并没有足够的资源。

if pod.Spec.NodeName != "" {
klog.Infof(template+" - Pod already has node assigned", req.Namespace, req.Name, req.UID)
return admission.Denied("pod has node assigned")
}

【Kubernetes 系列】持续更新中,搜索公众号【探索云原生】订阅,阅读更多文章。


3. 小结

该 Webhook 的作用为:将申请了 vGPU 资源的 Pod 的调度器修改为 hami-scheduler,后续使用 hami-scheduler 进行调度。

也存在一些特殊情况:

  • 对于开启特权模式的 Pod Webhook 会忽略,不会将其切换到 hami-scheduler 进行调度,而是依旧使用 default-scheduler。

  • 对于直接指定了 nodeName 的 Pod, Webhook 会直接拒绝,拦截掉 Pod 的创建。

基于以上特殊情况,可能会出现以下问题,也是社区中多次有同学反馈的:

特权模式 Pod 申请了 gpucore、gpumem 等资源,创建后一直处于 Pending 状态, 无法调度,提示节点上没有 gpucore、gpumem 等资源。

因为 Webhook 直接跳过了特权模式的 Pod,所以该 Pod 会使用 default-scheduler 进行调度,然后 default-scheduler 根据 Pod 中的 ResourceName 查看时发现没有任何 Node 有 gpucore、gpumem 等资源,因此无法调度,Pod 处理 Pending 状态。

ps:gpucore、gpumem 都是虚拟资源,并不会展示在 Node 上,只有 hami-scheduler 能够处理。

HAMi Webhook 工作流程如下:

  • 1)用户创建 Pod 并在 Pod 中申请了 vGPU 资源

  • 2)kube-apiserver 根据 MutatingWebhookConfiguration 配置请求 HAMi-Webhook

  • 3)HAMi-Webhook 检测 Pod 中的 Resource,发现是申请的由 HAMi 管理的 vGPU 资源,因此把 Pod 中的 SchedulerName 改成了 hami-scheduler,这样这个 Pod 就会由 hami-scheduler 进行调度了。

    • 对于特权模式的 Pod,Webhook 会直接跳过不处理

    • 对于使用 vGPU 资源但指定了 nodeName 的 Pod,Webhook 会直接拒绝

  • 4)接下来则进入 hami-scheduler 调度逻辑,下篇分析~

至此,我们就搞清楚了,为什么 Pod 会使用上 hami-scheduler 以及哪些 Pod 会使用 hami-scheduler 进行调度。 同时也说明了为什么特权模式 Pod 会无法调度的问题。

接下来就开始分析 hami-scheduler 实现了。

HAMi vGPU 原理分析 Part2:hami-webhook 原理分析的更多相关文章

  1. jQuery 2.0.3 源码分析Sizzle引擎解析原理

    jQuery 2.0.3 源码分析Sizzle引擎 - 解析原理 声明:本文为原创文章,如需转载,请注明来源并保留原文链接Aaron,谢谢! 先来回答博友的提问: 如何解析 div > p + ...

  2. 【MyBatis源码分析】插件实现原理

    MyBatis插件原理----从<plugins>解析开始 本文分析一下MyBatis的插件实现原理,在此之前,如果对MyBatis插件不是很熟悉的朋友,可参看此文MyBatis7:MyB ...

  3. 分析ReentrantLock的实现原理

    http://www.jianshu.com/p/fe027772e156 什么是AQS AQS即是AbstractQueuedSynchronizer,一个用来构建锁和同步工具的框架,包括常用的Re ...

  4. 编译调试 .NET Core 5.0 Preview 并分析 Span 的实现原理

    很久没有写过 .NET Core 相关的文章了,目前关店在家休息所以有些时间写一篇新的

  5. HashMap实现原理(jdk1.7),源码分析

    HashMap实现原理(jdk1.7),源码分析 ​ HashMap是一个用来存储Key-Value键值对的集合,每一个键值对都是一个Entry对象,这些Entry被以某种方式分散在一个数组中,这个数 ...

  6. 透过 ReentrantLock 分析 AQS 的实现原理

    对于 Java 开发者来说,都会碰到多线程访问公共资源的情况,这时候,往往都是通过加锁来保证访问资源结果的正确性.在 java 中通常采用下面两种方式来解决加锁得问题: synchronized 关键 ...

  7. 线程池底层原理详解与源码分析(补充部分---ScheduledThreadPoolExecutor类分析)

    [1]前言 本篇幅是对 线程池底层原理详解与源码分析  的补充,默认你已经看完了上一篇对ThreadPoolExecutor类有了足够的了解. [2]ScheduledThreadPoolExecut ...

  8. Net6 Configuration & Options 源码分析 Part2 Options

    Net6 Configuration & Options 源码分析 Part2 Options 第二部分主要记录Options 模型 OptionsConfigurationServiceCo ...

  9. IIS站点工作原理与ASP.NET工作原理

    IIS站点工作原理与ASP.NET工作原理  一.IIS IIS 7.0工作原理图 两种模式: 1.用户模式(User Mode)(运行用户的程序代码.限制在特定的范围内活动.有些操作必须要受到Ker ...

  10. GCC编译器原理(三)------编译原理三:编译过程(2-2)---编译之语法分析

    2.2 语法分析 语法分析器(Grammar Parser)将对由扫描器产生的记号进行语法分析,从而产生语法树(Syntax Tree).整个分析过程采用了上下文无关语法(Context-free G ...

随机推荐

  1. 聊聊 ruoyi-vue ,ruoyi-vue-plus ,ruoyi-vue-pro 谁才是真正的王者

    笔者在知乎.Github 上搜索不少快速开发框架 ,很多的话题都绕不开若依 RuoYi . 开源世界 RuoYi 单体框架有三个不同的项目,分别是:ruoyi-vue .ruoyi-vue-plus ...

  2. Pycomcad实现Autocad橡皮线效果

    import sys sys.path.append(r'F:\PycharmProject\PycomCAD') from pycomcad import * def tryit(): acad.I ...

  3. IDEA如何增加界面对比度

    这个问题困扰大家许久了应该,今天就让我来终结吧! 背景的对比度虽然没法直接修改,但是我们可以直接改背景色进而达到修改对比度的效果! Editor--color Scheme--general--Tex ...

  4. 基于Photon与Unreal Engine的VR协作平台开发实战教程

    引言 在数字化转型加速的今天,虚拟现实(VR)技术正在重塑远程协作模式.本教程将带领读者从零开始构建一个支持多人协同的VR办公平台,通过Unreal Engine 5的强大渲染能力与Photon引擎的 ...

  5. Linux 在文件中统计关键字出现的次数

    摘要:在当前文件或者当前目录下所有文件中,使用Linux命令grep.awk.sed.rg或者cat统计关键字出现的次数. 目录 问题背景 解决办法 使用grep和wc命令 使用awk命令模式匹配 使 ...

  6. NOIp2020复赛前日志

    NOIp2020复赛前日志 组合数和卢卡斯定理 首先写的顺序别搞错了 从\(n\)个不同元素中取出\(m(m≤n)\)个元素的所有组合的个数 \[C_n^m=\binom nm=C(n,m)=\fra ...

  7. WSL学习笔记

    WSL学习笔记 适用于 Linux 的 Windows 子系统 (WSL) 是 Windows 的一项功能,可用于在 Windows 计算机上运行 Linux 环境,而无需单独的虚拟机或双引导. WS ...

  8. controller的简单介绍

    介绍 Kubernetes控制器是一个主动调谐的过程,它会watch一些对象的期望状态,也会watch实际的状态,然后控制器会发送一些指令尝试让对象的当前状态往期望状态迁移. 控制器最简单的实现就是一 ...

  9. Nginx 本地代理转发请求 502 Bad Gateway

    问题 在使用 yum 安装 nginx 后可能会出现配置完成后却无法访问的问题,查看 audit.log 会发现类似于以下的错误信息 原因 出现此问题的原因是 SELinux 基于最小权限原则默认拦截 ...

  10. Python 潮流周刊#108:AI 会取代初级开发者吗?(摘要)

    本周刊由 Python猫 出品,精心筛选国内外的 400+ 信息源,为你挑选最值得分享的文章.教程.开源项目.软件工具.播客和视频.热门话题等内容.愿景:帮助所有读者精进 Python 技术,并增长职 ...