13.2 Docker栈

13.2.1 docker daemon调用栈分析

Docker作为阿里云Kubernetes集群使用的容器运行时,在1.11版之后,被拆分成了多个组件以适应OCI标准。拆分之后,其包括docker daemon、containerd、containerd-shim和runC。组件containerd负责集群节点上容器的生命周期管理,并向上为docker daemon提供gRPC接口,如图13-5所示。

图13-5 Docker的组成

在这个问题中,既然PLEG认为容器运行时出了问题,我们就需要从docker daemon进程看起。我们可以使用kill-USR1<pid>命令发送USR1信号给docker daemon,而docker daemon收到信号之后,会把所有线程调用栈输出到/var/run/docker文件夹里。

docker daemon进程的调用栈是比较容易分析的。稍加留意,我们会发现大多数调用栈都是图13-6中的样子。通过观察栈上每个函数的名字,以及函数所在的文件(模块)名称,我们可以了解到,这个调用栈的下半部分,是进程接到HTTP请求,并对请求做出路由转发的过程,而上半部分则是具体的处理函数。最终处理函数进入等待状态,等待一个mutex实例,如图13-6所示。

图13-6 线程等待mutex状态

到这里,我们需要稍微看一下ContainerInspectCurrent这个函数的实现。从实现可以看到,这个函数的第一个参数,就是这个线程正在操作的容器名指针。使用这个指针搜索整个调用栈文件,我们会找出所有等在这个容器上的线程。同时,我们可以看到图13-7中的这个线程。

这个线程调用栈上的函数ContainerExecStart也是在处理相同的容器。但不同的是,ContainerExecStart并没有在等这个容器,而是已经拿到了这个容器的操作权(mutex),并把执行逻辑转向了containerd调用。

关于这一点,我们也可以使用代码来验证。前面提到过,containerd通过gRPC向上对docker daemon提供接口。此调用栈上半部分内容,正是docker daemon在通过gRPC请求来呼叫containerd。

图13-7 线程远程调用containerd

13.2.2 Containerd调用栈分析

与docker daemon类似,我们可以通过kill-SIGUSR1<pid>命令来输出containerd的调用栈。不同的是,这次调用栈会直接输出到messages日志。

containerd作为一个gRPC的服务器,会在接到docker daemon的远程调用之后,新建一个线程去处理这次请求。关于gRPC的细节,我们这里其实不用太关注。在这次请求的客户端调用栈上,可以看到这次调用的核心函数在创建一个进程。我们在containerd的调用栈里搜索Start、Process以及process.go等字段,很容易发现图13-8中的这个线程。

这个线程的核心任务,就是依靠runC去创建容器进程。而在容器启动之后,runC进程会退出。所以下一步我们自然而然会想到,runC是不是顺利完成了自己的任务。查看进程列表我们会发现,系统中有个别runC进程还在执行,这不是预期的行为。容器的启动和进程的启动,耗时应该是差不多的,系统里有正在运行的runC进程,则说明runC不能正常启动容器。

图13-8 线程启动容器进程