Linux 容器运行时深度解析:containerd、CRI-O 与 runc 的分工

从 Docker 到标准运行时:为什么我们需要分清楚它们

很多刚开始接触 Kubernetes 生产环境的团队,在运维文档里看到 containerd、CRI-O 这些名词时,第一反应往往是困惑。我们不是一直用 Docker 吗,怎么又冒出这么多新东西?

Linux 容器运行时深度解析:containerd、CRI-O 与 runc 的分工

这种困惑恰恰反映了容器技术栈的演进。早期 Docker 是个“大而全”的解决方案,从镜像构建、网络存储到容器运行一手包办。但随着 Kubernetes 成为事实上的编排标准,社区发现编排引擎并不需要 Docker 的所有功能,反而需要一个更专注、更标准化的底层运行时接口。于是,CRI(Container Runtime Interface)规范应运而生,旨在为 kubelet 提供一个统一的容器操作接口。

在这个背景下,containerd 和 CRI-O 作为两种主流的 CRI 实现,走上了前台。而 runc,则作为更底层的 OCI 规范实现者,默默地支撑着它们。理解这三者的分工,是构建稳定、可维护容器平台的基础。

核心角色定位:一张图看清层级关系

为了直观理解,我们可以将整个调用栈自上而下分为三层:编排层、运行时管理层、容器实现层。

  • 编排层(如 Kubernetes kubelet):通过 CRI gRPC 接口发出指令,例如“创建 Pod”、“拉取镜像”。它不关心底层具体是谁。
  • 运行时管理层(containerd 或 CRI-O):接收 CRI 指令,负责镜像管理、容器生命周期事件调度、存储和网络配置的协调。它们是“管家”。
  • 容器实现层(runc 等 OCI 运行时):根据 OCI 镜像和运行时规范,调用 Linux 内核的 namespaces 和 cgroups 等能力,真正创建出隔离的容器进程。它们是“工匠”。

一次典型的容器创建流程(以 containerd 为例)是这样的:kubelet (CRI) → containerd (管理) → containerd-shim (适配) → runc (创建) → Linux Kernel。每一步都职责清晰,通过标准接口解耦。

containerd 与 CRI-O:两种不同的“管家”哲学

containerd 和 CRI-O 都实现了 CRI,都能让 Kubernetes 顺利运行容器,但设计目标决定了它们的架构和气质截然不同。

containerd 出身于 Docker 引擎,后被捐赠给 CNCF。它的目标是成为一个通用的、可嵌入的容器运行时。这意味着它不仅可以通过 CRI 插件服务 Kubernetes,还能通过其他接口服务 Docker CLI、Nomad 等系统。它内置了完整的镜像管理、存储快照(snapshotter)等功能,像一个功能齐全的工具箱。

CRI-O 则是由 Red Hat 主导,从诞生之初就明确了一个目标:专为 Kubernetes 优化。它只实现 CRI 规范,不做任何额外的事情。镜像管理、存储等职责,它依赖于其他专注于该领域的组件(如 containers/image 库、不同的存储驱动)。它的哲学是“只做一件事,并做到极致”。

这种差异在具体功能点上体现得非常明显:

对比维度 containerd CRI-O
设计目标 通用容器运行时 Kubernetes 专用运行时
接口支持 CRI (通过插件)、OCI、遗留 Docker API 仅 CRI
镜像管理 内置完整拉取、存储、垃圾回收 依赖外部库 (containers/image)
架构复杂度 中等,模块化插件设计 简单,组件少且专注
典型内存占用 约 100-200 MB 约 50 MB 或更低
适用场景 需要对接多种编排器,或需要丰富内置功能的场景 纯 Kubernetes 环境,追求极简与稳定

选择哪一个?如果你的团队只需要跑 Kubernetes,且希望节点组件尽可能轻量和专一,CRI-O 是个非常干净的选择。如果你的环境比较复杂,或者未来可能需要对接其他系统,containerd 的通用性会带来更多灵活性。

runc:那个不可或缺的“工匠”

无论是 containerd 还是 CRI-O,在需要真正创建容器进程时,绝大多数情况下都会调用 runc。runc 是一个轻量级的命令行工具,它的唯一职责就是按照 OCI(开放容器倡议)的运行时规范,把容器镜像启动起来。

