17

灵魂一问:为什么你的 Docker 容器刚启动就停了

 4 years ago
source link: http://os.51cto.com/art/202001/609700.htm
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

很多docker初学者,在运行容器的时候,或者是写第一个dockerfile的时候,问题最多的就是容器启动后就停了,怎么看都觉得命令没有问题,容器也没有错误日志,dockerfile也就那么几条……

其实你没有错,错的是docker,它执行的太快了

这话怎么说呢,我拿nginx官方的dockerfile给你解释下。

ANbUv2r.png!web

上面是nginx官方的dockerfile文件,我把set部分删掉了,其他没啥,主要看下CMD

为什么这里不是 systemctl nginx start ,或者 /etc/init.d/nginx start ,再或者nginx直接启动,而是用daemon off的方式启动?

这是因为如果nginx用后台模式运行,启动的命令执行完之后,这个启动的命令就退出了,这个时候,容器也就跟着退出了

又为什么命令执行完,容器就退出了?这个要从linux内核说起。

在linux操作系统中,当内核初始化完毕之后,会启动一个init进程,这个进程是整个操作系统的第一个用户进程,所以它的进程ID为1,也就是我们常说的PID1进程,然后所有的用户态进程,都是这个进程的子进程,所以,整个系统的用户进程,都是由init进程作为根进程的

要了解这个PID1进程,要从以下几个概念了解:

  • 进程表项

linux内核程序通过进程表对进程进行管理, 每个进程在进程表中占有一项,称为进程表项,它记录了进程的状态,打开的文件描述符等等一系统信息。当一个进程结束了运行或在半途中终止了运行,那么内核就需要释放该进程所占用的系统资源。这包括进程运行时打开的文件,申请的内存等。

但是,这里要注意的是,进程表项并没有随着进程的退出而被清除,它会一直占用内核的内存。为什么会有这么奇怪的行为呢?这是因为在某些程序中,我们必须明确地知道进程的退出状态等信息,而这些信息的获取是由父进程调用wait/waitpid而获取的。

设想这样一种场景,如果子进程在退出的时候直接清除文件表项的话,那么父进程就很可能没有地方获取进程的退出状态了,因此操作系统就会将文件表项一直保留至wait/waitpid系统调用结束。

  • 僵尸进程

僵尸进程指的是:进程退出后,到其父进程还未对其调用wait/waitpid之间的这段时间所处的状态。一般来说,这种状态持续的时间很短,所以我们一般很难在系统中捕捉到。但是,一些粗心的程序员可能会忘记调用wait/waitpid,或者由于某种原因未执行该调用等等,那么这个时候就会出现长期驻留的僵尸进程了。如果大量的产生僵尸进程,其进程号就会一直被占用,可能导致系统不能产生新的进程。

然后还有我们经常会见到的一种情况,就是父进程先于子进程结束,这种情况多见于手动kill某个父进程的情况,这种情况就是下面要说到的

  • 孤儿进程

父进程先于子进程退出,那么子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)接管,并由init进程对它完成状态收集(wait/waitpid)工作

PID1负责清理那些被抛弃的进程所留下来的痕迹,有效的回收的系统资源,保证系统长时间稳定的运行

了解了linux的PID1,接着来看下容器中的PID1进程

熟悉docker都知道,docker容器并不是一个完整的linux的操作系统,它也没什么内核初始化过程,更没有像init(1)这样的初始化过程。在docker容器中被标志为PID1的进程实际上就是一个普通的用户进程,我们还拿nginx官方的镜像起的容器来看。

我用docker run -d nginx直接启动的

6rAR7ba.png!web

可以看到,就是Dockerfile中指定的CMD那个进程,注意:如果你启动容器的时候,指定了命令,会覆盖CMD,也就是CMD是条默认启动的命令参数,如果启动容器时指定了命令,会覆盖,当Dockerfile中有多条CMD时,执行最后一条

这个进程其实在宿主机上有一个普通的用户进程ID

aiqUbqJ.png!web

之所以在容器中PID变成1,是因为linux内核提供的PID namespaces功能,如果宿主机上所有用户进程构成了一个完整的树形结构,那么PID namespaces实际上就是将这个CMD或ENTRYPOINT进程及其子进程作为另外一个分支,很显然这部分也是一个树形结构

当我们在宿主机上kill掉这个进程ID,那么整个容器便会处于退出状态

这也就解释了上面为什么命令执行完之后,容器就退出了

认真的小伙伴从上面图中看到了,我上面说linux中PID1进程为所有用户进程的父进程,但是在容器里面,通过ps命令看到的进程的父进程都是“0”,这又是为什么呢?

前面提到,容器中的进程树实际上是宿主机进程树的一棵子树,或者说分支,那么我们在宿主机上就可以找到这颗子树的父进程。

aMVnaeE.png!web

我们可以看到,这个docker容器中PID 0的进程应该就是这个containerd-shim

我们结合docker的结构图看一下

AJzQfiy.png!web

从架构图中,我们可以看到containerd-shim进程下还有一个runC进程,但是我们在上面过程中,并没有发现runC这个进程

runC是OCI标准的一个参考实现,而OCI Open Container Initiative,是由多家公司共同成立的项目,并由linux基金会进行管理,致力于container runtime的标准的制定和runc的开发等工作。 runc,是对于OCI标准的一个参考实现,是一个可以用于创建和运行容器的CLI(command-line interface)工具。

runc直接与容器所依赖的cgroup/linux kernel等进行交互,负责为容器配置cgroup/namespace等启动容器所需的环境,创建启动容器的相关进程

事实上,Docker容器的创建过程是这样子的 docker-containerd-shim –> runC –> entrypoint,而我们看到的最终状态是 docker-containerd-shim –> entrypoint,而runc进程创建完容器之后,自己就先退出去了,所以我们上面的过程中一直没有出现

看到这里你应该了解,为什么你启动容器或写好的dockerfile,总是刚启动就退出,而且没有任何错误了吧!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK