当前位置:   article > 正文

为啥你的容器刚启动就停了?这篇文章告诉你

容器启动后很快自动终止

49bf208d8825b9724f4a16276aa71c21.gif

e0b4adaa9fb7772f95c52b40bdf3c710.jpeg

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

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

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

1d14c882591356420cc7f5d428c1ff1f.png

上面是 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直接启动的

3fb5e78f8f2197d60ef9037741e98383.png

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

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

985b901078fd702e04fd11a627cae951.png

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

当我们在宿主机上kill掉这个进程ID,那么整个容器便会处于退出状态
这也就解释了上面为什么命令执行完之后,容器就退出了

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

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

2c19fcb31ab229323ca4000a4fbc1eee.png

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

b5f5234024cd9cf3a625125d084e2d99.png

从架构图中,我们可以看到 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,总是刚启动就退出,而且没有任何错误了吧!

来源:公众号运维研习社

  1. Linux学习指南
  2. 有收获,点个在看
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/菜鸟追梦旅行/article/detail/169372
推荐阅读
相关标签
  

闽ICP备14008679号