背景
我们知道,如果在Kubernetes中支持GPU设备调度,需要做如下的工作:
-
节点上安装nvidia驱动
-
节点上安装nvidia-docker
-
集群部署gpu device plugin,用于为调度到该节点的pod分配GPU设备。
除此之外,如果你需要监控集群GPU资源使用情况,你可能还需要安装DCCM exporter结合Prometheus输出GPU资源监控信息。
要安装和管理这么多的组件,对于运维人员来说压力不小。基于此,NVIDIA开源了一款叫NVIDIA GPU Operator的工具,该工具基于Operator Framework实现,用于自动化管理上面我们提到的这些组件。
在之前的文章中,作者分别介绍了NVIDIA GPU Operator所涉及的每一个组件并且演示了如何手动部署这些组件,在本篇文章中将介绍详细介绍NVIDIA GPU Operator的工作原理。
Operator Framework介绍
NVIDIA GPU Operator是基于Operator Framework实现,所以在介绍NVIDIA GPU Operator之前先简单介绍一下Operator Framework,便于理解NVIDIA GPU Operator。
官方对Operator的介绍如下:“An Operator is a method of packaging, deploying and managing a Kubernetes application.”(即Operator是一种打包、部署、管理k8s应用的方式)。
Operator Framework采用的是Controller模式,什么是Controller模式呢?简单以下面这幅图介绍一下:
-
Controller可以有一个或多个Informer,Informer通过事件监听机制从APIServer处获取所关心的资源变化(创建、删除、更新等)。
-
当Informer监听到某个事件发生时,先把资源更新到本地cache中,然后会调用callback函数将该事件放进一个队列中(WorkQueue)。
-
在队列的另一端,有一个永不终止的控制循环不断从队列中取出事件。
-
从队列中取出的事件将会交给一个特定的函数处理(图中的Worker,在Operator Framework中一般称为Reconcile函数),这个函数的运行逻辑需要根据业务实现。
Operator Framework提供如下的工作流来开发一个Operator:
-
使用SDK创建一个新的Operator项目
-
添加自定义资源(CRD)以及定义相关的API
-
指定使用SDK API监听的资源
-
定义处理资源变更事件的函数(Reconcile函数)
-
使用Operator SDK构建并生成Operator部署清单文件
组件介绍
从前面的文章中,我们知道NVIDIA GPU Operator总共包含如下的几个组件:
-
NFD(Node Feature Discovery):用于给节点打上某些标签,这些标签包括cpu id、内核版本、操作系统版本、是不是GPU节点等,其中需要关注的标签是“nvidia.com/gpu.present=true”,如果节点存在该标签,那么说明该节点是GPU节点。
-
NVIDIA Driver Installer:基于容器的方式在节点上安装NVIDIA GPU驱动,在k8s集群中以DaemonSet方式部署,只有节点拥有标签“nvidia.com/gpu.present=true”时,DaemonSet控制的Pod才会在该节点上运行。
-
NVIDIA Container Toolkit Installer:能够实现在容器中使用GPU设备,在k8s集群中以DaemonSet方式部署,只有节点拥有标签“nvidia.com/gpu.present=true”时,DaemonSet控制的Pod才会在该节点上运行。
-
NVIDIA Device Plugin:NVIDIA Device Plugin用于实现将GPU设备以Kubernetes扩展资源的方式供用户使用,在k8s集群中以DaemonSet方式部署,只有节点拥有标签“nvidia.com/gpu.present=true”时,DaemonSet控制的Pod才会在该节点上运行。
-
DCGM Exporter:周期性的收集节点GPU设备的状态(当前温度、总的显存、已使用显存、使用率等),然后结合Prometheus和Grafana将这些指标用丰富的仪表盘展示给用户。在k8s集群中以DaemonSet方式部署,只有节点拥有标签“nvidia.com/gpu.present=true”时,DaemonSet控制的Pod才会在该节点上运行。
-
GFD(GPU Feature Discovery):用于收集节点的GPU设备属性(GPU驱动版本、GPU型号等),并将这些属性以节点标签的方式透出。在k8s集群中以DaemonSet方式部署,只有节点拥有标签“nvidia.com/gpu.present=true”时,DaemonSet控制的Pod才会在该节点上运行。
工作流程
NVIDIA GPU Operator的工作流程可以描述为:
-
NVIDIA GPU Operator依如下的顺序部署各个组件,并且如果前一个组件部署失败,那么其后面的组件将停止部署:
-
NVIDIA Driver Installer
-
NVIDIA Container Toolkit Installer
-
NVIDIA Device Plugin
-
DCGM Exporter
-
GFD
-
每个组件都是以DaemonSet方式部署,并且只有当节点存在标签nvidia.com/gpu.present=true时,各DaemonSet控制的Pod才会在节点上运行。
源码介绍
前提说明
-
GPU Operator的代码地址为: https://github.com/NVIDIA/gpu-operator.git
-
本文分析的代码的tag为1.6.2
NVIDIA GPU Operator的CRD
前面我们提到过Operator的开发流程,在开发流程中需要添加自定义资源(CRD),那么NVIDIA GPU Operator的CRD是怎样定义的呢?
GPU Operator定义了一个CRD: clusterpolicies.nvidia.com,clusterpolicies.nvidia.com这种CRD用于保存GPU Operator需要部署的各组件的配置信息。通过helm部署GPU Operator时,会部署一个名为cluster-policy的CR,可以通过如下的命令获取其内容:
$ kubectl get clusterpolicies.nvidia.com cluster-policy -o yaml apiVersion: nvidia.com/v1 kind: ClusterPolicy metadata: annotations: meta.helm.sh/release-name: operator meta.helm.sh/release-namespace: gpu creationTimestamp: "2021-04-10T05:04:52Z" generation: 1 labels: app.kubernetes.io/component: gpu-operator app.kubernetes.io/managed-by: Helm name: cluster-policy resourceVersion: "10582204" selfLink: /apis/nvidia.com/v1/clusterpolicies/cluster-policy uid: 0d44ab71-c64b-4b23-a74f-45087f8725c7 spec: dcgmExporter: args: - -f - /etc/dcgm-exporter/dcp-metrics-included.csv image: dcgm-exporter imagePullPolicy: IfNotPresent repository: nvcr.io/nvidia/k8s version: 2.1.4-2.2.0-ubuntu20.04 devicePlugin: args: - --mig-strategy=single - --pass-device-specs=true - --fail-on-init-error=true - --device-list-strategy=envvar - --nvidia-driver-root=/run/nvidia/driver image: k8s-device-plugin imagePullPolicy: IfNotPresent nodeSelector: nvidia.com/gpu.present: "true" repository: nvcr.io/nvidia securityContext: privileged: true version: v0.8.2-ubi8 driver: image: nvidia-driver imagePullPolicy: IfNotPresent licensingConfig: configMapName: "" nodeSelector: nvidia.com/gpu.present: "true" repoConfig: configMapName: "" destinationDir: "" repository: registry.cn-beijing.aliyuncs.com/happy365 securityContext: privileged: true seLinuxOptions: level: s0 tolerations: - effect: NoSchedule key: nvidia.com/gpu operator: Exists version: 450.102.04 gfd: discoveryIntervalSeconds: 60 image: gpu-feature-discovery imagePullPolicy: IfNotPresent migStrategy: single nodeSelector: nvidia.com/gpu.present: "true" repository: nvcr.io/nvidia version: v0.4.1 operator: defaultRuntime: docker validator: image: cuda-sample imagePullPolicy: IfNotPresent repository: nvcr.io/nvidia/k8s version: vectoradd-cuda10.2 toolkit: image: container-toolkit imagePullPolicy: IfNotPresent nodeSelector: nvidia.com/gpu.present: "true" repository: nvcr.io/nvidia/k8s securityContext: privileged: true seLinuxOptions: level: s0 tolerations: - key: CriticalAddonsOnly operator: Exists - effect: NoSchedule key: nvidia.com/gpu operator: Exists version: 1.4.3-ubi8 status: state: notReady
AI 代码解读
可以看到在CR的spec部分保存了各组件的配置信息,这些配置信息来源于helm chart的values.yaml。
另外,出了保存各组件的配置信息,在status部分,还有一个字段state保存GPU Operator状态。
NVIDIA GPU Operator监听的资源
可以在pkg/controller/clusterpolicy/clusterpolicy_controller.go中的add函数,找到GPU Operator所监听的资源。从代码中可以看到,NVIDIA GPU Operator需要监听三种资源变化:
-
NVIDIA GPU Operator自定义资源(CRD)发生变化
-
集群中的节点发生变化(比如集群添加节点,集群节点的标签发生变化等)
-
由NVIDIA GPU Operator创建的Pod发生变化(即各个DaemonSet控制的Pod发生变化)
// add adds a new Controller to mgr with r as the reconcile.Reconciler func add(mgr manager.Manager, r reconcile.Reconciler) error { // Create a new controller c, err := controller.New("clusterpolicy-controller", mgr, controller.Options{Reconciler: r}) if err != nil { return err } // Watch for changes to primary resource ClusterPolicy // 1.当NVIDIA GPU Operator自定义资源(CRD)发生变化时,需要通知GPU Operator进行处理 err = c.Watch(&source.Kind{Type: &gpuv1.ClusterPolicy{}}, &handler.EnqueueRequestForObject{}) if err != nil { return err } // Watch for changes to Node labels and requeue the owner ClusterPolicy // 2.当有新节点添加或者节点更新时,需要通知GPU Operator进行处理 err = addWatchNewGPUNode(c, mgr, r) if err != nil { return err } // TODO(user): Modify this to be the types you create that are owned by the primary resource // Watch for changes to secondary resource Pods and requeue the owner ClusterPolicy // 3.与NVIDIA GPU Operator相关的pod发生变化时,需要通知GPU Operator进行处理 err = c.Watch(&source.Kind{Type: &corev1.Pod{}}, &handler.EnqueueRequestForOwner{ IsController: true, OwnerType: &gpuv1.ClusterPolicy{}, }) if err != nil { return err } return nil }
AI 代码解读
Reconcile函数
前面介绍Operator Framework提到过,开发Operator时需要开发者根据业务场景实现Reconcile函数,用于处理Operator所监听的资源发生变化时,应该做出哪些操作。
接下来分析一下Reconcile函数的执行逻辑,其中传入的参数为从队列中取出的资源变化的事件。
func (r *ReconcileClusterPolicy) Reconcile(request reconcile.Request) (reconcile.Result, error) { ctx := log.WithValues("Request.Name", request.Name) ctx.Info("Reconciling ClusterPolicy") // 获取ClusterPolicy实例,GPU Operator中定义了一个名为clusterpolicies.nvidia.com的CRD。 // 用于保存其helm chart的values.yaml中各组件的配置信息,比如:镜像名称,启动命令等。 // 同时,在gpu operator的helm chart已定义了一个名为cluster-policy的CR,在安装helm chart时会自动安装该CR。 instance := &gpuv1.ClusterPolicy{} err := r.client.Get(context.TODO(), request.NamespacedName, instance) if err != nil { // 如果没有发现CR,证明该CR被删除了,不会将request重新放进事件队列中进行再一次处理。 if errors.IsNotFound(err) { return reconcile.Result{}, nil } // 否则返回错误,该请求会被放进事件队列中再次处理。 // Error reading the object - requeue the request. return reconcile.Result{}, err } // 如果获取的ClusterPolicy实例名称与当前保存的ClusterPolicy实例名称不一致 // 那么将实例状态设置为Ignored,同时结束函数,直接返回,并且request不会被放入队列中再次处理。 if ctrl.singleton != nil && ctrl.singleton.ObjectMeta.Name != instance.ObjectMeta.Name { instance.SetState(gpuv1.Ignored) return reconcile.Result{}, err } // 初始化ClusterPolicyController,初始化的操作后面会详细分析。 err = ctrl.init(r, instance) if err != nil { log.Error(err, "Failed to initialize ClusterPolicy controller") return reconcile.Result{}, err } // for循环用于依次部署各组件:nvidia driver、nvidia container toolkit、nvidia device plugin // dcgm exporter和gfd。 for { // ctrl.step函数用于部署各组件(nvidia driver、nvidia container toolkit等)并返回部署的组件的状态。 // 每执行一次ctrl.step(),那么有一个组件将会被部署 status, statusError := ctrl.step() // Update the CR status // 更新CR状态,首先获取CR instance = &gpuv1.ClusterPolicy{} err := r.client.Get(context.TODO(), request.NamespacedName, instance) if err != nil { log.Error(err, "Failed to get ClusterPolicy instance for status update") return reconcile.Result{RequeueAfter: time.Second * 5}, err } // 如果CR状态与当前部署的组件状态不一致,更新CR状态。 if instance.Status.State != status { instance.Status.State = status err = r.client.Status().Update(context.TODO(), instance) if err != nil { log.Error(err, "Failed to update ClusterPolicy status") return reconcile.Result{RequeueAfter: time.Second * 5}, err } } // 如果部署当前组件失败,那么将request放进事件队列,等待再次处理。 if statusError != nil { return reconcile.Result{RequeueAfter: time.Second * 5}, statusError } // 如果当前部署的组件的状态不是Ready的,那么将request放入队列,等待再次处理。 if status == gpuv1.NotReady { // If the resource is not ready, wait 5 secs and reconcile log.Info("ClusterPolicy step wasn't ready", "State:", status) return reconcile.Result{RequeueAfter: time.Second * 5}, nil } // 如果该组件是Ready状态,那么判断当前的组件是不是最后一个需要部署的组件,如果是,退出循环。 // 否则部署下一个组件。 if ctrl.last() { break } } // 更新CR状态,将其设置为Ready状态。 instance.SetState(gpuv1.Ready) return reconcile.Result{}, nil }
AI 代码解读
简单总结一下Reconcile函数所做的事情:
-
获取cluster-policy这个CR。
-
初始化ctrl对象(需要用到cluster-policy中的配置),初始化的过程中将会注册负责安装各组件的函数,在接下来真正部署组件时会调用这些函数。
-
通过for循环,ctrl对象会依次部署各组件,如果部署完某个组件后,发现该组件处于NotReady状态,那么会将事件重新扔进队列中再次处理;如果组件处于Ready状态,那么接着部署下一个组件。
-
如果所有组件都部署成功,那么更新CR状态为Ready。
可以看到,整个安装组件的逻辑还是比较清晰的,接着看看ctrl初始化。
ClusterPolicyController对象的初始化操作
在Reconcile函数中,有这样一行代码:
err = ctrl.init(r, instance)
AI 代码解读
该行代码是初始化ClusterPolicyController类型的实例ctrl,ctrl是真正执行组件安装的对象。init函数内容如下:
func (n *ClusterPolicyController) init(r *ReconcileClusterPolicy, i *gpuv1.ClusterPolicy) error { .... // 省略不关心的代码 // 将ClusterPolicy实例保存 n.singleton = i // 保存ReconcileClusterPolicy实例 n.rec = r // 初始化当前部署成功的组件的索引 n.idx = 0 // 如果当前没有安装组件的函数注册,那么调用addState函数开始执行注册操作。 // 注册后将会在ClusterPolicyController对象的step函数中依次调用这些函数,各组件将会被部署。 if len(n.controls) == 0 { promv1.AddToScheme(r.scheme) secv1.AddToScheme(r.scheme) // addState函数用户注册安装各组件的函数。 // 注册部署nvidia driver组件的函数。 addState(n, "/opt/gpu-operator/state-driver") // 注册部署nvidia container toolkit组件的函数。 addState(n, "/opt/gpu-operator/state-container-toolkit") // 注册部署nvidia device plugin组件的函数。 addState(n, "/opt/gpu-operator/state-device-plugin") // 注册校验nvidia device plugin是否正常的函数。 addState(n, "/opt/gpu-operator/state-device-plugin-validation") // 注册部署dcgm exporter组件的函数。 addState(n, "/opt/gpu-operator/state-monitoring") // 注册部署gfd组件的函数。 addState(n, "/opt/gpu-operator/gpu-feature-discovery") } // fetch all nodes and label gpu nodes // 获取所有节点并且为GPU节点打上标签nvidia.com/gpu.present=true err = n.labelGPUNodes() if err != nil { return err } return nil }
AI 代码解读
可以看到,init函数最重要的操作就是调用addState函数注册一些函数,这些函数定义了每一个组件的安装逻辑,这些函数将会在ctrl的step函数中使用,这里需要注意组件的添加顺序,组件的安装顺序就是现在的添加顺序。
addState函数
addState函数用于将定义各个组件的安装逻辑的函数注册到ctrl对象中,函数比较简单,主要就是调用addResourcesControls函数,addResourcesControls有两个返回值:
-
各组件所涉及的资源,比如NVIDIA Driver Installer组件包含:DaemonSet、ConfigMap、ServiceAccount、Role、RoleBinding等。
-
定义每种资源的安装逻辑函数,比如:NVIDIA Driver Installer组件涉及资源ServiceAccount、ConfigMap和DaemonSet。其中操作ServiceAccount、ConfigMap函数比较简单,直接创建即可;而操作Daemonset的函数还得根据操作系统类型(例如CentOS 7.x或Ubuntu )设置DaemonSet中Pod Spec的镜像,然后才能提交APIServer创建。
返回的函数和资源都将被保存下来,完成注册操作。
func addState(n *ClusterPolicyController, path string) error { // TODO check for path // 返回的res中包含不同种类的k8s资源。 // 返回的ctrl为部署该组件所要执行的一系列函数。 res, ctrl := addResourcesControls(path, n.openshift) // 将安装该组件所需的函数添加到n.controls这个数组中,完成函数注册。 n.controls = append(n.controls, ctrl) // 保存返回的资源。 n.resources = append(n.resources, res) return nil }
AI 代码解读
addResourcesControls函数
addResourcesControls函数用于获取给定的目录下的yaml文件,然后通过yaml文件中"kind"字段获取该yaml所描述的k8s资源类型,根据不同的资源类型注册不同的k8s资源处理函数。
func addResourcesControls(path, openshiftVersion string) (Resources, controlFunc) { res := Resources{} ctrl := controlFunc{} log.Info("Getting assets from: ", "path:", path) // 从给定的目录path下读取所有的文件 manifests := getAssetsFrom(path, openshiftVersion) // 创建解析yaml文件的工具 s := json.NewYAMLSerializer(json.DefaultMetaFactory, scheme.Scheme, scheme.Scheme) reg, _ := regexp.Compile(`\b(\w*kind:\w*)\B.*\b`) // 循环处理path目录下的文件 for _, m := range manifests { // 从当前文件中寻找kind关键字,获取k8s资源类型,比如:Daemonset、ServiceAccount等。 kind := reg.FindString(string(m)) slce := strings.Split(kind, ":") kind = strings.TrimSpace(slce[1]) log.Info("DEBUG: Looking for ", "Kind", kind, "in path:", path) // 判断kind类型 switch kind { // 如果是k8s中的ServiceAccount case "ServiceAccount": // 将yaml文件的内容反序列化为res.ServiceAccount对象 _, _, err := s.Decode(m, nil, &res.ServiceAccount) panicIfError(err) // 请注意ServiceAccount是一个函数, ctrl = append(ctrl, ServiceAccount) ...... // 省略其他代码 case "DaemonSet": _, _, err := s.Decode(m, nil, &res.DaemonSet) panicIfError(err) ctrl = append(ctrl, DaemonSet) ...... // 省略其他代码 default: log.Info("Unknown Resource", "Manifest", m, "Kind", kind) } } return res, ctrl }
AI 代码解读
以nvidia driver组件为例,与其相关的yaml组件存放在gpu-operator容器中的/opt/gpu-operator/state-driver,该目下的文件如下:
$ ls -l total 48 -rw-r--r-- 1 yangjunfeng staff 104B 3 10 15:50 0100_service_account.yaml -rw-r--r-- 1 yangjunfeng staff 259B 3 10 15:50 0200_role.yaml -rw-r--r-- 1 yangjunfeng staff 408B 3 10 15:50 0300_rolebinding.yaml -rw-r--r-- 1 yangjunfeng staff 613B 3 10 15:50 0400_configmap.yaml -rw-r--r-- 1 yangjunfeng staff 1.2K 3 10 15:50 0410_scc.openshift.yaml -rw-r--r-- 1 yangjunfeng staff 1.9K 3 10 15:51 0500_daemonset.yaml
AI 代码解读
然后通过for循环依次处理目录下的每个yaml文件,比如:第一次是0100_service_account.yaml,那么经过一个循环后,ctrl数组的内容为:[ServiceAccount],其中ServiceAccount为处理0100_service_account.yaml中的对象的函数,第二次是处理0200_role.yaml,经过该循环后,ctrl数组的内容为:
[ServiceAccount,Role],当对所有文件处理完成后,返回ctrl数组。
ServiceAccount函数和Daemonset函数
每一种k8s资源类型都有一个函数对应,每种函数的处理逻辑各不相同,接下来以ServiceAccount和Daemonset为例。
如果从yaml文件中读取了一个ServiceAccount对象,该对象将由ServiceAccount函数处理,函数内容如下:
func ServiceAccount(n ClusterPolicyController) (gpuv1.State, error) { state := n.idx // 获取service account对象,该对象即从yaml中读取的service account对象 obj := n.resources[state].ServiceAccount.DeepCopy() logger := log.WithValues("ServiceAccount", obj.Name, "Namespace", obj.Namespace) // 设置Reference if err := controllerutil.SetControllerReference(n.singleton, obj, n.rec.scheme); err != nil { return gpuv1.NotReady, err } // 创建该service account if err := n.rec.client.Create(context.TODO(), obj); err != nil { if errors.IsAlreadyExists(err) { logger.Info("Found Resource") return gpuv1.Ready, nil } logger.Info("Couldn't create", "Error", err) return gpuv1.NotReady, err } return gpuv1.Ready, nil }
AI 代码解读
可以看到,对于一个Servicce Account对象,处理它的函数只是简单的将其与ClusterPolicy关联,然后创建它。如果创建没有问题,那么就返回Ready状态;如果已存在,那么也返回Ready状态,否则返回NotReady状态。
Daemonset函数是需要重点理解的函数,通过它我们可以解释一些现象。
// DaemonSet creates Daemonset resource func DaemonSet(n ClusterPolicyController) (gpuv1.State, error) { state := n.idx // 获取daemonst对象 obj := n.resources[state].DaemonSet.DeepCopy() logger := log.WithValues("DaemonSet", obj.Name, "Namespace", obj.Namespace) // 预处理该daemonset对象,这里的预处理是对该daemonset的某些域进行赋值处理, // 以nvidia driver组件的daemonset(名为nvidia-driver-daemonset)为例,preProcessDaemonSet是将ClusterPolicy这个CR中关于 // nvidia-driver-daemonset的配置赋值到该daemonset对象中。 err := preProcessDaemonSet(obj, n) if err != nil { logger.Info("Could not pre-process", "Error", err) return gpuv1.NotReady, err } // 关联该daemonset与ClusterPolicy对象 if err := controllerutil.SetControllerReference(n.singleton, obj, n.rec.scheme); err != nil { return gpuv1.NotReady, err } // 创建该daemonset if err := n.rec.client.Create(context.TODO(), obj); err != nil { if errors.IsAlreadyExists(err) { logger.Info("Found Resource") return isDaemonSetReady(obj.Name, n), nil } logger.Info("Couldn't create", "Error", err) return gpuv1.NotReady, err } // 检查该daemonset是否Ready return isDaemonSetReady(obj.Name, n), nil }
AI 代码解读
判断一个daemonset是否Ready是由isDaemonSetReady函数完成,主要逻辑如下:
-
通过DaemonSet的label寻找该DaemonSet,如果没有搜索到,那么返回NotReady
-
如果该daemonset的NumberUnavailable不为0,那么直接返回NotReady
-
该DaemonSet所控制的pod的状态如果都是Running,返回Ready,否则返回NotReady
func isDaemonSetReady(name string, n ClusterPolicyController) gpuv1.State { opts := []client.ListOption{ client.MatchingLabels{"app": name}, } // 通过label获取目标daemonset log.Info("DEBUG: DaemonSet", "LabelSelector", fmt.Sprintf("app=%s", name)) list := &appsv1.DaemonSetList{} err := n.rec.client.List(context.TODO(), list, opts...) if err != nil { log.Info("Could not get DaemonSetList", err) } // 如果没有发现daemonset,返回NotReady log.Info("DEBUG: DaemonSet", "NumberOfDaemonSets", len(list.Items)) if len(list.Items) == 0 { return gpuv1.NotReady } ds := list.Items[0] log.Info("DEBUG: DaemonSet", "NumberUnavailable", ds.Status.NumberUnavailable) // 如果该daemonset的NumberUnavailable不为0,那么直接返回NotReady if ds.Status.NumberUnavailable != 0 { return gpuv1.NotReady } // 只有所有pod都是Running时,该daemonset才算Ready return isPodReady(name, n, "Running") }
AI 代码解读
基于上面的代码,现在有一个问题可以讨论一下:当在所有GPU节点上安装nvidia driver时,如果有一个节点安装失败了,那么会发生什么情况?——从代码中可以知道,只有当该DaemonSet所有pod都处于Running时,该DaemonSet才是Ready状态,所以如果有一个节点安装失败了,那么DaemonSet在该节点的pod必然是非Running状态,此时该DaemonSet是NotReady状态,也就是安装nvidia driver组件获得状态是NotReady,那么GPU Operator将不会继续安装接下来的组件。
ClusterPolicyController的部署组件操作
ctrl部署各组件的操作是由其step函数完成的,如果该函数被调用一次,那么就有一个组件被安装。
func (n *ClusterPolicyController) step() (gpuv1.State, error) { // n.idx指示当前待安装的组件的索引 // 通过该索引可以获取安装组件的函数列表,例如我们之前举的例子,nvidia driver组件的 // 目录下有Service Account、Role、RoleBinding、ConfigMap、Daemonset等对象 // 那么n.controls[n.idx]中函数列表为:[ServiceAccount,Role,RoleBinding,ConfigMap,Daemonset] // 然后依次执行列表中的函数,如果有一个函数返回NotReady,那么将不会创建其后面的对象,并返回 // NotReady for _, fs := range n.controls[n.idx] { stat, err := fs(*n) if err != nil { return stat, err } if stat != gpuv1.Ready { return stat, nil } } // 索引值加1,指向下一个待安装的组件 n.idx = n.idx + 1 // 如果所有函数都返回Ready状态,那么才返step函数才返回Ready状态。 return gpuv1.Ready, nil }
AI 代码解读
问题探讨
关于NVIDIA GPU Operator,有一些问题可以讨论一下。
问题1: 各个组件都是以DaemonSet方式进行部署,那么NVIDIA GPU Operator是一次把所有DaemonSet都部署到集群中吗?
答:从前面的源码分析中可以看到,NVIDIA GPU Operator是一个组件一个组件部署的,如果前一个组件部署失败,后一个组件不会部署,自然而然后一个组件的DaemonSet也不会部署下去。
问题2:假设现在集群有三个GPU节点,在安装NVIDIA GPU Driver时,有两个GPU节点安装成功,一个GPU节点安装不成功,后续组件会接着安装吗?
答:不会,从前面的源码分析中可以看到,某个DaemonSet如果是Ready需要满足其所有Pod的状态都是Running,现在有一个节点安装失败,那么该DaemonSet在节点上部署的Pod将不会是Running状态,该DaemonSet返回NotReady状态,导致组件安装失败,后续组件将不会安装。
问题3:如果NVIDIA GPU Operator已经成功在集群中运行,并且集群中GPU节点已成功安装各个组件,如果此时有一个新的GPU节点加入到集群中,因为此时集群中已部署各组件,会不会出现安装GPU驱动的Pod还未处于Running,而NVIDIA Device plugin的Pod先处于Running,然后检查到节点没有驱动,NVIDIA Device plugin这个Pod进入Error状态?
答:不会,后面的组件的Pod中都存在一个InitContainer,都会做相应的检查,以NVIDIA Container Toolkit为例,其Pod中存在一个InitContainer用于检查节点GPU驱动是否安装成功。
initContainers: - args: - export SYS_LIBRARY_PATH=$(ldconfig -v 2>/dev/null | grep -v '^[[:space:]]' | cut -d':' -f1 | tr '[[:space:]]' ':'); export NVIDIA_LIBRARY_PATH=/run/nvidia/driver/usr/lib/x86_64-linux-gnu/:/run/nvidia/driver/usr/lib64; export LD_LIBRARY_PATH=${SYS_LIBRARY_PATH}:${NVIDIA_LIBRARY_PATH}; echo ${LD_LIBRARY_PATH}; export PATH=/run/nvidia/driver/usr/bin/:${PATH}; until nvidia-smi; do echo waiting for nvidia drivers to be loaded; sleep 5; done
AI 代码解读
目前的不足
NVIDIA GPU Operator的优点这里有不做多的介绍,有兴趣可以参考官方文档。这里还是想分析一下NVIDIA GPU Operator当前存在的一些不足,在本系列之前的文章中,我们分析了每个组件并手动安装了这些组件,也对一些组件的安装做出了缺点说明,现在总结一下这些缺点:
-
基于容器安装NVIDIA GPU驱动的方式目前还不太稳定,在GPU节点上如果重启Pod,会导致Pod重启失败,报驱动正在使用的错误,解决办法只有重启节点。
-
基于容器安装NVIDIA GPU驱动的方式目前还是区分操作系统类型,比如基于CentOS7基础docker镜像构建的docker镜像不能运行在操作系统为Ubuntu的k8s节点上。
-
基于容器安装NVIDIA Container Toolkit方式目前还不能自动识别节点的Container Runtime是docker还是containerd并执行相应的安装操作,这需要用户在安装NVIDIA GPU Operator时指定Container Runtime,同时也造成了集群的节点必须安装相同的Container Runtime。
-
在监控方面,目前NVIDIA GPU Operator只能提供以节点维度的GPU资源监控方案,而缺乏基于Pod或者基于集群维度的GPU资源监控仪表盘。
总结
本篇文章从源码的角度分析了NVIDIA GPU Operator,并依据源码给了一些问题的探讨,最后对NVIDIA GPU Operator当前的不足作了一下说明。