写点什么

被忽略的一点:Docker 的单进程模型

  • 2022 年 8 月 14 日
    上海
  • 本文字数:3453 字

    阅读完需:约 11 分钟

被忽略的一点:Docker的单进程模型

Dokcer 大家都在用,docker run 就可以快速的上手一个 demo 应用,但是大家有没有思考过 docker run 背后的原理,其内部的应用又是如何被创建的呢?


我先抛出本文要说明的知识点:docker 是一个单进程模型


本文分为几个步骤讲解:


  1. 首先讲解容器是如何实现隔离的,主要介绍的是 Namespace 技术。

  2. 接着介绍容器的本质:其就是一个进程。

  3. 最后会引出本文的主题,Docker 是一个单进程模型。


一、演示环境准备


此处为了演示,我们在同一台宿主机上创建 2 个 Docker 容器。


命令如下:


// 创建第1个容器docker run busybox -it /bin/sh
// 创建第2个容器docker run busybox -it /bin/sh
复制代码




查看两个容器的运行:

  • 为了下面的演示,我们将 b4bb5fb2d10c 当做 docker1。

  • 将 31e14d3dd570 当做 docker2。


docker ps
复制代码



现在整体的图示如下:



二、容器的"集装箱模型"


我们经常都把容器比喻为"集装箱",把容器内部运行的内容比喻为"货物"。


这种"集装箱"模型最大的好处就是,我们将自己处理好的应用封装起来,每个"集装箱"负责一款应用,每个应用与应用之间互相隔离,可以运行在同一个宿主机上。


当然,这里我要说一个"集装箱"模型最大的好处:任意移动。什么意思呢?

  • 写过 Java 的同学肯定都听过一句话:"一次编译,到处运行"。(具体是什么含义我就不解释了)

  • 面对到容器这里也是同样的意思:"一次封装,到处运行"。(我想大家一定都体会到这种模型的好处了吧)



三、Namespace:实现容器隔离


上面我们所说的"集装箱"模型,代表着每个 Docker 容器都是独立存在于宿主机上的,每个容器中的内容都相互隔离,互不干扰。那么其是如何实现这种环境隔离的呢?


这里一定要提到一个重要的概念了:Namespace。


Linux Namespace 的一句话概述:用来修改进程视图的主要方法,实现进程隔离。


备注:关于 Namespace 的概念我就不过多介绍了,网上有很多标准的概述,我相信学过容器的同学应该也都或多或少了解过这些概念。这里我系统通过演示案例带大家更好的理解这些内容。


3.1-查看容器 1 的 Namespace


每个运行的容器都会与一个 Namespace 进行绑定,从而拥有各自 Namespace 中的视图。


下面,我们首先查看 docker1 的 Namespace,然后再查看 docker2 的 Namespace。


执行如下的命令,查看 docker1 所运行的进程 PID:


docker inspect --format '{{ .State.Pid }}'  b4bb5fb2d10c
复制代码



然后我们查看宿主机的 proc 文件,看到上述 2022 进程所有 Namespace 对应的文件:


ls -l /proc/2022/ns
复制代码



从上述我们可以看到,这个 Docker 所关联的 Namespace 中相关的内容:


  • ipc:进程间通信。

  • mnt:挂载。

  • net:网络。

  • 等等


3.2-查看容器 2 的 Namespace


跟上面一样的方式,输入如下的命令查看 docker2 的 PID:


docker inspect --format '{{ .State.Pid }}'  b4bb5fb2d10c
复制代码



然后再查看对应的 Namespace 文件:


ls -l /proc/2742/ns
复制代码


3.3-小结


到这里,我们可以看到每一个 Docker 都绑定着各自的 Namespace 环境,然后每个 Namespace 环境下都有各自对应的网络/挂载/用户等环境内容。


现在,我们更新图片:


  • 宿主机所在的环境我们暂称之为"main namespace"。

  • docker1 所处的 namespace 环境我们称之为"docker1 namespace"。

  • docker2 所处的 namespace 环境我们称之为"docker2 namespace"。



现在再来看这张图片,可以看到宿主机/docker1/docker2 都运行在不同的 namespace 中,并且每个 namespace 中都有自己的一套网络/挂载/用户信息等配置。因此也就从逻辑上实现了环境的隔离。


四、探讨容器本质:进程


上面知道你在机器上运行的任何程序,他们都归属于同一类:进程。没错,docker 容器也不例外,其本质上也只是一个进程。


4.1-查看容器 1 进程


在上面我们使用了 docker inspect 命令查看了每个容器对应的进程,那个时候我们没有过多的介绍为什么要通过 docker 的 PID 去查看 namespace,这个地方我就来介绍一下。


首先,我们再次来执行下面的命令,查看容器 1 的进程 PID:


docker inspect --format '{{ .State.Pid }}'  b4bb5fb2d10c
复制代码



可以看到,命令结果显式其 PID 为 2022。


上面我们说了,容器本质上就是个进程,那么这个进程是运行在哪里的呢?没错,当然是在宿主机上。于是,我们可以输入如下的命令查看在宿主机上 PID 为 2022 的内容是什么。


  • 命令结果显式,PID 2022 进程是存在的。

  • 另外,可以看到这个进程的启动命令是/bin/sh。


ps -ef | grep 2022
复制代码



emm....有的同学就会问了,2022 这个进程的启动命令不就是我们 docker run 启动容器的时候,让容器执行的命令么?没错,是的。至于原理,我先不说,再往下看。


