Docker使用的是传统的client-server模式,用户通过DockerClient与Docker daemon建立通信,并将请求发送给后者
Docker 引擎由如下主要的组件构成:Docker 客户端(Docker Client)、Docker 守护进程 (Docker daemon)、containerd 以及 runc。它们共同负责容器的创建和运行
Docker 首次发布时,Docker 引擎由两个核心组件构成:LXC 和 Docker daemon。
Docker daemon 是单一的二进制文件,包含诸如 Docker 客户端、Docker API、容器运行 时、镜像构建等
LXC 提供了对诸如命名空间(Namespace)和控制组(CGroup)等基础工具的操作能力, 它们是基于 Linux 内核的容器虚拟化技术。然而LXC作为核心的组件依赖于外部工具,这会给项目带来巨大风险,甚至影响其发展;其次,LXC是基于Linux的,这对于一个立志于跨平台的项目来说是个问题。
Docker公司后续自研Libcontainer的目标是成为与平台无关的工具,可基于不同内核为 Docker 上层提供必要的 容器交互功能,在Docker 0.9 版本中,Libcontainer 取代 LXC 成为默认的执行驱动。
目前 Docker 引擎的架构示意图如下图所示
runc 是 OCI 容器运行时规范的参考实现,runc 实质上是一个轻量级的、针对 Libcontainer 进行了包装的命令行交 互工具,runc 生来只有一个作用——创建容器。
containerd主要任务是容器的生命周期管理——start | stop | pause | rm....
启动一个新的容器
针对如下启动命令
docker container run --name ctr1 -it alpine:latest sh
- 当使用 Docker 命令行工具执行如上命令时,Docker 客户端会将其转换为合适的 API 格式, 并发送到正确的 API 端点
- 一旦 daemon 接收到创建新容器的命令,它就会向 containerd 发出调用。daemon 已经不再包含任何创建容器的代码了。daemon 使用一种 CRUD 风格的 API,通过 gRPC 与 containerd 进行通信。
- containerd 将 Docker 镜像转换为 OCI bundle,并让 runc 基于此创建一个新的容器
- runc 与操作系统内核接口进行通信,基于所有必要的工具(Namespace、CGroup 等) 来创建容器。容器进程作为 runc 的子进程启动,启动完毕后,runc 将会退出
shim
shim 是实现无 daemon 的容器(用于将运行中的容器与 daemon 解耦,以便进行 daemon 升级等操作)不可或缺的工具。
前面提到,containerd 指挥 runc 来创建新容器。事实上,每次创建容器时它都会 fork 一 个新的 runc 实例。不过,一旦容器创建完毕,对应的 runc 进程就会退出。因此,即使运行上百个容器,也无 须保持上百个运行中的 runc 实例。
一旦容器进程的父进程 runc 退出,相关联的 containerd-shim 进程就会成为容器的父进程。 作为容器的父进程,shim 的部分职责如下。
- 保持所有 STDIN 和 STDOUT 流是开启状态,从而当 daemon 重启的时候,容器 不会因为管道(pipe)的关闭而终止。
- 将容器的退出状态反馈给 daemon。