网站首页 文章专栏 一个nvidia-docker引发k8s崩溃的问题探讨
AIPLAT平台出现无法启动容器,报如图1所示错误信息,重启后恢复正常。如何去定位、解决此类错误呢?
首先,查看错误提示:nvidia-container-cli: initialization error: cuda error: unknown error。
然后,进一步追踪操作信息发现,该错误发生在nvidia-docker执行如下命令时:
nvidia-container-cli --load-kmods configure --ldconfig=@/sbin/ldconfig --device=GPU-2fb041ff-6df3-4d00-772d-efb3139a17a1 --compute --utility --require=cuda>=10.1 brand=tesla,driver>=384,drive<385 brand=tesla,driver>=396,driver<397 brand=tesla,driver>=410,driver<411 --pid=181252 /var/lib/docker/overlay2/60a1d6605c36731e135206619271c0dfb8d473423f0e149fd4cf123017493796/merged
通过查阅资料发现,nvidia-container-cli是为了更深入的分析这个问题,有必要熟悉docker、nvidia-docker的架构及基本组件,在相关组件源码中定位、解决此问题。
图1 容器运行时报错
为了更好理解nvidia-docker,需要大致了解下docker组件及其架构,其架构图如下图2所示:
图2 docker组件及其架构图
如上图所示,docker可抽象为Docker CLI、Dockerd、Containerd、Containerd-shim、RunC等主要组件。安装docker ,其实是安装了docker 客户端、dockerd 等一系列的组件,其中比较重要的有下面几个:
1、Docker CLI:docker客户端,称作dcoker;
2、Dockerd:docker daemon(dockerd),一般也会被称为 docker engine;
3、Containerd:是一个工业级标准的容器运行时,它强调简单性、健壮性和可移植性,可以在宿主机中管理完成的容器生命周期:容器镜像的传输和存储、容器的执行和管理、存储和网络等;
4、Containerd-shim:它是 containerd 的组件,是容器的运行时载体,我们在docker宿主机上看到的shim也正是代表着一个个通过调用 containerd启动的docker 容器;
5、Runc:是一个轻量级的工具,它是用来运行容器的,只用来做这一件事,并且这一件事要做好。我们可以认为它就是个命令行小工具,可以不用通过docker引擎,直接运行容器。
Docker-CLI:docker客户端,用来把用户的请求发送给 docker daemon(dockerd)。Docker Client 发送容器管理请求后,请求由 Docker Daemon 接收并处理,当 DockerClient 接收到返回的请求响应并做简单处理后,Docker Client 一次完整的生命周期就此结束。
若需要继续发送容器管理请求,用户必须再次通过可执行文件 docker 创建 Docker Client ,并走完以上相同的流程。
该程序的安装路径为:
图 3 Docker-CLI安装路径
Dockerd:docker daemon(dockerd),一般也会被称为 docker engine。Docker Daemon 是 Docker 架构中一个常驻在后台的系统进程。所谓的“运行 Docker” ,即代表运行 Docker Daemon。总之,Docker Daemon 的作用主要有以下两方面:
1、接收并处理 Docker Client 发送的请求;
2、管理所有的 Docker 容器。
图4 Dockerd安装路径
Containerd是从dockerd(docker daemon)中分离的项目,dockerd 对容器的管理和操作基本都是通过 containerd 完成的。该程序的安装路径为:
图5 Containerd安装路径
Containerd 是一个工业级标准的容器运行时,它强调简单性、健壮性和可移植性。Containerd 可以在宿主机中管理完整的容器生命周期:容器镜像的传输和存储、容器的执行和管理、存储和网络等。详细点说,Containerd 负责干下面这些事情:
1、管理容器的生命周期(从创建容器到销毁容器);
2、拉取/推送容器镜像;
3、存储管理(管理镜像及容器数据的存储);
4、调用 runC 运行容器(与runC等容器运行时交互,如nv-runtime);
5、管理容器网络接口及网络。
图6 dockerd、containerd等架构图
Containerd 被设计成嵌入到一个更大的系统中(标准化、通用化、可移植),而不是直接由开发人员或终端用户使用。
RunC 是一个轻量级的工具,它是用来运行容器的,只用来做这一件事,并且这一件事要做好。我们可以认为它就是个命令行小工具,可以不用通过 docker 引擎,直接运行容器。事实上,RunC 是标准化的产物,它根据 OCI 标准来创建和运行容器。而 OCI(Open Container Initiative)组织,旨在围绕容器格式和运行时制定一个开放的工业化标准。
RunC可创建、运行、删除容器,以及停止、删除、重启容器内进程,Containerd是Runc的封装。
runc run mybusybox => 运行容器
runc delete mybusybox => 删除容器
runc pause mybusybox => 停止容器内进程
runc resume mybusybox => 重启容器内进程
runc kill mybusybox => 杀死容器内进程
如果您还对上述docker组件感觉比较抽象的话,可以继续阅读本小结内容。
1、Docker 与 Dockerd 的交互
Docker 客户端与 dockerd 之间就是通过 REST 的方式通信的。配置dockerd 监听 tcp 端口后,我们可以使用 curl 来代替 docker 客户端。
图7 docker请求dockerd下载镜像
2、创建、启动、管理容器
容器镜像的下载是由 dockerd 完成的,但容器的创建和运行就需要 containerd(docker-containerd) 来完成了。Dockerd 与 docker-containerd 之间是通过 grpc 协议通信的。
当 Containerd 收到 dockerd 启动容器的请求之后,会做一些初始化工作,然后启动 Containerd-shim 进程,并将相关配置作为参数传给它。docker-containerd 负责管理所有本机正在运行的容器,而一个 docker-containerd-shim 进程只负责管理一个运行的容器。后者相当于 docker-runc 的一个封装,充当 docker-containerd 和 docker-runc 之间的桥梁,docker-runc 能干的就交给 docker-runc 来做,docker-runc 做不了的就放到这里来做。
图8 dockerd及其子进程
上图中没有出现 docker-runc 进程,这是为什么呢?
实际上,在容器启动的过程中,docker-runc 进程是作为 docker-containerd-shim 的子进程存在的。docker-runc 进程根据配置找到容器的 rootfs 并创建子进程 bash 作为容器中的第一个进程。当这一切都完成后 docker-runc 进程退出,然后容器进程 bash 由 docker-runc 的父进程 docker-containerd-shim 接管。
3、为什么需要 docker-containerd-shim
为什么在容器的启动或运行过程中需要一个 docker-containerd-shim 进程呢?把它移除掉整个架构会更简洁也更优美一些!事实上 docker-containerd-shim 的存在是非常有必要的,其目的有如下几点:
它允许容器运行时(即 runC)在启动容器之后退出,简单说就是不必为每个容器一直运行一个容器运行时(runC);
即使在 containerd 和 dockerd 都挂掉的情况下,容器的标准 IO 和其它的文件描述符也都是可用的;
向 containerd 报告容器的退出状态
前两点尤其重要,有了它们就可以在不中断容器运行的情况下升级或重启 dockerd ,这对于生产环境来说意义重大。
4、容器创建流程
docker --> dockerd --> docker-containerd->docker-containerd-shm -->runc --> container-process
在理解docker架构的基础上,我们再来看一下nvidia-docker组件及其工作原理。
图9 nvidia-docker组件及其架构
1、不使用GPU流程
docker客户端将创建容器的请求发送给dockerd,当dockerd收到请求任务之后将请求发送给docker-containerd,然后调用docker-containerd-shm,再开启runc。容器启动后,runc退出交给shim管理。
No-GPU:docker --> dockerd --> docker-containerd-->docker-containerd-shm -->runc --> container-process
2、使用GPU流程
和普通不使用GPU的容器差不多,只是把docker默认的运行时替换成了NVIDIA自家的nvidia-container-runtime。 这样当nvidia-container-runtime创建容器时,先执行nvidia-container-runtime-hook这个hook去检查容器是否需要使用GPU(通过环境变量NVIDIA_VISIBLE_DEVICES来判断),如果需要,则调用libnvidia-container来暴露GPU给容器使用,否则,走默认的runc逻辑。
With-GPU:docker--> dockerd --> docker-containerd-->docker containerd-shim-->nvidia-container-runtime-->container-process
如图9所示,nvidia-docker(项目使用2.0版本)是在docker的基础上,按照开放容器标准,加入了自己的容器hook,引入了加载GPU的流程,其主要组件包括:
nvidia-docker
nvidia-container-runtime
nvidia-container-runtime-hook
libnvidia-container
runc
为了定位、解决错误,需要对各组件进行源码级分析。
nvidia提供的nvidia-docker项目,它通过修改docker的rutime为nvidia rutime,当我们执行nvidia-docker create/run时,它会默认加上 –runtime=nvidia参数,将runtime指定为nvidia。
当然,为了方便使用,直接可以手动修改docker daemon的启动参数。
项目地址:https://github.com/NVIDIA/nvidia-docker
图10 修改docker runtime为nvidia runtime
nvidia-container-runtime是真正的核心部分,它在原有的docker容器运行时runc的基础上增加一个prestart hook,用于调用libnvidia-container库。nvidia-container-runtime调用nvidia-container-runtime-hook(二进制文件),实现添加“钩子”。
项目地址:https://github.com/NVIDIA/nvidia-container-runtime
图11 nvidia-container-runtime及hook安装路径
分析nvidia-container-runtime源码发现,该组件的功能就是:按照OCI标准(Open Container Initiative Runtime Specification)添加prestart的钩子,钩子的路径就是nvidia-container-runtime-hook的路径,这样nvdida-docker在启动容器前就要先执行nvidia-container-runtime-hook。
图12 nvidia-container-runtime添加hook源码
在Hooks中,可以 配置容器生命周期事件的callbacks(回调):
1、Prestart:容器进程启动前执行hooks;
2、Poststart:容器进程启动后执行hooks;
3、Poststop:容器进程退出后执行hooks。
图13 nvidia-container-runtime中Hooks定义
nvidia-container-runtime-hook是一个简单的二进制包,定义在nvidia container runtime 的hook中执行。目的是将当前容器中的信息收集并处理,转换为参数调用nvidia-container-cli。该组件要做的就是将必要的信息整理为nvidia-container-cli参数,最终实现是否挂载GPU及driver。
图14 nvidia-docker通过环境变量使用GPU(all、none、n)
项目地址:https://github.com/NVIDIA/libnvidia-container(AIPLAT使用v1.0.2)
图15 libnvidia-container-cli命令行参数说明
看到这里,是不是有种豁然开朗的感觉?结合错误提示信息,我们应该大致可以定位第一部分问题报错的位置了,一定是在libnvidia-container中报的错。所以,有必要分析下libnvidia-container的源码,以便进一步弄清楚原因(这些工作将在第4部分介绍)。
runc与docker runc相同,详见2.4。
1、nvidia-docker 挂载GPU的流程
docker--> dockerd --> docker-containerd-->docker containerd-shim-->nvidia-container-runtime-- >container-process,容器运行时执行了“钩子”,“钩子”将环境变量等参数传递给libnvidia-container中的cli,再决定挂载GPU卡及驱动路径等信息,最终实现GPU在容器内可用。
2、报错位置
libnvidia-container中cli报错。
再来看一下错误提示信息:
nvidia-container-cli: initialization error: cuda error: unknown error
nvidia-container-cli --load-kmods configure --ldconfig=@/sbin/ldconfig --device=GPU-2fb041ff-6df3-4d00-772d-efb3139a17a1 --compute --utility --require=cuda>=10.1 brand=tesla, driver>=384,drive<385 brand=tesla,driver>=396,driver<397 brand=tesla,driver>=410,driver<411 --pid=181252 /var/lib/docker/overlay2/60a1d6605c36731e135206619271c0dfb8d473423f0e 149fd4cf123017493796/merged
通过源码分析及提示命令行帮助信息,不难发现输入参数对应的含义如下:
--load-kmods:加载内核模块
--ldconfig:是一个动态链接库管理命令,其目的为了让动态链接库为系统所共享
--device:GPU的uuid
--compute:是否开启计算功能
--utility:是否开启多功能
--ruquire:容器条件,版本信息等
--pid:容器PID(进程ID)
/var/lib/docker/overlay2/60a1d6605c36731e135206619271c0dfb8d473423f0e149fd4cf123017493796/merged(rootfs的merged文件,合并后的)
图16 libnvidia-container源码-参数配置
configure_comand 对应nvidia-container-cli 命令行的configure,该命令在进行参数初始化时调用了nvc_init()函数,如下图所示:
图17 libnvidia-container源码-cli参数初始化
nvc_init()函数调用了driver_init_1()函数,如下图所示:
图18 libnvidia-container源码-driver_init_1()函数
driver_init_1()函数调用了call_cuda()函数,该函数最终调用了驱动层的cuInit()函数,出错报"cuda error"
图19 libnvidia-container源码-call_cuda()函数
所以,最终定位在libnvidia-container-cli执行configuer命令时,报错:nvidia-container-cli: initialization error: cuda error: unknown error。错误发生在驱动层函数调用时,由于启动容器都配置了使用GPU,都走nvidia-docker这一流程,这就解释了所有容器报相同错误的原因。
由于错误类型超出了libnvidia-container的控制范围(unknow err),官方建议升级nvidia-docker解决(CUDA 依赖在libnvidia-container v1.1.0中移除,AIPLAT中的使用版本为v1.0.2)。
我去,源码分析的溜溜的