目录

容器 | Docker 版本号变换及架构变化摘记

1. 版本号变换

v1.11.0 — 14 Apr 2016

v1.11.1 — 27 Apr 2016

v1.11.2 — 2 Jun 2016

v1.12.0 — 29 Jul 2016

v1.12.1 — 19 Aug 2016

v1.12.2 — 12 Oct 2016

v1.12.3 — 27 Oct 2016

v1.12.4 — 13 Dec 2016

v1.12.5 — 16 Dec 2016

v1.12.6 — 11 Jan 2017

v1.13.0 — 19 Jan 2017

v1.13.1 — 9 Feb 2017

v17.03.0-ce — 2 Mar 2017

v17.03.1-ce — 28 Mar 2017

v17.04.0-ce — 6 Apr 2017

v17.05.0-ce — 6 May 2017

……

v18.05.0-ce — 26 Apr 2018

v18.06.0-ce — 19 Jul 2018

v18.06.1-ce — 22 Aug 2018

v18.09.0 — 8 Nov 2018

v18.09.1 — 10 Jan 2019

……

v19.03.0 — 23 Jul 2019

…….

v19.03.5 — 15 Nov 2019

2. 架构变换

2.1. 最开始

Docker 首次发布时,Docker 引擎由两个核心组件构成:LXC 和 Docker daemon。

  • Docker daemon 是一个单一的二进制文件,包含了诸如 Docker 客户端、Docker API、容器运行时、镜像构建等。
  • LXC 提供了对诸如命名空间(Namespace)和控制组(CGroup)等基础工具的操作能力,它们是基于 Linux 内核的容器虚拟化技术。

https://img.dawnguo.cn/Container/image-20201022145211245.png

2.2. LXC 变成 Libcontainer

Docker 公司开发了名为 Libcontainer 的自研工具,用于替代 LXC。Libcontainer 的目标是成为与平台无关的工具,可基于不同内核为 Docker 上层提供必要的容器交互功能。在 Docker 0.9 版本中,Libcontainer 取代 LXC 成为默认的执行驱动。

2.2.1. Docker 1.2.0 架构讲解

https://img.dawnguo.cn/Container/image-20201027095838012.png

2.3. 拆解 daemon — 现在的架构

Docker daemon 的整体性带来了越来越多的问题:难于变更、运行越来越慢。之后 Docker 公司开始努力拆解这个大而全的 Docker daemon 进程,并将其模块化。这项任务的目标是尽可能拆解出其中的功能特性,并用小而专的工具来实现它。这些小工具可以被替换,也可以被第三方拿去用于构建其他工具。

这遵循了 UNIX 中得以实践并验证过的一种软件哲学:小而专的工具可以组装成大型工具。

Docker 1.11.0 版本发布后 Docker Daemon 的架构由原来一个模块,被拆分成了 4 个模块:docker、containerd、docker-containerd-shim、docker-runc。关于容器执行和容器运行时的代码已经完全从 daemon 中移除,并重构为小而专的工具,比如容器运行代码就用 runc 实现了。runc 是 OCI 容器运行时标准的参考实现,它的目标就是与 OCI 规范保持一致。

OCI 定义两个容器相关的规范:镜像规范和容器运行时规范。这两个规范在 2017 年 7 月发布了 1.0 版本。但是在 1.0 版本发布之前,Docker 引擎就已经遵循该规范实现了部分功能。

https://img.dawnguo.cn/Container/image-20201022150614934.png

2.3.1. 各个组件

2.3.1.1. daemon

当越来越多的功能从 daemon 中拆解出来并模块化之后,daemon 的主要功能就剩镜像管理、镜像构建、REST API、身份验证、安全、核心网络、卷以及编排等。

2.3.1.2. containerd

Docker daemon 的功能进行拆解后,所有的容器执行逻辑被重构到了 containerd 中。它的主要任务是容器的生命周期管理(start|stop|pause|rm),它可以指挥与 OCI 兼容的容器运行时来创建容器,默认情况下使用 runc。除此之外, containerd 组件还需要确保 Docker 镜像能够以正确的 OCI Bundle 的格式传递给 runc。

