现代 Docker 引擎由如下主要的组件构成:Docker 客户端(Docker Client)、Docker 守护进程(Docker daemon)、containerd 以及 runc。它们共同负责容器的创建和运行。下面是 Docker 引擎的发展过程:
首发版本
Docker 首次发布时,Docker 引擎由两个核心组件构成:LXC 和 Docker daemon。
其中,Linux 容器(LXC)是第一个、最完整的 Linux 容器管理器的实现方案。2008 年,通过将 Cgroups 的资源管理能力和 Linux Namespace 的视图隔离能力组合在一起,LXC 完整的容器技术出现在 Linux 内核中,并且可以在单个 Linux 内核上运行而无需任何补丁。而 Docker daemon 是单一的二进制文件,包含诸如 Docker 客户端、Docker API、容器运行时、镜像构建等。
下图阐释了在 Docker 旧版本中,Docker daemon、LXC 和操作系统之间的交互关系。

不过,该架构依赖于 LXC,存在以下问题:
-
依赖于外部工具,对 Docker 来说,存在着巨大的生存风险
-
LXC 使得 Docker 无法实现跨平台(只能在 Linux 平台上使用)
Docker Daemon 的“大而全”也带来了严重的问题:
-
版本更新与功能扩展较难
-
运行较慢,带来性能问题
-
Docker Daemon 运行出现问题,会直接影响容器的运行,不符合软件哲学
Docker 0.9 版本
Docker 公司开发了名为 Libcontainer 的自研工具,用于替代 LXC。Libcontainer 的目标是成为与平台无关的工具,可基于不同内核为 Docker 上层提供必要的容器交互功能。在 Docker 0.9 版本中,Libcontainer 取代 LXC 成为默认的执行驱动。
Docker 1.11 版本
2017 年 7 月 OCI 基金会发布了两个规范(镜像规范与容器运行时规范)的 OCI1.0 版本。2016 年底发布的 Docker1.11 版本基本遵循了 OCI1.0 版本。
从 Docker1.11 版本开始,Docker Daemon 中不再包含任何容器运行时代码,而是将容器运行时单独剥离了出来,形成了 Runc 项目。
目前 Docker 引擎的架构示意图如下图所示,图中有简要的描述。

-
Docker Client:Docker 客户端,Docker 引擎提供的 CLI 工具,用于用户向 Docker 提交命令请求。 -
Dockerd:即 Docker Daemon。在现代 Dockerd 中的主要包含的功能有镜像构建、镜像管理、REST API、核心网络及编排等。其通过 gRPC 与 Containerd 进行通信。 -
runc:只有一个作用——创建容器。不过它是一个 CLI 包装器,实质上就是一个独立的容器运行时工具。因此直接下载它或基于源码编译二进制文件,即可拥有一个全功能的 runc。有时也将 runc 所在的那一层称为“OCI 层”。 -
containerd:在对 Docker daemon 的功能进行拆解后,所有的容器执行逻辑被重构到一个新的名为 containerd 的工具中。它的主要任务是容器的生命周期管理——start | stop | pause | rm······Docker 引擎技术栈中,containerd 位于 daemon 和 runc 所在的 OCI 层之间。containerd 在 Linux 和 Windows 中以 daemon 的方式运行,从 1.11 版本之后 Docker 就开始在 Linux 上使用它。如前所述,containerd 最初被设计为轻量级的小型工具,仅用于容器的生命周期管理。然而,随着时间的推移,它被赋予了更多的功能,比如镜像管理。containerd 是由 Docker 公司开发的,并捐献给了云原生计算基金会(Cloud Native Computing Foundation, CNCF)。
-
shim:实现无 daemon 的容器(用于将运行中的容器与 daemon 解耦,以便进行 daemon 升级等操作)不可或缺的工具。前面提到,containerd 指挥 runc 来创建新容器。事实上,每次创建容器时它都会 fork 一个新的 runc 实例。不过,一旦容器创建完毕,对应的 runc 进程就会退出。因此,即使运行上百个容器,也无须保持上百个运行中的 runc 实例。一旦容器进程的父进程 runc 退出,相关联的 containerd-shim 进程就会成为容器的父进程。作为容器的父进程,shim 的部分职责如下。- 保持所有 STDIN 和 STDOUT 流是开启状态,从而当 daemon 重启的时候,容器不会因为管道(pipe)的关闭而终止。
- 将容器的退出状态反馈给 daemon。