找回密码
 立即注册
首页 业界区 业界 HAMi vGPU 原理分析 Part2:hami-webhook 原理分析 ...

HAMi vGPU 原理分析 Part2:hami-webhook 原理分析

创蟀征 3 天前
1.png

上篇我们分析了 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 库实现的一个命令行工具。
  1. var (
  2.     sher        *scheduler.Scheduler
  3.     tlsKeyFile  string
  4.     tlsCertFile string
  5.     rootCmd     = &cobra.Command{
  6.        Use:   "scheduler",
  7.        Short: "kubernetes vgpu scheduler",
  8.        Run: func(cmd *cobra.Command, args []string) {
  9.           start()
  10.        },
  11.     }
  12. )
  13. func main() {
  14.     if err := rootCmd.Execute(); err != nil {
  15.        klog.Fatal(err)
  16.     }
  17. }
复制代码
最终启动的 start 方法如下:
  1. func start() {
  2.     device.InitDevices()
  3.     sher = scheduler.NewScheduler()
  4.     sher.Start()
  5.     defer sher.Stop()
  6.     // start monitor metrics
  7.     go sher.RegisterFromNodeAnnotations()
  8.     go initMetrics(config.MetricsBindAddress)
  9.     // start http server
  10.     router := httprouter.New()
  11.     router.POST("/filter", routes.PredicateRoute(sher))
  12.     router.POST("/bind", routes.Bind(sher))
  13.     router.POST("/webhook", routes.WebHookRoute())
  14.     router.GET("/healthz", routes.HealthzRoute())
  15.     klog.Info("listen on ", config.HTTPBind)
  16.     if len(tlsCertFile) == 0 || len(tlsKeyFile) == 0 {
  17.        if err := http.ListenAndServe(config.HTTPBind, router); err != nil {
  18.           klog.Fatal("Listen and Serve error, ", err)
  19.        }
  20.     } else {
  21.        if err := http.ListenAndServeTLS(config.HTTPBind, tlsCertFile, tlsKeyFile, router); err != nil {
  22.           klog.Fatal("Listen and Serve error, ", err)
  23.        }
  24.     }
  25. }
复制代码
开始初始化了一下 Device
这个后续 Webhook 会用到,等会再看
  1. device.InitDevices()
复制代码
然后启动了 Scheduler
  1. sher = scheduler.NewScheduler()
  2. sher.Start()
  3. defer sher.Stop()
复制代码
接着启动了一个 Goroutine 来从之前 device plugin 添加到 Node 对象上的 Annotations 中不断解析拿到具体的 GPU 信息
  1. go sher.RegisterFromNodeAnnotations()
复制代码
最后则是启动了一个 HTTP 服务
  1. router := httprouter.New()
  2. router.POST("/filter", routes.PredicateRoute(sher))
  3. router.POST("/bind", routes.Bind(sher))
  4. router.POST("/webhook", routes.WebHookRoute())
  5. 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 对象,具体内容如下:
  1. root@test:~# kubectl -n kube-system get MutatingWebhookConfiguration vgpu-hami-webhook -oyaml
  2. apiVersion: admissionregistration.k8s.io/v1
  3. kind: MutatingWebhookConfiguration
  4. metadata:
  5.   annotations:
  6.     meta.helm.sh/release-name: vgpu
  7.     meta.helm.sh/release-namespace: kube-system
  8.   labels:
  9.     app.kubernetes.io/managed-by: Helm
  10.   name: vgpu-hami-webhook
  11. webhooks:
  12. - admissionReviewVersions:
  13.   - v1beta1
  14.   clientConfig:
  15.     caBundle: xxx
  16.     service:
  17.       name: vgpu-hami-scheduler
  18.       namespace: kube-system
  19.       path: /webhook
  20.       port: 443
  21.   failurePolicy: Ignore
  22.   matchPolicy: Equivalent
  23.   name: vgpu.hami.io
  24.   namespaceSelector:
  25.     matchExpressions:
  26.     - key: hami.io/webhook
  27.       operator: NotIn
  28.       values:
  29.       - ignore
  30.   objectSelector:
  31.     matchExpressions:
  32.     - key: hami.io/webhook
  33.       operator: NotIn
  34.       values:
  35.       - ignore
  36.   reinvocationPolicy: Never
  37.   rules:
  38.   - apiGroups:
  39.     - ""
  40.     apiVersions:
  41.     - v1
  42.     operations:
  43.     - CREATE
  44.     resources:
  45.     - pods
  46.     scope: '*'
  47.   sideEffects: None
  48.   timeoutSeconds: 10