containerd 最初被设计为轻量级的小型工具,仅用于容器的生命周期管理。然而,随着时间的推移,它被赋予了更多的功能,比如镜像管理。containerd 额外的功能往往都是模块化的、可选的,便于自行选择所需功能。这样,像 kubernetes 这样的项目在使用 containerd 时,可以自己选择需要包含的功能。

containerd 在 Linux 中是以 daemon 的方式运行(从 1.11 版本之后就这样使用了,因为 containerd 是在 1.11 版本中被拆解出来的)。

containerd 是由 Docker 公司开发,并捐献了云原生计算基金会(Cloud Native Computing Foundation,CNCF)。2017 年 12 月发布了 1.0 版本,具体的发布信息可见 Github 中的 containerd/containerd。

2.3.1.3. shim

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

shim 的部分职责主要如下:

  • 保持所有 STDIN、STDOUT 流是开启状态,所以当 daemon 重启的时候,容器不会因为管道的关闭而终止,这样的好处就是对 daemon 的维护和升级工作不会影响到运行中的容器。
  • 将容器的退出状态反馈给 daemon。

2.3.1.4. runc

runc 是 OCI 容器运行时规范的参考实现,Docker 公司参与了规范的制定与 runc 的开发。runc 实质上一个轻量级的、针对 Libcontainer 进行了包装的命令行交互工具。

runc 只有一个作用那就是创建容器,它使用与 OCI 兼容的 bundle 来启动容器。runc 是一个 CLI 包装器,实质上就是一个独立的容器运行时工具。因此可以直接下载它并基于源码编译出一个二进制文件,即可拥有一个全功能的 runc。

runc 所在的那一层也称为 OCI 层,关于 runc 的发布信息见 Github opencontainers/runc 库。

上述提到的组件,在 Linux 中系统中都由单独的二进制文件来实现,具体包括 dockerd(Docker daemon)、containerd(containerd)、container-shim(shim)、docker-runc(runc)。通过 ps 命令可以看到上述这些组件的进程,只是有些进程只有在运行容器的时候才可见。

2.3.2. 启动一个新的容器的过程

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

1
docker container run -it alpine sh

API 是在 daemon 中实现的(该 API 是一套功能丰富、基于版本的 REST API(也就是 HTTP API),这套 API 已经成为了 Docker 的标志,并且被行业接受成为事实上的容器 API)。

那么,一旦 daemon 接收到创建容器的命令,它就会向 containerd 发出调用(daemon 已经不再包含任何创建容器的代码了)。daemon 使用一种 CRUD 风格的 API,通过 gRPC 与 containerd 进行通信。

这套功能丰富、基于版本的 REST API 已经成为 Docker 的标志,并且被行业接收成为事实上的容器 API。

daemon 进程会监听 /var/run/docker.sock 这个 Unix 套接字文件,来获取来自客户端的 Docker 请求。

contaienrd 并不负责创建容器,而是指挥 runc 去创建容器。container 将 Docker 镜像转换为 OCI bundle,并让 runc 基于此创建一个新的容器。

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

整个启动过程如下图所示:

https://img.dawnguo.cn/Container/image-20200813164502850.png

简单来说,docker daemon 就相当于一个服务器,接收客户端传来的数据,之后服务将这个数据发送给 containerd ,由 containerd 来创建管理容器。 daemon 其实就相当于一个接受者,实际管理容器的是 containerd。

2.3.3. 总结

将所有的用于启动、管理容器的逻辑和代码从 daemon 中移除,意味着容器运行时与 Docker daemon 实现了解耦(有时称之为无守护进程的容器)。这样的好处就是对 Docker daemon 的维护和升级工作不会影响到运行中的容器(在最开始的那个版本中,所有容器运行时的逻辑都在 daemon 中实现,启动和停止 daemon 都会导致宿主机上所有运行中的容器被杀掉,这在生产环境中是一个大问题)。

2.3.4. 小记

  • containerd 其实就是负责创建和管理容器的,虽然真正的创建工作由 runc 负责
  • runc 实质上一个轻量级的、针对 Libcontainer 进行了包装的命令行交互工具。

2.4. 巨人的肩膀

  1. 《深入浅出 Docker》