Docker、containerd、runc 的关系
一句话讲清三者的关系
如果用一个表格概括:
| 组件 | 角色 | 类比 |
|---|---|---|
| Docker | 面向开发者的全栈工具,提供 CLI、镜像构建、容器管理 | 4S 店:你只需要说”我要买车”,剩下的它帮你搞定 |
| containerd | 容器生命周期的管理者,管理镜像、存储、网络、进程 | 汽车总装厂:负责调度生产线、管理零部件库存 |
| runc | 真正创建并启动容器进程的底层工具,启动完就退出 | 流水线上拧最后一颗螺丝的机器人,拧完就走 |
三者的调用关系是:
docker CLI → dockerd → containerd → containerd-shim → runc
(你的命令) (API网关) (容器管家) (进程保姆) (容器启动器)
下面逐层展开。
一、Docker:开发者的一站式工具箱
Docker 是一套完整的平台工具。你日常用的 docker build、docker run、docker push 全部来自它。
当你执行 docker run -d nginx 时:
- docker CLI 把请求发给 dockerd(Docker 守护进程)
- dockerd 负责编排接下来的所有事情:拉镜像、配网络、启容器
Docker 的职责是将镜像构建(BuildKit)、镜像分发、网络配置、卷存储封装成一条条简单命令。开发者的日常工作只需要和 Docker 交互。
值得注意的是,dockerd 并不直接操作容器进程——它把容器启动和生命周期管理委托给 containerd,自己专注于 API 网关、镜像构建调度、网络与存储编排等高阶工作。
二、containerd:容器的”管家”
containerd 是 CNCF 的毕业项目,也是容器生态的基础设施。它是一个独立的守护进程,专门管理容器的完整生命周期。Docker、Kubernetes 等上层平台都通过它来操控容器。
containerd 的核心职责:
- 镜像管理:pull、push、存储、分层管理
- 容器生命周期:create、start、stop、delete、pause、resume
- 快照存储:通过 snapshotter 插件管理容器文件系统
- 任务执行:调用 runc 创建实际进程,并通过 shim 进程持续监控
在 Docker 体系内,分工是这样的:
- dockerd 负责镜像构建(BuildKit)、网络编排、卷管理、用户 API
- containerd 负责拉镜像、存镜像、管理镜像层、启动/停止容器
containerd 不关心上层是谁(Docker 还是 Kubernetes),它只管把容器管好。这种解耦意味着:你可以用 Docker 构建镜像,用 Kubernetes 直接对接 containerd 运行,同一套镜像体系通吃。
三、runc:容器的”启动器”
如果说 containerd 是管家,那 runc 就是管家手下的执行者。
3.1 runc 只做一件事
runc 的职责极其单一:读取一份符合 OCI Runtime Spec 的 JSON 配置文件,根据配置调用 clone() 创建 Namespace,设置 Cgroups,然后启动容器进程。做完这些,runc 自己就退出了。
它不是守护进程,不常驻后台。每次启动一个容器,containerd 就新开一个 runc 进程;容器启动完毕,runc 退出。容器进程由另一个角色接管——shim。
3.2 OCI 标准与 runc 的关系
OCI(Open Container Initiative,开放容器标准)定义了容器格式和运行时的行业规范,包含两个核心标准:
| 标准 | 内容 | 对应实现 |
|---|---|---|
| OCI Runtime Spec | 定义”如何运行一个容器”:Namespace 如何配置、Cgroups 如何设置、根文件系统如何挂载 | runc 是参考实现 |
| OCI Image Spec | 定义”镜像的格式”:文件系统层如何组织、元数据如何描述 | 所有 Registry 和构建工具遵循此标准 |
这意味着:只要一个程序能生成符合 OCI Runtime Spec 的配置,就能用 runc(或任何兼容的 OCI 运行时)启动容器。Docker、containerd、Kubernetes 都通过这个标准与底层运行时解耦。
四、一次 docker run 的完整旅程
理解了每个组件的职责后,来看一个容器从创建到运行的完整过程:
1. docker run nginx
│
▼
2. dockerd 收到请求,解析参数
│
▼
3. dockerd 调用 containerd:帮我拉 nginx 镜像,然后启动它
│
▼
4. containerd 连接 Registry 下载镜像的各层,存到本地 snapshotter
│
▼
5. containerd 启动一个新进程——containerd-shim(进程保姆)
│
▼
6. shim 调用 runc:给,这是 OCI 配置,帮我创建容器进程
│
▼
7. runc 执行 clone() 创建 Namespace,设置 Cgroups,
加载镜像文件系统,启动 nginx 主进程
│
▼
8. runc 退出。nginx 进程交给 shim 接管
│
▼
9. shim 持续监控 nginx 进程:
- 收集 stdout/stderr
- 向 containerd 报告退出状态
- 容器退出后负责清理
shim 为什么重要
你可能会问:runc 启动容器后退出,那谁来看着容器进程?答案是 shim。
shim 的设计要解决两个问题:
- containerd 重启不影响容器:如果 containerd 升级或崩溃重启,shim 进程不受影响,容器继续运行。
- 收集退出状态:runc 退出后,如果它还是容器的父进程,容器退出会变成僵尸进程。shim 作为父进程,可以正确回收并报告退出信息。
五、Kubernetes 视角:Docker 是可选的
在 Kubernetes 集群中,容器运行时的链路是:
Kubernetes → containerd → runc
注意这里没有 dockerd。Kubernetes 直接对接 containerd 管理容器,因为:
- Kubernetes 自己就通过 CNI 插件管理网络、CSI 插件管理存储,不需要 dockerd 再做一层
- containerd 更轻量、更专注,资源开销更小
- Kubernetes 只需要”管好容器进程”,不需要”帮开发者构建镜像”
但这不意味着 Docker 被淘汰了——镜像格式是 OCI 标准,去哪儿都能跑。开发者依然用 Docker 构建和测试镜像,把镜像推到 Registry,Kubernetes 拉下来通过 containerd 直接运行。Docker 和 Kubernetes 的分工是:Docker 管开发环节,containerd 管生产运行。
六、总结
| Docker | containerd | runc | |
|---|---|---|---|
| 给谁用 | 开发者 | 平台 / 编排系统 | 更底层的运行时 |
| 做什么 | 构建镜像、CLI 交互、完整工具链 | 管理镜像传输和容器生命周期 | 创建一个符合 OCI 标准的容器进程 |
| 运行方式 | 守护进程(dockerd) | 守护进程 | 一次性进程(启动完退出) |
| 依赖关系 | 依赖 containerd | 依赖 runc | 只依赖 Linux 内核 |
| 是否常驻 | 是 | 是 | 否 |
理解这三者的分工,是理解现代容器技术栈的第一步。