为什么你的 Docker 容器刚启动就停了?
很多 docker 初学者,在运行容器的时候,或者是写第一个 dockerfile 的时候,问题最多的就是容器启动后就停了,怎么看都觉得命令没有问题,容器也没有错误日志,dockerfile 也就那么几条……
其实你没有错,错的是 docker,它执行的太快了
这话怎么说呢,我拿 nginx 官方的 dockerfile 给你解释下
上面是 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 直接启动的
可以看到,就是 Dockerfile 中指定的 CMD 那个进程,注意:如果你启动容器的时候,指定了命令,会覆盖 CMD,也就是 CMD 是条默认启动的命令参数,如果启动容器时指定了命令,会覆盖,当 Dockerfile 中有多条 CMD 时,执行最后一条
这个进程其实在宿主机上有一个普通的用户进程 ID
之所以在容器中 PID 变成 1,是因为 linux 内核提供的 PID namespaces 功能,如果宿主机上所有用户进程构成了一个完整的树形结构,那么 PID namespaces 实际上就是将这个 CMD 或 ENTRYPOINT 进程及其子进程作为另外一个分支,很显然这部分也是一个树形结构
当我们在宿主机上 kill 掉这个进程 ID,那么整个容器便会处于退出状态
这也就解释了上面为什么命令执行完之后,容器就退出了
认真的小伙伴从上面图中看到了,我上面说 linux 中 PID1 进程为所有用户进程的父进程,但是在容器里面,通过 ps 命令看到的进程的父进程都是“0”,这又是为什么呢?
前面提到,容器中的进程树实际上是宿主机进程树的一棵子树,或者说分支,那么我们在宿主机上就可以找到这颗子树的父进程
我们可以看到,这个 docker 容器中 PID 0 的进程应该就是这个 containerd-shim
我们结合 docker 的结构图看一下
从架构图中,我们可以看到 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,总是刚启动就退出,而且没有任何错误了吧!
持续更新,欢迎扫码关注,敬请期待!
版权声明: 本文为 InfoQ 作者【运维研习社】的原创文章。
原文链接:【http://xie.infoq.cn/article/8acecf06b4cb20c578de44bed】。文章转载请联系作者。
评论