复制代码
具体效果是在创建 Pod 时,kube-apiserver 会调用该 service 对应的 webhook,这样就注入了我们的自定义逻辑。
关注的对象为 Pod 的 CREATE 事件:
  1.   rules:
  2.   - apiGroups:
  3.     - ""
  4.     apiVersions:
  5.     - v1
  6.     operations:
  7.     - CREATE
  8.     resources:
  9.     - pods
  10.     scope: '*'
复制代码
但是不包括以下对象
  1.   namespaceSelector:
  2.     matchExpressions:
  3.     - key: hami.io/webhook
  4.       operator: NotIn
  5.       values:
  6.       - ignore
  7.   objectSelector:
  8.     matchExpressions:
  9.     - key: hami.io/webhook
  10.       operator: NotIn
  11.       values:
  12.       - ignore
复制代码
即:namespace 或者 资源对象上带 hami.io/webhook=ignore label 的都不走该 Webhook 逻辑。
请求的 Webhook 为
  1.     service:
  2.       name: vgpu-hami-scheduler
  3.       namespace: kube-system
  4.       path: /webhook
  5.       port: 443
复制代码
即:对于满足条件的 Pod 的 CREATE 时,kube-apiserver 会调用该 service 指定的服务,也就是我们的 hami-webhook。
接下来就开始分析 hami-webhook 具体做了什么。
源码分析

这个 Webhook 的具体实现如下:
  1. // pkg/scheduler/webhook.go#L52
  2. func (h *webhook) Handle(_ context.Context, req admission.Request) admission.Response {
  3.     pod := &corev1.Pod{}
  4.     err := h.decoder.Decode(req, pod)
  5.     if err != nil {
  6.        klog.Errorf("Failed to decode request: %v", err)
  7.        return admission.Errored(http.StatusBadRequest, err)
  8.     }
  9.     if len(pod.Spec.Containers) == 0 {
  10.        klog.Warningf(template+" - Denying admission as pod has no containers", req.Namespace, req.Name, req.UID)
  11.        return admission.Denied("pod has no containers")
  12.     }
  13.     klog.Infof(template, req.Namespace, req.Name, req.UID)
  14.     hasResource := false
  15.     for idx, ctr := range pod.Spec.Containers {
  16.        c := &pod.Spec.Containers[idx]
  17.        if ctr.SecurityContext != nil {
  18.           if ctr.SecurityContext.Privileged != nil && *ctr.SecurityContext.Privileged {
  19.              klog.Warningf(template+" - Denying admission as container %s is privileged", req.Namespace, req.Name, req.UID, c.Name)
  20.              continue
  21.           }
  22.        }
  23.        for _, val := range device.GetDevices() {
  24.           found, err := val.MutateAdmission(c)
  25.           if err != nil {
  26.              klog.Errorf("validating pod failed:%s", err.Error())
  27.              return admission.Errored(http.StatusInternalServerError, err)
  28.           }
  29.           hasResource = hasResource || found
  30.        }
  31.     }
  32.     if !hasResource {
  33.        klog.Infof(template+" - Allowing admission for pod: no resource found", req.Namespace, req.Name, req.UID)
  34.        //return admission.Allowed("no resource found")
  35.     } else if len(config.SchedulerName) > 0 {
  36.        pod.Spec.SchedulerName = config.SchedulerName
  37.     }
  38.     marshaledPod, err := json.Marshal(pod)
  39.     if err != nil {
  40.        klog.Errorf(template+" - Failed to marshal pod, error: %v", req.Namespace, req.Name, req.UID, err)
  41.        return admission.Errored(http.StatusInternalServerError, err)
  42.     }
  43.     return admission.PatchResponseFromRaw(req.Object.Raw, marshaledPod)
  44. }
