容器引擎的构成
首先以 CNCF 的 containerd 容器引擎为例,讲述容器引擎大致的构成。

上图如果把它分成左右两边的话,可以认为 containerd 提供了两大功能。

runtime,也就是对于容器生命周期的管理。
storage,也就是对一个镜像存储的管理。
按照水平层次来看的话:

第一层是 GRPC,containerd 对于上层来说是通过 GRPC serve 的形式来对上层提供服务的。Metrics 这个部分主要是提供 cgroup Metrics 的一些内容;
下面一层的左边是容器镜像的一个存储,中间是 images、containers,下面是 Metadata,这部分 Matadata 是通过 bootfs 存储在磁盘上面的。右边的 Tasks 是管理容器的容器结构,Events 是对容器的一些操作都会有一个 Event 向上层发出,然后上层可以去订阅这个 Event,由此知道容器状态发生什么变化;
最下层是 Runtimes 层,这个 Runtimes 可以从类型区分,比如 runC 或者是 kata container 之类。
Docker 的组件
Docker 的软件架构包括:

Docker Client:向 Docker Server 进程发起请求,如:build、pull、run 等操作。Docker Client 既可以在访问本地守护(local host)进程,也可以访问远程(remote host)守护进程。
Docker Server:侦听 REST API 请求并管理 Docker 对象,例如:镜像,容器,网络和卷。守护程序还可以与其他守护程序通信以管理 Docker 服务。
Docker Registry(注册表,仓库注册服务器):存储 Docker Image 的中央仓库。其中 Docker Hub 是任何人都可以使用的 Public Registry,Docker Server 默认配置在 Docker Hub 上查找 Images。个人也可以运行 Private Registry,如果使用 Docker DataCenter,则其中包括 Docker Trusted registry(DTR)。使用 docker pull 或 docker run 指令时,所需的 Image 将从 Docker Server 配置的 Registry 中提取。
注意,仓库(Repository)和注册表(Registry)是有区别的。Registry 上往往存放着多个 Repository,每个 Repository 中又包含了多个Images,每个 Image 有着不同的 Tag(标签)。
Docker 的软件架构
从上图可以看出,Docker 主要的模块有:

Docker Client
Docker Daemon
Docker Registry
Graph
Driver
Libcontainer
Docker Container
用户使用 Client 与 Daemon 建立通信,并发送请求给后者 Daemon 作为 Docker 的核心,首先提供 Server 来接受 Client 的请求,而后通过 Engine 执行 Docker 内部的一系列工作,每一项工作都是以一个 Job 的形式的存在。

当需要为 Container 提供 Image 时,则从 Registry 中下载镜像,并通过镜像管理驱动 Graphdriver 将下载镜像以 Graph 的形式存储;
当需要为 Container 创建网络环境时,则通过网络管理驱动 Networkdriver 创建并配置 Container 网络环境;
当需要为 Container 限制运行资源或执行用户指令等操作时,则通过 Execdriver 来完成。
而 Libcontainer 则作为一个独立的 Container 管理模块,Networkdriver 以及 Execdriver 都是通过 Libcontainer 来完成对 Container 进行的操作。当执行 docker run 的一系列工作后,一个实际的 Container 就处于运行状态,该 Container 拥有独立的文件系统,独立并且安全的运行环境等。

Docker Client
Docker Client 在 Linux 上表现为一个 docker 可执行文件,当 Client 接收到 Daemon 返回的响应并进行简单处理后,Client 一次完整的生命周期就结束了。Client 可以通过 3 种方式和 Daemon 建立通信:

tcp://host:port
unix:path_to_socket;fd://socketfd
通过设置命令行参数设置 TLS 连接
Docker Daemon
Docker Daemon 在 Linux 上表现为一个常驻在后台的系统进程,可以通过 systemd 来进行管理,实际上跟 Docker Client 是同一个 docker 可执行文件。

Docker Daemon 可以细分为以下模块:

API Server:Daemon 会在后台启动一个 Docker Server,是一个 API Server,基于 Golang 的 Gorilla/Mux 包,接受 Client 发送的请求并路由分发到不同的 Handler 进行处理。
值得注意的是:Docker Server 的启动是靠一个名为 serveapi 的 Job 运行来完成的。所以 Server 的本质是众多 Job 中的一个。

Engine:是 Docker 的运行引擎,它扮演 Docker Container 存储仓库的角色,并且通过执行 Job 的方式来操纵管理这些容器。Docker Engine 有两个不同的版本:Docker Engine Enterprise(企业版)和 Docker Engine Community(社区版)。

