当前位置: 首页 > 科技观察

一个灵魂的问题:为什么你的Docker容器启动后马上就停止了?

时间:2023-03-12 03:24:28 科技观察

很多docker初学者,在运行一个容器的时候,或者写第一个dockerfile的时候,最常见的问题就是容器启动后就停止了。好像命令没问题,容器里也没有错误日志,只有几个dockerfile……其实你没有看错,是docker,执行速度太快了。你解释。以上是nginx官方的dockerfile。我删除了设置的部分,其他的就没有了。主要看为什么CMD不是systemctlnginxstart,或者/etc/init.d/nginxstart,或者直接启动nginx,而是Startwithdaemonoff?这是因为如果nginx运行在后台模式,执行完启动命令后,启动命令就会退出。这个时候容器也会退出。为什么命令执行完容器就退出了?这从linux内核开始。在Linux操作系统中,当内核初始化时,会启动一个init进程。这个进程是整个操作系统的第一个用户进程,所以它的进程ID是1,也就是我们常说的PID1进程,然后所有的用户态进程都是这个进程的子进程。因此,整个系统的用户进程都是以init进程为根的。要理解这个PID1进程,需要理解以下几个概念:进程表项Linux内核程序进程是通过进程表来管理的,每个进程在进程表中占有一个表项,称为进程表项,它记录了进程的状态进程、打开的文件描述符和其他系统信息。当一个进程运行完毕或中途终止时,内核需要释放该进程占用的系统资源。这包括进程运行时打开的文件、分配的内存等。不过这里要注意,进程表项并不会随着进程退出而被清除,它会一直占用内核的内存。为什么会有这种奇怪的行为?这是因为在一些程序中,我们必须清楚地知道进程的退出状态等信息,而这些信息的获取是通过父进程调用wait/waitpid获得的。想象这样一个场景,如果子进程在退出时直接清除文件入口,那么父进程可能没有地方获取进程的退出状态,所以操作系统会一直保留文件入口,直到wait/waitpid系统通话结束。僵尸进程僵尸进程是指从进程退出到其父进程未对其调用wait/waitpid期间它所处的状态。一般来说,这种状态持续的时间很短,所以我们一般很难在系统中捕捉到。但是,一些粗心的程序员可能会忘记调用wait/waitpid,或者由于某种原因调用没有执行等等,那么这时候就会出现长寿僵尸进程。如果产生大量的僵尸进程,它们的进程号会一直被占用,可能导致系统无法产生新的进程。然后就是我们经常看到的一种情况,就是父进程先于子进程结束。这种情况在手动杀死父进程的情况下比较常见。这种情况就是下面说的孤儿进程。如果子进程退出,子进程就会成为孤儿进程。孤儿进程会被init进程(进程号1)接管,init进程会在其上完成状态收集(wait/waitpid)工作。PID1负责清理那些废弃进程留下的痕迹,有效回收系统资源。保证系统长时间稳定运行了解了linux的PID1之后,我们来看一下容器中的PID1进程。熟悉docker的就知道,docker容器并不是一个完整的linux操作系统,它没有内核初始化过程,更没有init(1)这样的初始化过程。docker容器中标记为PID1的进程其实就是一个普通的用户进程。我们再看看官方nginx镜像的容器。我直接使用dockerrun-dnginx启动,可以看到是Dockerfile中指定的CMD进程。注意:如果在启动容器的时候指定了命令,会覆盖CMD,即CMD是默认的启动命令参数,如果在启动容器的时候指定了命令,会被覆盖。当Dockerfile中有多个CMD时,最后执行的进程实际上在主机上有一个共同的用户进程ID。容器中PID变为1的原因是因为linux内核提供的PID命名空间功能,如果宿主机上的所有用户进程形成一个完整的树状结构,那么PID命名空间实际上就是CMD或者ENTRYPOINT进程及其子进程-processes作为另一个分支,显然这部分也是一个树状结构当我们在宿主机上kill进程ID时,整个容器会处于exit状态,这就解释了为什么上面的命令执行完后容器会退出。认真的朋友可以从上图看出,上面我说了Linux中的PID1进程是所有用户进程的父进程,但是在容器中,通过ps命令看到的进程的父进程是“0”,为什么是这?前面说过,容器中的进程树其实是宿主进程树的一个子树,或者说是一个分支,所以我们可以在宿主机上找到这个子树的父进程。我们可以看到docker容器中PID为0的进程应该就是containerd-shim。我们先看一下docker的结构图。从架构图中我们可以看到containerd-shim进程下有一个runC进程,但是我们在上面的进程中,并没有找到runC进程。runC是OCI标准的参考实现,OCIOpenContainerInitiative是一个由多家公司共同发起并由Linux基金会管理的项目。它专用于容器运行时标准。制定和runc开发等工作。runc是OCI标准的参考实现,是一个CLI(命令行界面)工具,可用于创建和运行容器。runc直接与容器所依赖的cgroup/linux内核交互,负责配置容器启动容器所需的环境,如cgroup/namespace,并创建启动容器的相关进程。其实Docker容器的创建过程是这样的docker-containerd-shim->runC->entrypoint,而我们看到的最终状态是docker-containerd-shim->entrypoint,runc进程创建容器后,它是先退出的,所以我们在上面的过程中还没有过,看到这里应该明白为什么启动容器或者写dockerfile,总是启动就退出,而且不会报错!