当前位置:主页 > 查看内容

NVIDIA GPU Operator分析六:NVIDIA GPU Operator原理分析

发布时间:2021-05-25 00:00| 位朋友查看

简介:背景 我们知道 如果在Kubernetes中支持GPU设备调度 需要做如下的工作 节点上安装nvidia驱动节点上安装nvidia-docker集群部署gpu?device?plugin 用于为调度到该节点的pod分配GPU设备。 除此之外 如果你需要监控集群GPU资源使用情况 你可能还需要安装 DCCM?exp……
背景

我们知道 如果在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?InstallerNVIDIA?Container?Toolkit?InstallerNVIDIA?Device?PluginDCGM?ExporterGFD每个组件都是以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

可以看到在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
}

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
}

简单总结一下Reconcile函数所做的事情

获取cluster-policy这个CR。初始化ctrl对象 需要用到cluster-policy中的配置 初始化的过程中将会注册负责安装各组件的函数 在接下来真正部署组件时会调用这些函数。通过for循环 ctrl对象会依次部署各组件 如果部署完某个组件后 发现该组件处于NotReady状态 那么会将事件重新扔进队列中再次处理 如果组件处于Ready状态 那么接着部署下一个组件。如果所有组件都部署成功 那么更新CR状态为Ready。

可以看到 整个安装组件的逻辑还是比较清晰的 接着看看ctrl初始化。

ClusterPolicyController对象的初始化操作

在Reconcile函数中 有这样一行代码

err? ?ctrl.init(r,?instance)

该行代码是初始化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
}

可以看到 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
}
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
}

以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

然后通过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

可以看到 对于一个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
}

判断一个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 )
}

基于上面的代码 现在有一个问题可以讨论一下 当在所有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
}
问题探讨

关于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
目前的不足

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当前的不足作了一下说明。


本文转自网络,原文链接:https://developer.aliyun.com/article/784292
本站部分内容转载于网络,版权归原作者所有,转载之目的在于传播更多优秀技术内容,如有侵权请联系QQ/微信:153890879删除,谢谢!

推荐图文

  • 周排行
  • 月排行
  • 总排行

随机推荐