核心概念

镜像

镜像是什么呢?通俗地讲,它是一个只读的文件和文件夹组合。它包含了容器运行时所需要的所有基础文件和配置信息,是容器启动的基础。所以你想启动一个容器,那首先必须要有一个镜像。镜像是 Docker容器启动的先决条件。

容器

容器是Docker的另一个核心概念。通俗地讲,容器是镜像的运行实体。镜像是静态的只读文件,而容器带有运行时需要的可写文件层,并且容器中的进程属于运行状态。即容器运行着真正的应用进程。容器有创建、运行、停止、暂停和删除五种状态。

虽然容器的本质是主机上运行的一个进程,但是容器有自己独立的命名空间隔离和资源限制。也就是说,在容器内部,无法看到主机上的进程、环境变量、网络等信息,这是容器与直接运行在主机上进程的本质区别。

仓库

Docker 的镜像仓库类似于代码仓库,用来存储和分发Docker镜像。镜像仓库分为公共镜像仓库和私有镜像仓库。

目前,Docker Hub 是Docker官方的公开镜像仓库,它不仅有很多应用或者操作系统的官方镜像,还有很多组织或者个人开发的镜像供我们免费存放、下载、研究和使用。除了公开镜像仓库,你也可以构建自己的私有镜像仓库。

架构

基于开放容器计划(OCI)相关标准的要求,Docker引擎采用了模块化的设计原则,其组件是可替换的。Docker引擎由如下主要的组件构成:Docker客户端(Docker Client)、Docker守护进程(Docker daemon)、containerd以及runc。它们共同负责容器的创建和运行。

目前Docker引擎的架构示意图如图所示:

image.png

Docker客户端

Docker客户端其实是一种泛称。其中docker命令是Docker用户与Docker服务端交互的主要方式。除了使用docker命令的方式,还可以使用直接请求REST API的方式与Docker服务端交互,甚至还可以使用各种语言的SDK与Docker服务端交互。

runc

runc是OCI容器运行时规范的参考实现,实质上是一个轻量级的、针对Libcontainer进行了包装的命令行交互工具(Libcontainer取代了早期Docker架构中的LXC)。通俗地讲,runc是一个用来运行容器的轻量级工具,是真正用来运行容器的。

containerd

containerd是Docker服务端的一个核心组件,它是从dockerd中剥离出来的,它的主要任务是容器的生命周期管理——start | stop | pause | rm....。随着时间的推移,它被赋予了更多的功能,比如镜像管理。此外,containerd组件确保了Docker镜像能够以正确的OCI Bundle的格式传递给runc。

containerd位于dockerd和runc所在的OCI层之间,通过containerd-shim启动并管理runc。Kubernetes也可以通过cri-containerd使用containerd,在Kubernetes中,containerd是一个很受欢迎的容器运行时。

shim

shim 是实现无daemon的容器(用于将运行中的容器与containerd解耦,以便进行dockerd升级等操作)不可或缺的工具。

containerd通过指挥runc来创建新容器。事实上,每次创建容器时它都会fork一个新的runc实例。不过,一旦容器创建完毕,对应的runc进程就会退出。因此,即使运行上百个容器,也无须保持上百个运行中的runc实例。一旦容器进程的父进程runc退出,相关联的containerd-shim进程就会成为容器的父进程。

作为容器的父进程,shim的部分职责如下:

  • 保持所有STDIN和STDOUT流是开启状态,从而当dockerd重启的时候,容器不会因为管道(pipe)的关闭而终止。
  • 将容器的退出状态反馈给 dockerd。

延伸阅读:从 docker 到 runC

各组件之间的关系

首先通过以下命令来启动一个busybox容器:

$ docker run -d busybox sleep 3600

容器启动后,通过以下命令查看一下dockerd的PID:

$ sudo ps aux |grep dockerd

root      4147  0.3  0.2 1447892 83236 ?       Ssl  Jul09 245:59 /usr/bin/dockerd

通过上面的输出结果可以得知dockerd的PID为4147。为了 Docker 各组件之间的调用关系,下面使用 pstree 命令查看一下进程父子关系:

$ sudo pstree -l -a -A 4147

dockerd


  |-containerd --config /var/run/docker/containerd/containerd.toml --log-level info


  |   |-containerd-shim -namespace moby -workdir /var/lib/docker/containerd/daemon/io.containerd.runtime.v1.linux/moby/d14d20507073e5743e607efd616571c834f1a914f903db6279b8de4b5ba3a45a -address /var/run/docker/containerd/containerd.sock -containerd-binary /usr/bin/containerd -runtime-root /var/run/docker/runtime-runc


  |   |   |-sleep 3600

事实上,dockerd启动的时候,containerd就随之启动了,dockerd与containerd一直存在。当执行docker run命令(通过busybox镜像创建并启动容器)时,containerd会创建containerd-shim充当 “垫片” 进程,然后启动容器的真正进程sleep 3600。这个过程和架构图是完全一致的。

启动容器的流程

当使用Docker命令行工具执行启动容器的命令时,Docker客户端会将其转换为合适的API格式,并发送到正确的API端点。

API是在daemon中实现的。一旦daemon接收到创建新容器的命令,它就会向containerd发出调用。daemon使用一种CRUD风格的API,通过gRPC与containerd进行通信。

containerd将Docker镜像转换为OCI bundle,并让runc基于此创建一个新的容器。

然后,runc与操作系统内核接口进行通信,基于所有必要的工具(Namespace、CGroup等)来创建容器。容器进程作为runc的子进程启动,启动完毕后,runc将会退出。


与昊
225 声望636 粉丝

IT民工,主要从事web方向,喜欢研究技术和投资之道