Job:一个 Job 可以认为是 Engine 内部最基本的工作执行单元。Docker 做的每一项工作,都可以抽象为一个 Job。例如:在容器内部运行一个进程,这是一个 Job;创建一个新的容器,这是一个 Job,从 Internet上 下载一个文档,这是一个 Job,等等。Job 的设计者,把 Job 设计得与 Unix Processor 相仿。比如说:Job 有一个名称,有参数,有环境变量,有标准的输入输出,有错误处理,有返回状态等。

 

Docker Registry
Docker Registry 是一个存储 Images 的仓库。在 Docker 的运行过程中,Daemon 会与 Registry 通信,并实现搜索镜像、下载镜像、上传镜像三个功能,这三个功能对应的 Job 分别为 search、pul 与 push。

Docker 可以使用公有的 Docker Registry,即:Docker Hub,可以从中找到来自开源项目、软件供应商、乃至个人账户的 Docker Image。同时,Docker 也允许用户构建本地私有的 Docker Registry,这样可以保证容器镜像的获取在内网完成。

Graph
Graph 充当已下载镜像的保管者,以及已下载镜像之间关系的记录者。一方面,Graph 存储着本地具有版本信息的文件系统镜像,另一方面也通过 GraphDB 记录着所有文件系统镜像彼此之间的关系。

其中,GraphDB 是一个构建在 SQLite 之上的小型图数据库,实现了节点的命名以及节点之间关联关系的记录。它仅仅实现了大多数图数据库所拥有的一个小的子集,但是提供了简单的接口表示节点之间的关系。

同时在 Graph 的本地目录中,关于每一个的容器镜像,具体存储的信息有:该容器镜像的元数据,容器镜像的大小信息,以及该容器镜像所代表的具体 Rootfs。

在这里插入图片描述

Driver
Driver 作为驱动模块,Docker 通过 Driver 来实现对 Container 执行环境的定制。可以分为以下三类驱动:

Graphdriver
Networkdriver
Execdriver
Graphdriver
Graphdriver 用于完成 Image 的管理,包括存储与获取。当用户需要下载指定的镜像时,Graphdriver 就将镜像存储在本地的指定目录;当用户需要使用指定的镜像来创建容器的 Rootfs 时,Graphdriver 就从本地镜像存储目录中获取指定的容器镜像。

在 Graphdriver 初始化之前,有 4 种文件系统或类文件系统在其内部注册,它们分别是:

Aufs
Btrfs
Vfs
Devmapper
而 Graphdriver 在初始化之时,通过获取系统环境变量 DOCKER_DRIVER 来提取所使用 Driver 的指定类型。而之后所有的 Graph 操作,都使用该 Driver 来执行。

Graphdriver 的架构如下:

在这里插入图片描述

Networkdriver
Networkdriver 用于完成 Container 网络环境的配置,其中包括:

Docker deamon 启动时为其创建 Bridge 网桥;
Container 创建时为其创建专属虚拟网卡设备,以及为 Container 分配 IP、Port 并与宿主机做端口映射,设置容器防火墙策略等。
Networkdriver 的架构如下:

在这里插入图片描述

Execdriver
Execdriver 作为 Container 的执行驱动,负责创建 Container 运行时 namespace,负责容器资源使用的统计与限制,负责容器内部进程的真正运行等。

在 Execdriver 实现的初期使用了 LXC Driver 调用 LXC 的接口,来操纵容器的配置以及生命周期,而现在 Execdriver 默认使用 Native 驱动,不再依赖于 LXC。可以通过启动 Daemon 时指定 ExecDriverflag 参数来进行选择,默认为 native。

Execdriver 架构如下:

在这里插入图片描述

Libcontainer
Libcontainer 是 Docker 架构中一个使用 Golang 实现的库,设计初衷是希望该库可以不依靠任何依赖,直接访问 Kernel 中与容器相关的 API。

正是由于 Libcontainer 的存在,Docker 最终得以操纵 Container 的 Namespace、Cgroups、Apparmor、网络设备以及防火墙规则等。这一系列操作的完成都不需要依赖 LXC 或者其他库。

Libcontainer 架构如下:

在这里插入图片描述

Docker 将底层容器运行时剥离出来,实现更好的平台无关性。LibContainer 是对各种容器的抽象,发展为 RunC,并贡献给 OCP 组织作为定义容器环境的标准。

Docker Container
Docker Container 变现为一个运行在 Linux 操作系统之上的容器进程,是 Docker 服务交付的最终形式。

用户通过指定 Image,使得 Container 可以自定义 Rootfs 等文件系统。
用户通过指定计算资源的配额,使得 Container 使用指定的计算资源。
用户通过配置网络及其安全策略,使得 Container 拥有独立且安全的网络环境。
用户通过指定运行的命令,使得 Container 执行指定的工作。