你可以把 runc 想象成最底层的执行引擎。它不管理镜像,不处理网络,也不提供常驻服务。它的工作就是:

  1. 读取 OCI 格式的容器配置(config.json)和根文件系统(rootfs)。
  2. 调用 Linux 内核系统调用,创建出独立的命名空间(network, pid, mount 等)。
  3. 设置 cgroups 以限制 CPU、内存等资源。
  4. 在隔离好的环境里,启动指定的进程。

下面是一个极其简化的示意,展示了 runc 如何根据一个 OCI 配置创建容器(实际调用由 containerd/CRI-O 完成):

# 这是一个概念性示例,实际由运行时管理组件调用
# runc 需要一个 bundle 目录,里面包含 config.json 和 rootfs/
$ runc create mycontainer
$ runc start mycontainer

一个常见的误解是认为 containerd 或 CRI-O “包含”了 runc。实际上,它们是调用与被调用的关系。runc 是一个独立的二进制文件,符合 OCI 标准的任何运行时(如 gVisor, Kata Containers)都可以作为替代品被 containerd 使用,这为安全沙箱等高级场景提供了可能。

生产环境中的分工与踩坑点

理解了理论分工,在实际运维中,我们更需要关注它们的协作边界和可能出问题的地方。

场景一:镜像拉取失败,谁的责任?

当 Pod 状态卡在 ImagePullBackOff,你需要知道排查路径。如果使用 containerd,镜像拉取、存储、标签管理都是其内置功能,问题可能出在 containerd 的配置(如 /etc/containerd/config.toml 中的 mirror 配置)、磁盘空间或镜像仓库认证上。如果使用 CRI-O,镜像拉取由独立的库处理,你需要检查 CRI-O 的配置文件(如 /etc/crio/crio.conf)以及相关的认证文件(如 /etc/containers/registries.conf)。

场景二:容器网络不通,该找谁?

无论是 containerd 还是 CRI-O,在容器网络方面,它们都是“协调者”,而非“执行者”。它们的职责是在创建容器时,调用配置好的 CNI(容器网络接口)插件,比如 Calico 或 Flannel 的二进制文件。因此,当容器网络出现问题时,首要的排查方向是:

  1. CNI 插件是否安装正确、进程是否存活。
  2. 节点上的网络配置(如 iptables 规则、路由表)是否被 CNI 插件正确设置。
  3. 运行时(containerd/CRI-O)的 CNI 配置目录(如 /etc/cni/net.d/)下的配置文件是否正确。

把网络问题归咎于 containerd 或 CRI-O 本身,往往是找错了方向。

场景三:从 Docker 切换到 containerd 后的“不习惯”

很多团队从 Docker 切换到 containerd 后,第一个不习惯是命令行工具变了。不能再使用 docker psdocker exec 来直接管理容器。替代方案是使用 crictl(针对 CRI 运行时)或 containerd 自带的 ctr 命令。虽然命令不同,但功能都能覆盖。更重要的是,这种切换减少了 dockerd 这一中间层,使得调用链更短,理论上更稳定,资源占用也更低。

总结与选型建议

回顾一下三者的核心分工:

  • runc:底层标准执行者,负责创建符合 OCI 规范的容器进程。
  • containerd:通用运行时管理器,提供丰富的容器生命周期和资源管理功能,通过插件支持多种上层系统。
  • CRI-O:Kubernetes 专用运行时,极致轻量与专注,严格遵循 CRI 规范。

对于大多数团队,我的建议是:

  1. 拥抱标准,告别 Docker 运行时:在新部署的 Kubernetes 集群(1.24+)中,直接使用 containerd 或 CRI-O 作为运行时,这是社区的明确方向。
  2. 根据环境复杂度选择:如果你的技术栈纯粹围绕 Kubernetes,且团队偏好 Red Hat 系生态,CRI-O 的简洁和低开销很有吸引力。如果你的环境需要更多灵活性,或者未来有不确定性,containerd 的通用性和强大生态是更稳妥的选择。
  3. 理解运维工具链的变化:切换运行时意味着运维习惯和排查工具需要更新。提前让团队熟悉 crictlctr 命令以及对应运行时的日志位置和配置方式。

最终,无论选择哪一个,清晰的职责划分和标准的接口(CRI, OCI)才是保障容器基础设施长期稳定和可维护的关键。它们各司其职,又通过标准协议紧密协作,共同构成了现代云原生应用的坚实基石。

原创文章,作者:,如若转载,请注明出处:https://fudengji.cn/article/127/

(0)
上一篇 2026年7月31日 上午12:15
下一篇 2026年7月31日

相关推荐