4.2-查看容器 2 进程


与上面一样,先查看容器 2 的进程 PID,然后再查看宿主机上相同 PID 的进程内容。


docker inspect --format '{{ .State.Pid }}' 31e14d3dd570
ps -ef | grep 2742
复制代码



与预期的结果一样,宿主机的 PID 2742 进程就是容器 2,并且启动命令也是/bin/sh。


4.3-小结


通过上面的介绍,我们知道,docker 容器其实就是宿主机上运行的一个进程:


  • docker1 的 PID 为 2022。

  • docker2 的 PID 为 2742。


此时我们可以得到如下的概览图:



五、最终的主题:Docker 的单进程模型


在介绍完了上面这么多的内容之后,我们就开始来介绍最终的主题了:Docker 的单进程模型。


在宿主机查看容器 PID 的时候,看到其启动的命令就是 docker run 时指定的命令(/bin/sh),也就是要让 Docker 容器要运行的功能(也称为其职责)。


5.1-容器的进程视图


下面我们分别看一下各自容器中的进程视图是什么样的。


首先,我们在容器 1 看一下其进程内容:


ps
复制代码



然后在容器 2 中查看一下其进程内容:


ps
复制代码



可以看到,在容器内部,进程非常非常的少,只有 2 个进程:


  • 其中一个 PID 为 1,就是 docker run 要运行的内容。

  • 另外一个 PID 为 7 是 ps 执行命令所创建的,可以忽略。


这里我就要说明一个点:在容器内部看到的/bin/sh 进程其实就是宿主机上的/bin/sh 进程。其中:


  • docker1 内部的/bin/sh PID 1 号进程就是宿主机的 PID 2022 号进程。

  • docker1 内部的/bin/sh PID 1 号进程就是宿主机的 PID 2742 号进程。


那么,这是如何做到的呢?


实际上,Docker 在启动了容器之后,对/bin/sh 程序做了"障眼法",将宿主机中的/bin/sh 进程"虚拟"成了容器内部的第 1 号进程,使得每个容器内部看到的/bin/sh 程序就如何运行在自己独立隔离的环境中一样。


当然,上面的实现依靠的就是我们最初所说的 Linux 的 Namespace 机制。


此时,我们的系统结构图就如下所示了:


  • 我们在宿主机上启动了两个容器 docker1 和 docker2。

  • docker1 和 docker2 分别都有自己的 namespace。

  • docker1 在宿主机上对应的 PID 为 2022,docker2 在宿主上对应的 PID 为 2742。

  • 每个容器内部虚拟出 PID 为 1 的进程映射为宿主机的 PID(此处为 2022 和 2742)。



5.2-僵尸进程/孤儿进程


在继续讲解之前,我介绍几个概念:


  • 父进程:父进程拥有一系列的子进程。

  • 子进程:父进程所管理的进程称为子进程。当一个子进程结束时,正常的情况下,父进程会对子进程进行善后处理(获取终止子进程的有关信息、释放它仍占用的资源)。

  • 僵尸进程/僵死进程:当一个子进程终止时,其父进程没有对其进行善后处理,此时我们称这个子进程为僵尸进程。僵尸进程会一直等待父进程来处理自己,如果父进程没有对其进行处理,会造成资源的浪费。

  • 孤儿进程:一个进程都有一个父进程来管理(系统进程除外),如果父进程在子进程结束之前终止,那么我们就称这个子进程为孤儿进程(失去了父亲)。


5.3-Docker 为单进程的原因


我们说 Docker 容器是"单进程模型",并不代表容器只能运行一个进程,而是指容器没有回收孤儿进程的能力。


例如我们在上面的演示案例中,为容器起了一个/bin/sh 进程(称为容器的 1 号进程),如果我们继续在容器中创建进程,那么新的进程都是这个 1 号进程的子进程。Docker 判断一个容器是否启动正常,是判断 Docker 容器的 1 号进程的状态,如果 1 号进程启动正常,那么就认为容器运行正常,否则,容器运行失败。


因此,如果在容器内部启动了过多的进程,那么当容器的 1 号进程结束之后,由于 1 号进程不具有管理多进程,多线程的能力,所以在容器内部创建的其他进程都会处于没有人接管的状态,此时这些进程都会变为孤儿进程。


六、小结


本文就介绍到这里了,主要讲解了容器是如何通过 Namespace 进行隔离的,以及容器的单进程模型。


有些人肯定就会说了,一个容器对应一个进程,对于大型的项目来说,运行着大量的软件应用,这些软件之间的协作岂不是称为大问题了(设计复杂的跨容器操作等)。提出这种想法是可以理解的,但是容器最佳的应用场景并不是"单独作战",而是多个容器进行协作,也就是我们经常谈到的"容器编排",例如 Docker Swarm,以及目前大火的 Kubernetes。


在后续我会再单独出一篇文章讲解 Kubernetes 的 Pod 的设计原理:Pod 作为 Kubernetes 调度的最小单位,其允许在一个 Pod 中运行多个容器,从而将很多个的单进程(容器)组建为进程组(多个容器)。


发布于: 刚刚阅读数: 3
用户头像

沉默不是金,是金子就主动让它亮起来 2021.07.29 加入

哔哩哔哩后端开发工程师。公粽号:董哥的黑板报

评论

发布
暂无评论
被忽略的一点:Docker的单进程模型_Docker_董哥的黑板报_InfoQ写作社区