背景
我們知道,如果在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
可以看到在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目前的不足作了一下說明。