掘金 人工智能 07月24日 14:10
HAMi vGPU 原理分析 Part2:hami-webhook 原理分析
index_new5.html
../../../zaker_core/zaker_tpl_static/wap/tpl_guoji1.html

 

本文深入解析了 HAMi 系统的 hami-scheduler 组件,特别是其 webhook 的工作机制。当用户创建 Pod 并申请 vGPU 资源时,HAMi 的 Mutating Webhook 会自动拦截,并将 Pod 的调度器指向 hami-scheduler。该 webhook 能够识别 NVIDIA、寒武纪、海光等多种 GPU 资源,并根据 Pod 的资源申请(如 nvidia.com/gpu、gpucore 等)来判断是否需要由 hami-scheduler 处理。文章还详细说明了特权模式 Pod 和指定 nodeName 的 Pod 在此流程中的特殊处理方式,解释了为何特权模式 Pod 可能出现无法调度的现象。整体而言,HAMi Webhook 扮演着关键的流量引导角色,确保 vGPU 资源能够被 HAMi-Scheduler 有效调度。

💡 HAMi Webhook 作为 Mutating Webhook,其核心作用是在 Pod 创建时,根据 Pod 对 vGPU 资源的申请,自动修改 Pod 的 `spec.schedulerName` 字段,将其指向 `hami-scheduler`。这一过程的触发条件是 Pod 的容器中申请了 HAMi 支持的特定 GPU 资源,例如 `nvidia.com/gpu`、`iluvatar.ai/vgpu`、`hygon.com/dcunum`、`cambricon.com/mlu` 等。如果 Pod 申请了这些资源,webhook 会将 `schedulerName` 设置为 `hami-scheduler`,从而确保后续的调度流程由 HAMi 来管理。

🛡️ 对于特权模式(Privileged)的 Pod,HAMi Webhook 会选择忽略。这意味着即使这类 Pod 申请了 vGPU 资源,它们的 `schedulerName` 也不会被修改为 `hami-scheduler`,而是继续由 Kubernetes 的默认调度器 (`default-scheduler`) 进行调度。这一设计是由于特权模式下的 Pod 可以访问宿主机所有设备,进行额外的调度限制意义不大。然而,这也会导致一个潜在问题:如果 `default-scheduler` 无法识别 Pod 所需的虚拟 GPU 资源(如 `gpucore`、`gpumem`),则该 Pod 可能会一直处于 Pending 状态,无法被成功调度。

🚫 当用户在创建 Pod 时,如果直接指定了 `spec.nodeName`,HAMi Webhook 会直接拒绝该 Pod 的创建。这是因为指定 `nodeName` 表明 Pod 不需要经过调度过程,而是直接被指派到特定节点。在这种情况下,HAMi Webhook 会拦截,防止 Pod 创建,以避免潜在的资源分配问题,因为直接调度可能绕过了 HAMi-Scheduler 的资源感知和分配逻辑。

⚙️ 除了直接申请 `ResourceName`(如 `nvidia.com/gpu`)外,HAMi Webhook 还能识别对 GPU 核心数 (`ResourceCores`)、内存 (`ResourceMem`) 或内存百分比 (`ResourceMemPercentage`) 的申请。如果 Pod 申请了这些资源,并且 `hami-scheduler` 的配置中 `DefaultResourceNum` 大于 0,Webhook 会自动为 Pod 添加 `nvidia.com/gpu` 资源的申请,并将其 `schedulerName` 改为 `hami-scheduler`。这为用户提供了更灵活的 GPU 资源申请方式,并能自动适配 HAMi 的调度。

📈 HAMi Webhook 的工作流程可以通过以下步骤概括:用户创建包含 vGPU 资源的 Pod -> Kubernetes API Server 根据 `MutatingWebhookConfiguration` 配置调用 HAMi Webhook -> HAMi Webhook 识别 Pod 中的 vGPU 资源申请,并修改 Pod 的 `schedulerName` 为 `hami-scheduler` (除非是特权模式或指定了 nodeName) -> Pod 由 `hami-scheduler` 进行后续调度。

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

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

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

主要包括以下几个问题:

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

以下分析基于 HAMi v2.4.0

1. hami-scheduler 启动命令

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 以及 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 -oyamlapiVersion: admissionregistration.k8s.io/v1kind: MutatingWebhookConfigurationmetadata:  annotations:    meta.helm.sh/release-name: vgpu    meta.helm.sh/release-namespace: kube-system  labels:    app.kubernetes.io/managed-by: Helm  name: vgpu-hami-webhookwebhooks:- 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#L52func (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)}

逻辑比较简单:

至此,核心部分就是如何判断该 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]Devicesfunc 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 申请了 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 工作流程如下:

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

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

Fish AI Reader

Fish AI Reader

AI辅助创作,多种专业模板,深度分析,高质量内容生成。从观点提取到深度思考,FishAI为您提供全方位的创作支持。新版本引入自定义参数,让您的创作更加个性化和精准。

FishAI

FishAI

鱼阅,AI 时代的下一个智能信息助手,助你摆脱信息焦虑

联系邮箱 441953276@qq.com

相关标签

HAMi Kubernetes vGPU Scheduler Webhook
相关文章