网站首页 文章专栏 一个nvidia-docker引发k8s崩溃的问题探讨
一个nvidia-docker引发k8s崩溃的问题探讨
编辑时间:2020-09-19 16:00:44 作者:lmc 浏览量:60367

1  问题描述

       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的架构及基本组件,在相关组件源码中定位、解决此问题。

image1-1.png

图1   容器运行时报错

2  docker组件及其工作原理

     为了更好理解nvidia-docker,需要大致了解下docker组件及其架构,其架构图如下图2所示:

image2.png

图2   docker组件及其架构图


       如上图所示,docker可抽象为Docker CLIDockerdContainerdContainerd-shimRunC等主要组件。安装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引擎,直接运行容器。

2.1  Docker CLI

        Docker-CLI:docker客户端用来把用户的请求发送给 docker daemon(dockerd)。Docker Client 发送容器管理请求后,请求由 Docker Daemon 接收并处理,当 DockerClient 接收到返回的请求响应并做简单处理后,Docker Client 一次完整的生命周期就此结束。

       若需要继续发送容器管理请求,用户必须再次通过可执行文件 docker 创建 Docker Client ,并走完以上相同的流程。

       该程序的安装路径为:

iamge3.png

图 3   Docker-CLI安装路径

2.2  Dockerd

        Dockerd:docker daemon(dockerd)一般也会被称为 docker engine。Docker Daemon 是 Docker 架构中一个常驻在后台的系统进程。所谓的“运行 Docker” ,即代表运行 Docker Daemon。总之,Docker Daemon 的作用主要有以下两方面:

        1、接收并处理 Docker Client 发送的请求;
        2、管理所有的 Docker 容器。

image4.png

图4   Dockerd安装路径

2.3  Containerd

        Containerd是从dockerd(docker daemon)中分离的项目,dockerd 对容器的管理和操作基本都是通过 containerd 完成的。该程序的安装路径为:

image5.png

图5   Containerd安装路径

 

        Containerd 是一个工业级标准的容器运行时,它强调简单性、健壮性和可移植性。Containerd 可以在宿主机中管理完整的容器生命周期:容器镜像的传输和存储、容器的执行和管理、存储和网络等。详细点说,Containerd 负责干下面这些事情:

       1、管理容器的生命周期(从创建容器到销毁容器);

       2、拉取/推送容器镜像;

       3、存储管理(管理镜像及容器数据的存储);

       4、调用 runC 运行容器(与runC等容器运行时交互,如nv-runtime);

       5、管理容器网络接口及网络。

image6.png

图6   dockerd、containerd等架构图

     

       Containerd 被设计成嵌入到一个更大的系统中(标准化、通用化、可移植),而不是直接由开发人员或终端用户使用。

2.4  Runc

      

        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        =>    杀死容器内进程

2.5  小结

       如果您还对上述docker组件感觉比较抽象的话,可以继续阅读本小结内容。

       1、Docker 与 Dockerd 的交互

       Docker 客户端与 dockerd 之间就是通过 REST 的方式通信的。配置dockerd 监听 tcp 端口后,我们可以使用 curl 来代替 docker 客户端。

image8.png

图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 做不了的就放到这里来做。

image8.png

图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

3   nvidida-docker组件及其工作原理

      在理解docker架构的基础上,我们再来看一下nvidia-docker组件及其工作原理。

image9.png

图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

       为了定位、解决错误,需要对各组件进行源码级分析。

3.1   nvidia-docker

       nvidia提供的nvidia-docker项目,它通过修改docker的rutime为nvidia rutime,当我们执行nvidia-docker create/run时,它会默认加上 –runtime=nvidia参数,将runtime指定为nvidia。

       当然,为了方便使用,直接可以手动修改docker daemon的启动参数。

       项目地址:https://github.com/NVIDIA/nvidia-docker

image10.png

图10   修改docker runtime为nvidia runtime

3.2  nvidia-container-runtime

         nvidia-container-runtime是真正的核心部分,它在原有的docker容器运行时runc的基础上增加一个prestart hook,用于调用libnvidia-container库。nvidia-container-runtime调用nvidia-container-runtime-hook(二进制文件),实现添加“钩子”。

        项目地址:https://github.com/NVIDIA/nvidia-container-runtime

image11.png

图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

image12.png

图12   nvidia-container-runtime添加hook源码


       在Hooks中,可以 配置容器生命周期事件的callbacks(回调):

       1、Prestart:容器进程启动前执行hooks;

       2、Poststart:容器进程启动后执行hooks;

       3、Poststop:容器进程退出后执行hooks。


image13.png

图13   nvidia-container-runtime中Hooks定义


3.3  nvidia-container-runtime-hook

        nvidia-container-runtime-hook是一个简单的二进制包,定义在nvidia  container runtime 的hook中执行。目的是将当前容器中的信息收集并处理,转换为参数调用nvidia-container-cli。该组件要做的就是将必要的信息整理为nvidia-container-cli参数最终实现是否挂载GPU及driver

image14.png

图14   nvidia-docker通过环境变量使用GPU(all、none、n)

 3.4  libnvidia-container

 libnvidia-container:提供一个库和一个简单的CLI程序,使用这个库可以使NVIDIA GPU及Linux容器。

 项目地址:https://github.com/NVIDIA/libnvidia-containerAIPLAT使用v1.0.2)


image15.png

image15-1.png

图15   libnvidia-container-cli命令行参数说明

       看到这里,是不是有种豁然开朗的感觉?结合错误提示信息,我们应该大致可以定位第一部分问题报错的位置了,一定是在libnvidia-container中报的错所以,有必要分析下libnvidia-container的源码,以便进一步弄清楚原因(这些工作将在第4部分介绍)。

3.5 runc

        runc与docker runc相同,详见2.4。

3.6 小结

       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报错。

4  错误定位

      再来看一下错误提示信息:

      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文件,合并后的)


image16.png

图16   libnvidia-container源码-参数配置

      configure_comand 对应nvidia-container-cli 命令行的configure,该命令在进行参数初始化时调用了nvc_init()函数,如下图所示:

image16-1.png

图17   libnvidia-container源码-cli参数初始化

       nvc_init()函数调用了driver_init_1()函数,如下图所示:


image17.png

图18   libnvidia-container源码-driver_init_1()函数

      driver_init_1()函数调用了call_cuda()函数,该函数最终调用了驱动层的cuInit()函数,出错报"cuda error"


image18.png

图19   libnvidia-container源码-call_cuda()函数

        所以,最终定位在libnvidia-container-cli执行configuer命令时,报错:nvidia-container-cli: initialization error: cuda error: unknown error。错误发生在驱动层函数调用时,由于启动容器都配置了使用GPU,都走nvidia-docker这一流程,这就解释了所有容器报相同错误的原因。

5   解决方法

       由于错误类型超出了libnvidia-container的控制范围(unknow err),官方建议升级nvidia-docker解决(CUDA 依赖在libnvidia-container v1.1.0中移除,AIPLAT中的使用版本为v1.0.2)。

来说两句吧
最新评论
  • Google 2020-10-15 20:11:10

    我去,源码分析的溜溜的