我们知道 如果在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.2NVIDIA?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 否则返回NotReadyfunc?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当前的不足作了一下说明。
有 域名 怎么申请 网站备案 ?域名之后,还需要国内的空间,如 虚拟主机 ,或者 ...
操作场景 用户可以选择导出该区域公共镜像信息或用户在该区域拥有的私有镜像信息...
TOP云 (west.cn)5月3日消息,据外媒报道,一枚杂米 域名 beats3.com近日易主,...
TOP云 (west.cn)1月23日消息,今天上午,工信部官网公布:.center/.video/.soc...
本文转载自微信公众号「前端思维框架」,作者ViktorHub。转载本文请联系前端思维...
昨天17点03分,双拼 域名 difang.wang 在国内平台以2800元成交了,该 域名注册 ...
我们一般写应用都需要有后台管理系统,那么uni-app也不例外。 本次内容假设我们...
引言 Wireshark是IT技术人员必备大杀器,Wireshark分析数据包的能力十分强大,能...
TOP云 (west.cn)1月17日消息,近日双拼 域名 成交消息不断,据业内消息,就在...
SQL是用于数据分析和数据处理的最重要的编程语言之一,因此与数据科学相关的工作...