Docker 引擎架构:理解 Docker、Moby、containerd 和 runc 之间的关系
1. Docker 引擎的演进历程
Docker 引擎的架构经历了多次重大变革,从最初的单体架构逐步演变为现在的模块化设计。理解这些组件之间的关系,需要先了解 Docker 引擎的演进历程。
在早期版本中,Docker 引擎是一个单体程序,包含了所有功能:镜像管理、容器运行时、网络配置等。然而随着容器技术的发展和 Kubernetes 等编排系统的兴起,社区意识到需要将容器运行时标准化,使其可以被不同的上层工具使用。这促使 Docker 将其引擎拆分为多个独立的组件。
2015 年,Docker 公司与其他容器行业领导者共同成立了 https://opencontainers.org/ (OCI,Open Container Initiative),旨在制定容器格式和运行时的行业标准。这一举措推动了 Docker 引擎的模块化重构。
2016 年,Docker 公司将 Docker 引擎的核心组件开源为 Moby 项目,并将容器运行时 containerd 捐赠给 CNCF(Cloud Native Computing Foundation)。这标志着 Docker 引擎从单体架构向模块化架构的重大转变。
2. 核心组件及其职责
2.1 Docker(Docker Engine)
Docker Engine 是用户直接交互的客户端和守护进程,它提供了完整的容器平台功能。从用户角度来看,Docker Engine 包含以下几个部分:
Docker CLI(命令行工具):用户通过 docker
命令与 Docker 交互,如 docker run
、docker build
、docker pull
等。这是用户最常使用的界面,它将用户的命令转换为 API 请求发送给 Docker Daemon。
Docker Daemon(dockerd):这是 Docker 的核心守护进程,负责处理客户端请求、管理镜像、网络、数据卷等。它作为服务运行在后台,监听来自 Docker CLI 或其他客户端的请求。Docker Daemon 本身并不直接运行容器,而是将容器的实际运行委托给 containerd。
Docker API:Docker 提供了 RESTful API,允许程序化地管理 Docker 资源。Docker CLI 实际上就是通过这个 API 与 Docker Daemon 通信的。
从架构角度看,Docker Engine 现在更像是一个高层次的容器管理平台,它在 containerd 之上提供了更丰富的功能,包括:
- 镜像构建(Dockerfile、BuildKit)
- 镜像仓库管理(push、pull)
- 网络管理(bridge、overlay、macvlan 等)
- 数据卷管理
- Swarm 集群编排
- 插件系统
2.2 Moby
Moby 是 Docker 公司在 2017 年推出的开源项目,它是 Docker Engine 的上游项目。理解 Moby 和 Docker 的关系,可以类比为 Fedora 和 Red Hat Enterprise Linux 的关系。
Moby 项目的目标是提供一个"容器系统的组装工具包"。它包含了一系列容器化系统的组件,这些组件可以像乐高积木一样组合在一起,构建定制化的容器平台。Moby 项目包括:
- 容器运行时组件:如 containerd、runc
- 镜像构建工具:如 BuildKit
- 网络组件:如 libnetwork
- 存储组件:如 overlay2、devicemapper
- 编排组件:如 SwarmKit
Docker Engine 实际上是基于 Moby 项目组件构建的一个产品化版本。Docker 公司从 Moby 项目中选取稳定的组件,进行集成测试和质量保证,然后发布为 Docker Engine(Docker CE 或 Docker EE)。
对于普通用户来说,直接使用 Docker Engine 即可。Moby 项目主要面向以下场景:
- 容器平台的开发者和研究者
- 需要定制容器系统的企业
- 希望在 Docker 上游做实验性开发的开发者
2.3 containerd
containerd 是一个工业级的容器运行时,它管理容器的完整生命周期:镜像传输和存储、容器执行和监控、低级存储和网络接口。containerd 于 2017 年捐赠给 CNCF,并在 2019 年成为 CNCF 的毕业项目。
containerd 的设计目标是提供一个最小化但功能完整的容器运行时,它可以被嵌入到更大的系统中。Kubernetes 从 1.24 版本开始,移除了对 dockershim 的支持,直接使用 containerd 作为容器运行时,这体现了 containerd 的重要性和成熟度。
containerd 的主要职责包括:
镜像管理:containerd 负责镜像的拉取、存储和管理。它支持 OCI 镜像格式和 Docker 镜像格式,可以从容器镜像仓库拉取镜像,并将镜像解压到本地文件系统。
容器生命周期管理:创建、启动、停止、删除容器是 containerd 的核心功能。它通过调用 runc 来实际创建和运行容器,但容器的生命周期管理策略由 containerd 负责。
快照管理:containerd 使用快照(snapshot)机制来管理容器的文件系统。快照是镜像层的一个视图,containerd 支持多种快照驱动,如 overlay、btrfs、zfs 等。
任务管理:在 containerd 的术语中,"任务"(task)代表正在运行的容器进程。containerd 监控和管理这些任务,包括进程的创建、执行和回收。
命名空间隔离:containerd 使用命名空间(namespace)来隔离不同客户端的资源。例如,Docker 使用 moby
命名空间,Kubernetes 使用 k8s.io
命名空间。这使得多个容器平台可以共享同一个 containerd 实例。
containerd 提供了 gRPC API,上层系统(如 Docker、Kubernetes)通过这个 API 与 containerd 通信。你可以使用 ctr
命令行工具直接与 containerd 交互,例如:
# 列出镜像
ctr images list
# 运行容器
ctr run docker.io/library/nginx:latest nginx-test
# 列出任务
ctr tasks list
2.4 runc
runc 是一个轻量级的容器运行时,它是 OCI(Open Container Initiative)运行时规范的参考实现。runc 的职责非常专一:根据 OCI 规范创建和运行容器。
runc 是从 Docker 的 libcontainer 库演化而来的。2015 年,Docker 公司将 libcontainer 改造为一个独立的命令行工具,并将其捐赠给 OCI 作为标准实现。runc 用 Go 语言编写,它封装了 Linux 内核的容器相关特性。
runc 的核心功能包括:
创建容器:runc 根据 OCI 运行时规范的配置文件(config.json
)创建容器。这个配置文件描述了容器的所有属性:根文件系统位置、环境变量、资源限制、命名空间配置等。
配置 Linux 命名空间:runc 使用 Linux 命名空间(namespace)来隔离容器:
- PID 命名空间:隔离进程 ID
- NET 命名空间:隔离网络栈
- MNT 命名空间:隔离文件系统挂载点
- UTS 命名空间:隔离主机名和域名
- IPC 命名空间:隔离进程间通信
- USER 命名空间:隔离用户和组 ID
配置 cgroups:runc 使用 Linux 控制组(cgroups)来限制和监控容器的资源使用:
- CPU 限制
- 内存限制
- 磁盘 I/O 限制
- 网络带宽限制
设置安全特性:runc 配置容器的安全特性,包括:
- Capabilities:细粒度的权限控制
- Seccomp:系统调用过滤
- AppArmor/SELinux:强制访问控制
管理容器进程:runc 启动容器的主进程,并处理进程的信号、退出状态等。
runc 是一个低级工具,通常不直接被用户使用。它的使用方式如下:
# 创建一个 OCI bundle(包含 rootfs 和 config.json)
mkdir mycontainer
cd mycontainer
# 创建根文件系统
mkdir rootfs
# 生成默认配置
runc spec
# 运行容器
runc run mycontainer
在实际的容器系统中,containerd 调用 runc 来创建容器,用户通过 Docker 或 Kubernetes 间接使用 runc。
2.5 其他相关组件
除了上述核心组件,Docker 生态系统还包含其他重要组件:
containerd-shim:这是 containerd 和 runc 之间的中间层。每个容器都有一个对应的 shim 进程,它的作用是:
- 允许 runc 在启动容器后退出,而不影响容器运行
- 保持容器的 STDIN、STDOUT 打开
- 报告容器的退出状态
- 在 containerd 重启时,保持容器继续运行
BuildKit:这是新一代的镜像构建引擎,提供了比传统 docker build
更好的性能和功能:
- 并行构建
- 增量构建
- 构建缓存
- 多阶段构建优化
Docker Compose:虽然不是运行时组件,但 Compose 是定义和运行多容器应用的工具,它使用 YAML 文件描述应用的服务、网络和卷。
libnetwork:Docker 的网络库,提供了容器网络的抽象层,支持多种网络驱动。
3. 组件之间的调用关系
理解了各个组件的职责后,让我们看看它们之间是如何协作的。当用户执行 docker run
命令时,整个调用链如下:
详细的调用流程如下:
第一步:用户命令解析
用户在终端执行 docker run nginx
,Docker CLI 解析命令参数,包括镜像名称、端口映射、环境变量等。然后,Docker CLI 通过 Docker API(通常是 Unix socket /var/run/docker.sock
或 TCP 端口)将请求发送给 Docker Daemon。
第二步:Docker Daemon 处理请求
Docker Daemon(dockerd)接收到请求后,首先检查本地是否有 nginx 镜像。如果没有,dockerd 会从配置的镜像仓库(默认是 Docker Hub)拉取镜像。镜像拉取完成后,dockerd 会进行以下处理:
- 解析镜像的配置
- 创建容器的网络配置
- 准备数据卷挂载
- 生成容器的配置信息
第三步:调用 containerd
Docker Daemon 通过 gRPC 调用 containerd 的 API,请求创建容器。dockerd 将容器配置传递给 containerd,包括镜像引用、容器名称、资源限制等。containerd 接收请求后,进行以下操作:
- 解压镜像层到本地文件系统
- 创建容器的根文件系统快照
- 生成 OCI 运行时配置(
config.json
)
第四步:启动 containerd-shim
containerd 不直接调用 runc,而是先启动一个 containerd-shim 进程。这个 shim 进程的作用是在容器和 containerd 之间提供一个中间层,使得:
- containerd 可以重启或升级,而不影响正在运行的容器
- 容器的标准输入输出流可以正确地被保持和转发
- 容器退出时,shim 可以收集退出状态并通知 containerd
第五步:runc 创建容器
containerd-shim 调用 runc,传递 OCI bundle(包含 rootfs 和 config.json)的路径。runc 执行以下底层操作:
- 创建 Linux 命名空间(PID、NET、MNT、UTS、IPC、USER)
- 设置 cgroups 限制(CPU、内存、I/O)
- 配置网络接口
- 设置安全策略(capabilities、seccomp、AppArmor/SELinux)
- 挂载根文件系统
- 执行容器的 entrypoint 或 command
runc 完成容器创建后,容器进程开始运行。此时,runc 进程退出,但容器进程继续运行,由 containerd-shim 监控。
第六步:容器运行和监控
容器进程运行后,containerd-shim 监控容器的状态:
- 转发容器的标准输入输出
- 监听容器进程的退出信号
- 当容器退出时,收集退出状态码
containerd 通过 shim 获取容器的运行状态,并通过 gRPC API 暴露给 Docker Daemon。Docker Daemon 将容器状态信息返回给 Docker CLI,最终呈现给用户。
用户可以通过 docker ps
查看运行中的容器,通过 docker logs
查看容器日志,通过 docker stop
停止容器。这些操作都会经过类似的调用链:Docker CLI → Docker Daemon → containerd → containerd-shim → 容器进程。
4. 不同场景下的组件使用
4.1 使用 Docker Engine
这是最常见的场景,用户安装 Docker Engine,通过 docker
命令管理容器。在这个场景下:
- 用户层:Docker CLI
- 管理层:Docker Daemon(dockerd)
- 运行时层:containerd + runc
这种方式提供了完整的 Docker 功能,包括镜像构建、网络管理、数据卷管理、Swarm 集群等。适合:
- 本地开发环境
- 小型生产部署
- 使用 Docker Compose 的应用
- 需要 Docker Swarm 的场景
4.2 使用 containerd(Kubernetes)
Kubernetes 从 1.24 版本开始,移除了 dockershim,直接使用 containerd 作为容器运行时。在这个场景下:
- 用户层:kubectl、Kubernetes API
- 管理层:kubelet
- 运行时层:containerd + runc
kubelet 通过 CRI(Container Runtime Interface)与 containerd 通信。containerd 实现了 CRI 插件,使其可以无缝对接 Kubernetes。这种方式的优势包括:
- 更简洁的架构,减少调用链
- 更好的性能
- 更小的攻击面
- 更快的启动速度
对于 Kubernetes 用户,虽然不再使用 Docker,但可以继续使用 Docker 构建镜像,因为 containerd 完全兼容 Docker 镜像格式。
4.3 直接使用 containerd
一些场景下,用户可能希望直接使用 containerd,而不需要 Docker 的额外功能。例如:
- 构建自定义的容器平台
- 嵌入式系统或物联网设备
- 需要最小化运行时的场景
直接使用 containerd 可以通过 ctr
命令行工具或 containerd API。这种方式提供了最基础的容器功能,但缺少 Docker 的高级特性(如 Dockerfile 构建、Docker Compose 等)。
4.4 直接使用 runc
在极少数情况下,用户可能需要直接使用 runc。这通常出现在:
- 教育和研究目的,理解容器的底层实现
- 开发新的容器运行时
- 需要完全自定义容器配置的场景
直接使用 runc 需要手动准备 OCI bundle(rootfs 和 config.json),这是非常低级的操作,不推荐在生产环境中使用。
5. OCI 标准的重要性
Open Container Initiative(OCI)标准在容器生态系统中扮演着关键角色。OCI 定义了两个主要规范:
5.1 运行时规范(Runtime Specification)
运行时规范定义了如何运行一个"文件系统 bundle",这个 bundle 包含了容器的根文件系统和配置文件。规范的主要内容包括:
配置文件格式:config.json
文件描述了容器的所有属性,包括进程参数、环境变量、挂载点、Linux 特定配置(命名空间、cgroups、capabilities 等)。
容器生命周期:规范定义了容器的状态转换:creating → created → running → stopped。每个状态转换对应特定的操作(create、start、kill、delete)。
运行时和生命周期钩子:允许在容器生命周期的特定时刻执行自定义操作。
runc 是这个规范的参考实现,但也有其他实现,如 crun(用 C 编写)、kata-containers(基于虚拟化的运行时)等。这些不同的实现都遵循同一规范,因此可以互换使用。
5.2 镜像规范(Image Specification)
镜像规范定义了容器镜像的格式,包括:
镜像清单(Manifest):描述镜像的配置和层。一个镜像由多个只读层组成,每层代表 Dockerfile 中的一条指令。
镜像配置:包含镜像的元数据,如作者、创建时间、默认命令、环境变量等。
文件系统层:镜像的实际内容,通常使用 tar 归档和 gzip 压缩。
Docker 镜像格式是基于 OCI 镜像规范的,这意味着 Docker 构建的镜像可以在任何符合 OCI 标准的运行时中运行,如 containerd、CRI-O、Podman 等。
5.3 标准化的价值
OCI 标准化带来了以下好处:
互操作性:不同的工具和平台可以共享相同的镜像和容器。你可以用 Docker 构建镜像,用 Podman 运行,用 Kubernetes 编排。
创新空间:标准化的接口允许社区开发新的工具和运行时,而不需要重新发明轮子。例如,gVisor 和 Kata Containers 提供了更强的隔离性,同时保持与现有工具的兼容性。
供应商中立:OCI 由多个公司和组织共同维护,避免了单一供应商的锁定。用户可以自由选择最适合自己需求的工具。
生态系统繁荣:标准化促进了容器生态系统的发展,催生了大量优秀的开源项目,如 Buildah(无需 daemon 的镜像构建)、Skopeo(镜像管理工具)、Podman(无 daemon 的容器引擎)等。
6. 实战示例:查看组件运行状态
为了更好地理解这些组件的关系,我们可以通过一些实际操作来观察它们的运行状态。
6.1 查看 Docker 进程结构
在 Linux 系统上运行一个容器,然后查看进程树:
# 运行一个 nginx 容器
docker run -d --name mynginx nginx
# 查看进程树
pstree -p | grep -A 10 dockerd
你会看到类似以下的进程结构:
dockerd(1234)───containerd(1235)───containerd-shim(5678)───nginx(5679)
├─nginx(5680)
└─nginx(5681)
这清晰地展示了调用链:dockerd → containerd → containerd-shim → 容器进程。
6.2 直接使用 containerd 命令
你可以使用 ctr
命令直接与 containerd 交互:
# 列出镜像(注意需要指定命名空间)
ctr -n moby images list
# 列出容器
ctr -n moby containers list
# 列出正在运行的任务
ctr -n moby tasks list
这里的 -n moby
指定了 Docker 使用的命名空间。containerd 使用命名空间来隔离不同客户端的资源。
6.3 查看 OCI bundle
当 containerd 创建容器时,它会在磁盘上生成 OCI bundle。你可以在 containerd 的数据目录中找到这些文件:
# containerd 的数据目录(可能因系统而异)
ls -la /var/lib/containerd/io.containerd.runtime.v2.task/moby/
# 查看某个容器的配置
cat /var/lib/containerd/io.containerd.runtime.v2.task/moby/<container-id>/config.json
这个 config.json
文件就是传递给 runc 的配置,它包含了容器的所有运行时配置。
6.4 使用不同的运行时
Docker 允许配置不同的运行时。除了默认的 runc,你还可以使用其他 OCI 兼容的运行时:
# 配置 Docker 使用不同的运行时
# 编辑 /etc/docker/daemon.json
{
"runtimes": {
"kata": {
"path": "/usr/bin/kata-runtime"
},
"gvisor": {
"path": "/usr/bin/runsc"
}
}
}
# 重启 Docker
systemctl restart docker
# 使用指定的运行时运行容器
docker run --runtime=kata -d nginx
docker run --runtime=gvisor -d nginx
这展示了 OCI 标准的强大之处:你可以轻松地切换底层运行时,而无需改变上层工具。
7. 常见问题和误区
7.1 Docker 被移除了吗?
Kubernetes 1.24 移除了 dockershim,这引起了一些误解。实际上:
- Kubernetes 移除的是 dockershim,而不是对 Docker 镜像的支持
- 你仍然可以使用 Docker 构建镜像,这些镜像可以在 Kubernetes 中运行
- Kubernetes 现在直接使用 containerd,这是 Docker Engine 内部也在使用的组件
- 对于终端用户,这个改变几乎是透明的
7.2 containerd 和 Docker 哪个更好?
这个问题没有绝对的答案,取决于使用场景:
使用 Docker Engine 的场景:
- 本地开发环境,需要 Docker CLI 的便利性
- 使用 Docker Compose 定义多容器应用
- 需要 Docker 的高级功能(如 BuildKit、多阶段构建)
- 使用 Docker Swarm 进行集群编排
直接使用 containerd 的场景:
- Kubernetes 集群,直接使用 containerd 更简洁高效
- 需要更小的攻击面和更好的安全性
- 嵌入式系统或资源受限的环境
- 构建自定义的容器平台
实际上,Docker Engine 内部使用 containerd,所以并不是非此即彼的选择。Docker 在 containerd 之上提供了更多功能和更好的用户体验。
7.3 Podman 是什么?
Podman 是 Red Hat 开发的另一个容器引擎,它的特点是:
- 无守护进程(daemonless):Podman 不需要后台守护进程,每个命令直接与容器交互
- 兼容 Docker CLI:Podman 的命令行接口与 Docker 高度兼容,可以作为 Docker 的替代品
- Rootless 容器:Podman 更好地支持非 root 用户运行容器
- Pod 概念:Podman 原生支持 Kubernetes 风格的 Pod(多个容器共享网络命名空间)
Podman 也使用 OCI 标准,底层同样使用 runc 作为运行时。它可以看作是 Docker 的一个替代方案,特别适合不希望运行守护进程的场景。
7.4 是否需要理解所有这些组件?
对于大多数用户,不需要深入了解每个组件的细节:
- 应用开发者:主要使用 Docker CLI 或 Kubernetes API,理解基本的容器概念即可
- 运维工程师:需要理解 Docker Engine、containerd 的配置和调优
- 平台开发者:需要深入理解所有组件的架构和 API
- 安全专家:需要了解从 runc 到 Docker Daemon 的整个调用链,以识别潜在的安全风险
了解这些组件的关系有助于:
- 更好地排查问题
- 优化容器性能
- 理解安全边界
- 做出正确的技术选型
8. 总结
Docker 引擎的架构体现了软件工程中的重要原则:模块化、标准化和关注点分离。通过将容器系统拆分为多个独立的组件,每个组件专注于自己的职责,整个生态系统变得更加灵活和强大。
回顾一下核心组件的关系:
Docker Engine 是用户直接交互的容器平台,提供完整的功能集。它在 containerd 之上增加了镜像构建、网络管理、卷管理等功能。
Moby 是 Docker Engine 的上游项目,提供了一套容器系统的组件工具包。Docker Engine 是基于 Moby 构建的产品化版本。
containerd 是工业级的容器运行时,管理容器的生命周期、镜像、快照等。它是 CNCF 毕业项目,被 Docker、Kubernetes 等广泛使用。
runc 是 OCI 运行时规范的参考实现,负责实际创建和运行容器。它使用 Linux 内核特性(命名空间、cgroups)来隔离和限制容器。
containerd-shim 作为中间层,使得容器可以在 containerd 重启时继续运行,并负责监控容器进程。
这些组件通过清晰的接口协作,形成了现代容器系统的基础。OCI 标准确保了不同组件之间的互操作性,促进了容器生态系统的繁荣。无论是使用 Docker、Kubernetes、Podman 还是其他容器工具,它们都建立在这些基础组件之上。
理解这些组件的关系,不仅有助于更好地使用容器技术,也为深入学习容器原理、优化容器性能、排查容器问题打下了坚实的基础。随着云原生技术的发展,这些知识将变得越来越重要。