背景
我们知道,如果在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 driver的组件
- 安装nvidia container toolkit的组件
- 安装nvidia devcie plugin的组件
- 安装nvidia dcgm exporter组件
- 安装gpu feature discovery组件
本系列文章不打算一上来就开始讲NVIDIA GPU Operator,而是先把各个组件的安装详细的分析一下,然后手动安装这些组件,最后再来分析NVIDIA GPU Operator就比较简单了。
在本篇文章中,我们将详细介绍NVIDIA GPU Operator安装NVIDIA Container Toolkit组件的原理。
NVIDIA Container Toolkit简介
如果您需要在容器中使用NVIDIA GPU设备,那么NVIDIA Container Toolkit是必不可少的组件。它的主要作用是将NVIDIA GPU设备挂载到容器中。
支持docker的NVIDIA Container Tookit由如下的组件构成:
- nvidia-docker2
- nvidia-container-runtime
- nvidia-container-toolkit
- libnvidia-container
下面这幅架构图来自NVIDIA官网,详细介绍了各个组件的关系。
libnvidia-container介绍
libnvidia-container提供了一个库和简单的CLI工具,以实现在容器当中支持使用GPU设备的目标。
nvidia-container-toolkit介绍
nvidia-container-toolkit是一个实现了runC prestart hook接口的脚本,该脚本在runC创建一个容器之后,启动该容器之前调用,其主要作用就是修改与容器相关联的config.json,注入一些在容器中使用NVIDIA GPU设备所需要的一些信息(比如:需要挂载哪些GPU设备到容器当中)。
nvidia-container-runtime介绍
nvidia-container-runtime主要用于将容器runC spec作为输入,然后将nvidia-container-toolkit脚本作为一个prestart hook注入到runC spec中,将修改后的runC spec交给runC处理。
nvidia-docker2介绍
nvidia-docker2只适用于docker,其主要作用是可以通过环境变量指定容器需要使用节点上哪些GPU。
基于容器安装NVIDIA Container Toolkit
基于容器安装NVIDIA Container Toolkit所涉及的脚本请参考项目container-config,项目所涉及的代码在src目录下,有几个脚本需要说明一下:
- run.go:容器启动时的执行脚本(入口),由go语言编写,即容器中/work/nvidia-toolkit这个二进制文件。
- toolkit.sh:用于安装NVIDIA Container Toolkit,/work/nvidia-toolkit将会调用该脚本。
- docker.go:由go语言编写,该源文件将会编译成容器中/work/docker二进制文件,用户更新节点/etc/docker/daemon.json并重启docker进程使配置生效,/work/docker同样也会被/work/nvidia-toolkit调用。
1.镜像构建
以Dockerfile.ubi8为例进行分析,比较重要的是如下的这两步:
- 将以.go文件编译成二进制文件。
RUN go build -o nvidia-toolkit run.go
RUN go build -o containerd containerd.go
RUN go build -o crio crio.go
RUN go build -o docker docker.go
RUN go build -o toolkit toolkit.go
RUN rm -rf go.* && \
rm -rf *.go && \
rm -rf vendor
- 安装包libnvidia-container1,libnvidia-container-tools,nvidia-container-toolkit, nvidia-container-runtime,然后容器启动时,将这些包中的二进制文件拷贝到宿主机上。
RUN /bin/bash -c " \
yum install -y procps \
libnvidia-container1-\${LIBNVIDIA_CONTAINER_VERSION/-/-0.1.} \
libnvidia-container-tools-\${LIBNVIDIA_CONTAINER_VERSION/-/-0.1.} \
nvidia-container-toolkit-\${NVIDIA_CONTAINER_TOOLKIT_VERSION/-/-0.1.} \
nvidia-container-runtime-\${NVIDIA_CONTAINER_RUNTIME_VERSION/-/-0.1.}"
2.挂载相关目录到容器
在启动容器时,需要将节点上如下的目录挂载到容器中。
- /etc/docker => /etc/docker(仅对docker有效,对containerd需要挂载/etc/containerd):因为需要修改节点/etc/docker/daemon.json这个文件,然后重启docker服务,NVIDIA Container Toolkit才会生效。
- /run/nvidia => /run/nvidia: 前面我们介绍过,基于容器安装NVIDIA驱动后,节点的/run/nvidia/driver其实是整个driver的rootfs,NVIDIA Container Toolkit需要使用到该目录。
- /usr/local/nvidia => /usr/local/nvidia:NVIDIA Container Toolkit将会安装到节点的/usr/local/nvidia目录下,所以该目录需要挂载。
- /var/run => /var/run:重启docker的过程中,需要用到/var/run/docker.sock这个文件,所以该目录需要挂载。
3.安装过程说明
整个安装过程由nvidia-toolkit这个工具完成,容器的启动命令为:
nvidia-toolkit /usr/local/nvidia
其中,/usr/local/nvidia为NVIDIA Container Toolkit的安装路径。
nvidia-toolkit(由run.go编译而成)这个工具主要做如下的几件事:
- 将自身的进程ID写入/run/nvidia/toolkit.pid中,并启动goroutine捕获系统信号,执行退出清理操作(删除已安装的NVIDIA Container Toolkit,当容器被kill时,会执行卸载NVIDIA Container Toolkit操作)。
- 如果节点上的/usr/local/nvidia/toolkit存在,表明该节点已经安装了NVIDIA Container Toolkit,那么删除/usr/local/nvidia/toolkit这个目录,重新安装。
- 调用toolkit.sh执行安装操作。
- 调用/work/docker(由docker.go编译而成)命令修改docker的配置(/etc/docker/daemon.json),并重启docker。
- 处于sleep状态,等待系统信号。
// Run runs the core logic of the CLI
func Run(c *cli.Context) error {
err := VerifyFlags()
if err != nil {
return fmt.Errorf("unable to verify flags: %v", err)
}
// 1.初始化,主要是将自身进程id写入/run/nvidia/toolkit.pid,并启动goroutine捕获系统信号
err = Initialize()
if err != nil {
return fmt.Errorf("unable to initialize: %v", err)
}
defer Shutdown()
// 2.调用toolkit.sh执行安装操作
err = InstallToolkit()
if err != nil {
return fmt.Errorf("unable to install toolkit: %v", err)
}
// 修改runtime配置,以docker为例,主要是修改/etc/docker/daemon.json这个文件
err = SetupRuntime()
if err != nil {
return fmt.Errorf("unable to setup runtime: %v", err)
}
if !noDaemonFlag {
err = WaitForSignal()
if err != nil {
return fmt.Errorf("unable to wait for signal: %v", err)
}
err = CleanupRuntime()
if err != nil {
return fmt.Errorf("unable to cleanup runtime: %v", err)
}
}
return nil
}
toolkit.sh这个脚本主要完成如下的一些事件:
- 将容器中NVIDIA Container Toolkit组件所涉及的命令行工具和库文件移动到/usr/local/nvidia/toolkit目录下
$ ls /usr/local/nvidia/toolkit/
libnvidia-container.so.1 nvidia-container-cli.real nvidia-container-runtime.real
libnvidia-container.so.1.3.2 nvidia-container-runtime nvidia-container-toolkit
nvidia-container-cli nvidia-container-runtime-hook nvidia-container-toolkit.real
- 在 /usr/local/nvidia/toolkit/.config/nvidia-container-runtime创建nvidia-container-runtime的配置文件config.toml,并设置nvidia-container-cli.root的值为/run/nvidia/driver。
$ cat /usr/local/nvidia/toolkit/.config/nvidia-container-runtime/config.toml
disable-require = false
#swarm-resource = "DOCKER_RESOURCE_GPU"
#accept-nvidia-visible-devices-envvar-when-unprivileged = true
#accept-nvidia-visible-devices-as-volume-mounts = false
[nvidia-container-cli]
root = "/run/nvidia/driver"
#path = "/usr/bin/nvidia-container-cli"
environment = []
#debug = "/var/log/nvidia-container-toolkit.log"
#ldcache = "/etc/ld.so.cache"
load-kmods = true
#no-cgroups = false
#user = "root:video"
ldconfig = "@/sbin/ldconfig"
[nvidia-container-runtime]
#debug = "/var/log/nvidia-container-runtime.log"
整个安装过程还是比较清晰的,如果需要了解更详细的内容,可以去读一下这些脚本。
/work/docker(针对Runtime为docker)主要是修改/etc/docker/daemon.json的Runtime为nvidia,并重启docker。
"runtimes": {
"nvidia": {
"args": [],
"path": "/usr/local/nvidia/toolkit/nvidia-container-runtime"
}
},
在K8s集群的节点上安装NVIDIA Container Toolkit
这一部分将演示如何基于容器的方式为k8s集群的节点安装NVIDIA Container Toolkit。
前提条件
在执行安装操作前,你需要确认以下的条件是否满足:
- k8s集群节点上不能安装NVIDIA Container Toolkit,如果已经安装,需要卸载掉。
- k8s集群节点已经通过基于容器的方式安装了NVIDIA驱动,如果还没安装驱动,请参考本系列之前的文章《NVIDIA GPU Operator分析一:NVIDIA Driver安装》。
- 本次演示的集群节点的操作系统为Centos7.7,如果是其他类型的操作,不保证安装步骤是否可用。
安装步骤
1.下载gpu-opeator源码。
$ git clone -b 1.6.2 https://github.com/NVIDIA/gpu-operator.git
$ cd gpu-operator
$ export GPU_OPERATOR=$(pwd)
2.确认节点NVIDIA驱动已经安装。
$ kubectl get po -n gpu-operator-resources -l app=nvidia-driver-daemonset
NAME READY STATUS RESTARTS AGE
nvidia-driver-daemonset-4kxr2 1/1 Running 0 10m
nvidia-driver-daemonset-8fdqz 1/1 Running 0 10m
nvidia-driver-daemonset-ng6jn 1/1 Running 0 10m
# 进入容器执行nvidia-smi,检查能否执行成功。
$ kubectl exec -ti nvidia-driver-daemonset-4kxr2 -n gpu-operator-resources -- nvidia-smi
Thu Mar 25 12:29:43 2021
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 450.102.04 Driver Version: 450.102.04 CUDA Version: 11.0 |
|-------------------------------+----------------------+----------------------+
| GPU Name Persistence-M| Bus-Id Disp.A | Volatile Uncorr. ECC |
| Fan Temp Perf Pwr:Usage/Cap| Memory-Usage | GPU-Util Compute M. |
| | | MIG M. |
|===============================+======================+======================|
| 0 Tesla V100-SXM2... On | 00000000:00:07.0 Off | 0 |
| N/A 29C P0 24W / 300W | 0MiB / 16160MiB | 0% Default |
| | | N/A |
+-------------------------------+----------------------+----------------------+
+-----------------------------------------------------------------------------+
| Processes: |
| GPU GI CI PID Type Process name GPU Memory |
| ID ID Usage |
|=============================================================================|
| No running processes found |
+-----------------------------------------------------------------------------+
3.确认节点已经打了标签nvidia.com/gpu.present=true。
$ kubectl get nodes -L nvidia.com/gpu.present
NAME STATUS ROLES AGE VERSION GPU.PRESENT
cn-beijing.192.168.8.44 Ready <none> 13d v1.16.9-aliyun.1 true
cn-beijing.192.168.8.45 Ready <none> 13d v1.16.9-aliyun.1 true
cn-beijing.192.168.8.46 Ready <none> 13d v1.16.9-aliyun.1 true
cn-beijing.192.168.9.159 Ready master 13d v1.16.9-aliyun.1
cn-beijing.192.168.9.160 Ready master 13d v1.16.9-aliyun.1
cn-beijing.192.168.9.161 Ready master 13d v1.16.9-aliyun.1
4.修改assets/state-container-toolkit/0300_rolebinding.yaml,注释两个字段,否则无法提交:
- 将userNames这一行和其后面的一行注释。
#userNames:
#- system:serviceaccount:gpu-operator:nvidia-container-toolkit
- 将roleRef.namespace这一行注释。
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: nvidia-container-toolkit
#namespace: gpu-operator-resources
5.修改assets/state-container-toolkit/0400_container_toolkit.yml这个文件。对于1.6.2这个版本,直接提交这个yaml会失败,所以需要做如下修改:
apiVersion: apps/v1
kind: DaemonSet
metadata:
labels:
app: nvidia-container-toolkit-daemonset
name: nvidia-container-toolkit-daemonset
namespace: gpu-operator-resources
annotations:
openshift.io/scc: hostmount-anyuid
spec:
selector:
matchLabels:
app: nvidia-container-toolkit-daemonset
template:
metadata:
# Mark this pod as a critical add-on; when enabled, the critical add-on scheduler
# reserves resources for critical add-on pods so that they can be rescheduled after
# a failure. This annotation works in tandem with the toleration below.
annotations:
scheduler.alpha.kubernetes.io/critical-pod: ""
labels:
app: nvidia-container-toolkit-daemonset
spec:
tolerations:
# Allow this pod to be rescheduled while the node is in "critical add-ons only" mode.
# This, along with the annotation above marks this pod as a critical add-on.
- key: CriticalAddonsOnly
operator: Exists
- key: nvidia.com/gpu
operator: Exists
effect: NoSchedule
serviceAccount: nvidia-container-toolkit
hostPID: true
initContainers:
- name: driver-validation
# 替换initContainer的IMAGE
image: "nvcr.io/nvidia/cuda@sha256:ed723a1339cddd75eb9f2be2f3476edf497a1b189c10c9bf9eb8da4a16a51a59"
imagePullPolicy: IfNotPresent
command: ['sh', '-c']
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"]
securityContext:
privileged: true
seLinuxOptions:
level: "s0"
volumeMounts:
- name: nvidia-install-path
mountPath: /run/nvidia
mountPropagation: Bidirectional
containers:
# 替换container的IMAGE
- image: "nvcr.io/nvidia/k8s/container-toolkit:1.4.3-ubi8"
args: ["/usr/local/nvidia"]
env:
- name: RUNTIME_ARGS
value: ""
imagePullPolicy: IfNotPresent
name: nvidia-container-toolkit-ctr
securityContext:
privileged: true
seLinuxOptions:
level: "s0"
# 需要添加挂载点/etc/docker,/var/run
volumeMounts:
- name: docker-config
mountPath: /etc/docker
- name: docker-socket
mountPath: /var/run
- name: nvidia-install-path
mountPath: /run/nvidia
mountPropagation: Bidirectional
- name: nvidia-local
mountPath: /usr/local/nvidia
- name: crio-hooks
mountPath: /usr/share/containers/oci/hooks.d
volumes:
- name: nvidia-install-path
hostPath:
path: /run/nvidia
# 将节点的/etc/docker挂载到容器的/etc/docker
- name: docker-config
hostPath:
path: /etc/docker
# 将节点的/var/run挂载到容器的/var/run
- name: docker-socket
hostPath:
path: /var/run
- name: nvidia-local
hostPath:
path: /usr/local/nvidia
- name: crio-hooks
hostPath:
path: /run/containers/oci/hooks.d
nodeSelector:
nvidia.com/gpu.present: "true"
6.提交应用。
$ kubectl apply -f assets/state-container-toolkit
7.检查所有pod是否处于running。
$ kubectl get po -n gpu-operator-resources -l app=nvidia-container-toolkit-daemonset
NAME READY STATUS RESTARTS AGE
nvidia-container-toolkit-daemonset-6dksq 1/1 Running 0 14h
nvidia-container-toolkit-daemonset-q7f2l 1/1 Running 0 14h
nvidia-container-toolkit-daemonset-rv79v 1/1 Running 0 14h
8.查看pod日志。
$ kubectl logs nvidia-container-toolkit-daemonset-6dksq -n gpu-operator-resources --tail 10
time="2021-03-25T12:25:55Z" level=info msg="Parsing arguments: [/usr/local/nvidia/toolkit]"
time="2021-03-25T12:25:55Z" level=info msg="Successfully parsed arguments"
time="2021-03-25T12:25:55Z" level=info msg="Loading config: /etc/docker/daemon.json"
time="2021-03-25T12:25:55Z" level=info msg="Successfully loaded config"
time="2021-03-25T12:25:55Z" level=info msg="Flushing config"
time="2021-03-25T12:25:55Z" level=info msg="Successfully flushed config"
time="2021-03-25T12:25:55Z" level=info msg="Sending SIGHUP signal to docker"
time="2021-03-25T12:25:55Z" level=info msg="Successfully signaled docker"
time="2021-03-25T12:25:55Z" level=info msg="Completed 'setup' for docker"
time="2021-03-25T12:25:55Z" level=info msg="Waiting for signal"
可以看到,nvidia-container-toolkit已经安装成功了。
缺点
目前,基于容器安装NVIDIA Container Toolkit组件有如下的一个缺点:集群中所有节点都必须安装同一种Runtime,而且如果Runtime不是Docker,还需要特别指定。如果一个集群某些节点的Runtime为Docker,而另一些节点的Runtime为Containerd这种情况是不允许的。
总结
本篇文章说明了在k8s集群中基于容器安装NVIDIA Container Toolkit的原理。与安装驱动不同的是,一般情况下,我们无需定制安装NVIDIA Container Toolkit组件的docker镜像,直接使用官方提供的即可。