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