前言
在前面一章的学习中,我们已经了解到了 Docker 的前世今生以及它的一些开源项目,Docker 凭借高度组件化和快捷部署等优点,成为运维和开发中炙手可热的工具。
- 运维视角(Ops)
在运维视角中,主要包括下载镜像、运行和登录新的容器、在容器内输入相关命令、销毁容器等。运维人员关心的是怎么操作,如何利用 Docker 帮助运维人员完成更高效的工作。
- 开发视角(Dev)
在开发视角中,更多关注与应用相关的内容,学习 Docker 底层原理和技术细节,学习编写 Dockerfile,将应用容器化、并在容器内运行,推送至云端仓库,包括使用 Golang 为 Docker 一众开源生态提交贡献等。
底层实现
基本架构
Docker 采用了 C/S 架构,即客户端(client)和服务端(server)。Docker 守护进程(Daemon)作为服务端接受来自客户端的请求,并处理这些请求(创建、运行、分发容器)。客户端和服务端既可以运行在一个机器上,也可通过 socket 或者 RESTful API 来进行通信。
- Docker Daemon 守护进程一般在宿主主机后台运行,等待接收来自客户端的消息。
- Docker Client 客户端则为用户提供一系列可执行命令,用户用这些命令实现跟 Docker 守护进程交互。
Docker 引擎
在“Docker 前世今生”已经略微提到了关于 Docker 引擎的一些内容,可以将其类比成虚拟机(VM)的引擎 ESXi。不过在本节中,将会带来更多 Docker 引擎的具体细节。
Docker 引擎是用来运行和管理容器的核心软件。基于开放容器计划(OCI)相关标准的要求,Docker 引擎采用了模块化的设计原则,其组件是高度解耦的,是可以替换的。
Docker 引擎主要由以下构件组成:Docker 客户端(Docker Client)、Docker 守护进程(Docker Daemon)、contained 以及 runc 和 shim。
(补图)
早期的 Docker 引擎是由两个核心组件构成:LXC 和 Docker Daemon。
- Docker Daemon 是单一的二进制组件,包含了 Docker Client、Docker API、容器运行时、镜像构建等。
- LXC 提供了诸如 命名空间(Namespace)和 控制组(CGroup)等基础工具的操作能力,它们是基于 Linux 内核的容器虚拟化技术。
(Docker 和 LXC 交互图)
高度依赖于 LXC 的 Docker 始终是一个难以解决的问题,如同核心技术掌握在别人手中,随时都有可能被掐断命脉之焦灼。因此,Docker 公司自研开发了名为 Libcontainer 的工具,用于替代 LXC,Libcontainer 用于在不同 OS 内核上为 Docker 上层提供必要的容器交互功能。
Docker Daemon 组件化
随着时间的推移,Docker Daemon 变得越来越厚重、冗余,这不符合社区生态所期望的,于是,拆解重构 Docker Daemon 的工作就被腿推上日程了。
UNIX 的软件哲学:“小而专的工具可以组装为大型工具。”
直至今天,这项拆解和重构的工作仍在继续中,Docker Daemon 的所有 容器执行 和容器运行时 的代码都已经完全被移除,而是重构成小而精悍的工具。目前 Docker Daemon 中依然存在的功能包括但不限于 API、镜像管理、身分验证、安全特性、核心网络及卷(持久化存储)。
(Docker 引擎架构)
runc 工具
runc 是开放容器计划(OCI)主导下的技术实现,Docker 公司参与了规范的制定和 runc 的开发。
runc 实质上就是一个针对 Libcontainer 进行了包装的命令行交互工具,具备轻量化的特点,runc 被用于创建容器。
containerd 工具
在对厚重的 Docker Daemon 的功能进行拆解重构之后,所有容器内的执行逻辑被集成到一个名为 containerd 的工具中,在 Linux 和 Windows 中它以守护进程 (daemon) 运行,它的主要用途是 容器的生命周期管理(start|stop|pause|rm…)。
原来这就是生命周期管理?可以类比 ”service start|stop|restart|status… 服务名 ” 的系统服务管理命令进行理解!QwQ
时至今日,containerd 已经成为 Kubernetes 中默认的、常见的 容器运行时。
启动容器的流程
当使用 Docker 命令行工具启动一个容器时,Docker Client 会将这个命令转换成 指定的 API 格式,并发送到正确的端点。
API 是在 daemon 中实现的,这套功能丰富、基于版本的 REST API 已经成为 Docker 的标志,并且被行业接受成为事实上的容器 API。
要想深入学习后端知识的话,了解 REST API 技术原理还是必不可少的,可以作为拓展再去搜寻相关资料学习。
daemon 使用一种创建容器的 CRUD 风格的 API,通过 gRPC 与 containerd 进行通信。
CRUD 属于 REST 架构,看来真的要补课了!
(交互图)
一旦 daemon 接收到创建新容器的命令,它就会向 containerd 发出调用,再由 containerd 指挥 runc 去做(也就是说,runc 是负责创建容器直接执行者),因为 daemon 已经不包括任何关于创建容器的代码了。
shim 组件
在上述交互图中,可以发现 shim 组件的身影。不过 shim 与 daemon 是解耦的,所以 shim 在进行自身的维护和更新时,是不会影响 daemon 工作的。
shim 的部分职责如下:
- 保持所有 STDIN 和 STDOUT 流是开启状态,从而当 daemon 重启的时候,容器不会因为管道(pipe)的关闭而终止。
- 将容器的退出状态反馈给 daemon。
在 Unix/Shell 中,管道(pipe)是一个很重要的概念,我会在后续 Shell 脚本编写的章节中再详细地介绍这一概念。
重构后的优势
由于 OCI 的影响,重构后的 Dokcer Daemon 已经完全移除了用于创建、管理容器的代码,这意味着在容器与 daemon 之间是解耦的,有时也会称之为“无守护进程的容器,daemonless containerd”;因此,对 Dokcer Daemon 的升级和维护过程,并不会影响到正在运行状态中的容器。