复制代码
逻辑比较简单:

  • 1)判断 Pod 是否需要使用 HAMi-Scheduler 进行调度
  • 2)需要的话就修改 Pod 的 SchedulerName 字段为 hami-scheduler(名字可配置)
至此,核心部分就是如何判断该 Pod 是否需要使用 hami-scheduler 进行调度呢?
如何判断是否使用 hami-scheduler

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

首先对于特权模式的 Pod,HAMi 是直接忽略的
  1. if ctr.SecurityContext != nil {
  2.   if ctr.SecurityContext.Privileged != nil && *ctr.SecurityContext.Privileged {
  3.      klog.Warningf(template+" - Denying admission as container %s is privileged", req.Namespace, req.Name, req.UID, c.Name)
  4.      continue
  5.   }
  6. }
复制代码
因为开启特权模式之后,Pod 可以访问宿主机上的所有设备,再做限制也没意义了,因此这里直接忽略。
具体判断逻辑

然后根据 Pod 中的 Resource 来判断是否需要使用 hami-scheduler 进行调度:
  1. for _, val := range device.GetDevices() {
  2.     found, err := val.MutateAdmission(c)
  3.     if err != nil {
  4.        klog.Errorf("validating pod failed:%s", err.Error())
  5.        return admission.Errored(http.StatusInternalServerError, err)
  6.     }
  7.     hasResource = hasResource || found
  8. }
复制代码
如果 Pod Resource 中有申请 HAMi 这边支持的 vGPU 资源则,那么就需要使用 HAMi-Scheduler 进行调度。
而那些 Device 是 HAMi 支持的呢,就是之前 start 中初始化的:
  1. var devices map[string]Devices
  2. func GetDevices() map[string]Devices {
  3.     return devices
  4. }
  5. func InitDevices() {
  6.     devices = make(map[string]Devices)
  7.     DevicesToHandle = []string{}
  8.     devices[cambricon.CambriconMLUDevice] = cambricon.InitMLUDevice()
  9.     devices[nvidia.NvidiaGPUDevice] = nvidia.InitNvidiaDevice()
  10.     devices[hygon.HygonDCUDevice] = hygon.InitDCUDevice()
  11.     devices[iluvatar.IluvatarGPUDevice] = iluvatar.InitIluvatarDevice()
  12.     //devices[d.AscendDevice] = d.InitDevice()
  13.     //devices[ascend.Ascend310PName] = ascend.InitAscend310P()
  14.     DevicesToHandle = append(DevicesToHandle, nvidia.NvidiaGPUCommonWord)
  15.     DevicesToHandle = append(DevicesToHandle, cambricon.CambriconMLUCommonWord)
  16.     DevicesToHandle = append(DevicesToHandle, hygon.HygonDCUCommonWord)
  17.     DevicesToHandle = append(DevicesToHandle, iluvatar.IluvatarGPUCommonWord)
  18.     //DevicesToHandle = append(DevicesToHandle, d.AscendDevice)
  19.     //DevicesToHandle = append(DevicesToHandle, ascend.Ascend310PName)
  20.     for _, dev := range ascend.InitDevices() {
  21.        devices[dev.CommonWord()] = dev
  22.        DevicesToHandle = append(DevicesToHandle, dev.CommonWord())
  23.     }
  24. }
复制代码
devices 是一个全局变量, InitDevices 则是在初始化该变量,供 Webhook 中使用,包括 NVIDIA、海光、天数、昇腾等等。
这里以 NVIDIA 为例说明 HAMi 是如何判断一个 Pod 是否需要自己来调度的,MutateAdmission 具体实现如下:
  1. func (dev *NvidiaGPUDevices) MutateAdmission(ctr *corev1.Container) (bool, error) {
  2.     /*gpu related */
  3.     priority, ok := ctr.Resources.Limits[corev1.ResourceName(ResourcePriority)]
  4.     if ok {
  5.        ctr.Env = append(ctr.Env, corev1.EnvVar{
  6.           Name:  api.TaskPriority,
  7.           Value: fmt.Sprint(priority.Value()),
  8.        })
  9.     }
  10.     _, resourceNameOK := ctr.Resources.Limits[corev1.ResourceName(ResourceName)]
  11.     if resourceNameOK {
  12.        return resourceNameOK, nil
  13.     }
  14.     _, resourceCoresOK := ctr.Resources.Limits[corev1.ResourceName(ResourceCores)]
  15.     _, resourceMemOK := ctr.Resources.Limits[corev1.ResourceName(ResourceMem)]
  16.     _, resourceMemPercentageOK := ctr.Resources.Limits[corev1.ResourceName(ResourceMemPercentage)]
  17.     if resourceCoresOK || resourceMemOK || resourceMemPercentageOK {
  18.        if config.DefaultResourceNum > 0 {
  19.           ctr.Resources.Limits[corev1.ResourceName(ResourceName)] = *resource.NewQuantity(int64(config.DefaultResourceNum), resource.BinarySI)
  20.           resourceNameOK = true
  21.        }
  22.     }
  23.     if !resourceNameOK && OverwriteEnv {
  24.        ctr.Env = append(ctr.Env, corev1.EnvVar{
  25.           Name:  "NVIDIA_VISIBLE_DEVICES",
  26.           Value: "none",
  27.        })
  28.     }
  29.     return resourceNameOK, nil
  30. }
复制代码
首先判断如果 Pod 申请的 Resource 中有对应的 ResourceName 就直接返回 true
  1. _, resourceNameOK := ctr.Resources.Limits[corev1.ResourceName(ResourceName)]
  2. if resourceNameOK {
  3.    return resourceNameOK, nil
  4. }
复制代码
NVIDIA GPU 对应的 ResourceName 为:
  1. 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 资源的申请。
  1. _, resourceCoresOK := ctr.Resources.Limits[corev1.ResourceName(ResourceCores)]
  2. _, resourceMemOK := ctr.Resources.Limits[corev1.ResourceName(ResourceMem)]
  3. _, resourceMemPercentageOK := ctr.Resources.Limits[corev1.ResourceName(ResourceMemPercentage)]
  4. if resourceCoresOK || resourceMemOK || resourceMemPercentageOK {
  5.     if config.DefaultResourceNum > 0 {
  6.        ctr.Resources.Limits[corev1.ResourceName(ResourceName)] = *resource.NewQuantity(int64(config.DefaultResourceNum), resource.BinarySI)
  7.        resourceNameOK = true
  8.     }
  9. }
复制代码
修改 SchedulerName

对于上述满足条件的 Pod,需要由 HAMi-Scheduler 进行调度,Webhook 中会将 Pod 的 spec.schedulerName 改成 hami-scheduler。
具体如下:
  1. if !hasResource {
  2.     klog.Infof(template+" - Allowing admission for pod: no resource found", req.Namespace, req.Name, req.UID)
  3.     //return admission.Allowed("no resource found")
  4. } else if len(config.SchedulerName) > 0 {
  5.     pod.Spec.SchedulerName = config.SchedulerName
  6. }
复制代码
这样该 Pod 就会由 HAMi-Scheduler 进行调度了,接下来就是 HAMi-Scheduler 开始工作了。
这里也有一个特殊逻辑:如果创建时直接指定了 nodeName,那 Webhook 就会直接拒绝,因为指定 nodeName 说明 Pod 都不需要调度了,会直接到指定节点启动,但是没经过调度,可能该节点并没有足够的资源。
  1. if pod.Spec.NodeName != "" {
  2.         klog.Infof(template+" - Pod already has node assigned", req.Namespace, req.Name, req.UID)
  3.         return admission.Denied("pod has node assigned")
  4. }
复制代码
【Kubernetes 系列】持续更新中,搜索公众号【探索云原生】订阅,阅读更多文章。
2.png

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 实现了。

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
您需要登录后才可以回帖 登录 | 立即注册