containerd 是什么?一文带你认识容器运行时 – wiki基地


一文带你认识容器运行时:深度剖析 Containerd

在当今的云计算和微服务时代,容器技术已成为事实标准。无论是开发者构建应用,还是运维人员部署和管理系统,都离不开容器的身影。Docker、Kubernetes 等名字家喻户晓,它们让容器的使用变得前所未有的便捷。然而,在这繁荣的容器生态背后,隐藏着一个至关重要的、但可能对许多人来说相对陌生的核心组件——容器运行时(Container Runtime)。

容器运行时,顾名思义,就是负责真正“运行”容器的软件。它扮演着引擎的角色,接收上层指令,然后创建、启动、停止以及管理容器的生命周期。在众多的容器运行时中,Containerd 凭借其稳定性、高效性以及在 Docker 和 Kubernetes 生态中的核心地位,脱颖而出,成为了事实上的标准容器运行时之一。

本文将带你深入了解 Containerd:它是什么,从何而来,为何如此重要,以及它在容器世界中扮演着怎样的角色。

第一部分:容器世界的基石——什么是容器运行时?

在深入了解 Containerd 之前,我们首先需要理解“容器运行时”这个概念。

简单来说,容器运行时是容器技术栈中最底层的组件之一。它负责与操作系统内核(如 Linux 的 Namespace, Cgroup, Seccomp 等隔离技术)交互,创建和管理容器进程。你可以把容器运行时想象成一个虚拟机监控器(Hypervisor)对于虚拟机的作用——它是实际执行和管理隔离环境的软件。

为什么需要不同的容器运行时?

早期容器技术发展相对分散,不同的工具(如 Docker、CoreOS rkt)可能有自己的容器格式和运行时实现。这导致了生态系统的碎片化,难以实现互操作性。为了解决这个问题,开放容器倡议(Open Container Initiative, OCI)应运而生。

OCI 旨在制定一套标准的容器镜像格式和容器运行时规范,以便任何符合这些规范的镜像可以在任何符合这些规范的运行时上运行。OCI 规范主要包括两个部分:

  1. OCI Runtime Specification (runtime-spec): 定义了如何解压OCI镜像并在特定配置下运行容器。Runc 是目前最流行的 OCI runtime-spec 的参考实现。
  2. OCI Image Specification (image-spec): 定义了OCI容器镜像的格式和文件结构。

OCI 规范的出现,将容器运行时分为了不同的层次:

  • 低层运行时 (Low-level Runtime): 负责与操作系统内核直接交互,使用 Namespace、Cgroup 等技术创建隔离的容器进程。它们通常接收一个 OCI 配置捆绑包(bundle,包含 config.json 和 rootfs),然后运行容器进程。runc 就是典型的低层运行时。其他低层运行时包括 crunkata-containers(提供更强的硬件隔离)。
  • 高层运行时 (High-level Runtime): 在低层运行时的基础上,增加了镜像管理、API Server、容器生命周期管理、网络、存储卷挂载等更丰富的功能。它们通常提供更易用的接口供上层工具(如 Docker、Kubernetes)调用。Containerd 和 CRI-O 就是典型的高层运行时。

Containerd 正是属于高层容器运行时

第二部分:Containerd 的诞生与演进

Containerd 的故事,与 Docker 紧密相连。

起源于 Docker

最初,Docker 的架构是相对单体的。强大的 docker daemon 几乎负责了容器相关的所有事情:镜像构建、镜像管理、容器创建、运行、网络、存储等等。这在早期极大地简化了容器的使用,但也带来了一些问题:

  1. 单体复杂性: docker daemon 过于庞大和复杂,难以维护和扩展。
  2. 紧耦合: 其他工具(如 Kubernetes)如果想利用 Docker 的核心能力来运行容器,必须通过 docker daemon 提供的较高层 API 或一个叫做 dockershim 的适配器。这增加了额外的依赖和复杂性。
  3. 稳定性: docker daemon 的崩溃可能会影响所有运行中的容器(尽管有重启策略)。

为了解决这些问题,Docker 公司决定将 docker daemon 中的核心容器管理功能剥离出来,形成一个独立的组件。这个组件就是 Containerd。

走向独立与开源

2016 年底,Docker 公司正式宣布将 Containerd 开源,并于 2017 年 3 月将其捐赠给云原生计算基金会(CNCF)。这标志着 Containerd 走向了独立发展的道路,并成为了一个社区驱动的项目。

将 Containerd 从 Docker 中剥离并捐赠给 CNCF 具有重要的意义:

  • 标准化: 它为容器生态系统提供了一个标准的、模块化的容器运行时核心。
  • 解耦: 使得 Docker 客户端、Docker Daemon 和实际的容器执行引擎(Containerd + runc)之间实现了清晰的分离。
  • 生态整合: 使得 Kubernetes 等其他容器编排平台可以直接与 Containerd 集成,而无需依赖完整的 Docker Daemon,提升了效率和稳定性。

Containerd 在 CNCF 中持续发展,并于 2019 年 2 月正式从孵化项目升级为毕业项目(Graduated Project)。毕业项目代表着该项目已经成熟、稳定、具有广泛的社区采纳度,并且有明确的治理模式。这进一步巩固了 Containerd 在云原生领域的基石地位。

第三部分:揭秘 Containerd——它究竟是什么?

那么, Containerd 究竟是什么?

定义: Containerd 是一个工业级、标准化的容器运行时,强调简洁、健壮和可移植性。它管理着容器的完整生命周期,包括:

  • 镜像的传输和存储(Image Transfer and Storage)
  • 容器的执行和监控(Container Execution and Supervision)
  • 低层存储的管理(Low-level Storage)
  • 网络接口的挂载和配置(Network Attachment and Configuration)

它设计用于嵌入到更高级别的系统(如 Docker Engine、Kubernetes Kubelet)中,而不是直接由终端用户操作。虽然它提供了命令行客户端 ctr,但这更多是用于调试和低级别交互,而不是日常用户界面。

Containerd 不是一个完整的容器平台(它不包含镜像构建工具、Swarm 编排、用户界面等),也不是一个低层运行时(它不直接与内核交互创建进程),它是一个介于两者之间的高层运行时核心

核心特点:

  • 专注于核心功能: Containerd 只做容器运行时的本职工作,不包含其他高级功能,这使得它更加轻量和高效。
  • OCI 兼容: 完全实现了 OCI 规范,能够运行符合 OCI 镜像规范的镜像。
  • gRPC API: 提供稳定、高性能的 gRPC API 接口,方便其他系统进行集成。
  • 模块化与插件化: 内部设计高度模块化,许多功能通过插件实现,易于扩展和定制(例如,支持不同的 Snapshotter、Content Store、Runtime 等)。
  • daemonless 设计: 通过使用 containerd-shim 进程,实现了真正的“守护进程无关”容器。这意味着即使 Containerd 主进程崩溃或重启,正在运行的容器也不会受到影响,提高了系统的稳定性。
  • 针对 Kubernetes 优化: 提供了一个专门的 CRI (Container Runtime Interface) 插件,使得 Kubernetes 可以通过标准的 CRI 接口高效地与 Containerd 集成。

第四部分:Containerd 的核心功能与职责

Containerd 作为高层运行时,负责协调和管理多个底层组件,以实现容器的完整生命周期。其主要职责包括:

  1. 镜像管理 (Image Management):

    • 负责从镜像仓库(如 Docker Hub, Quay.io 等)拉取(pull)和推送(push)镜像。
    • 管理本地的镜像存储。Containerd 使用一种内容地址可寻址(content-addressable)的存储方式,每个镜像层都根据其内容的哈希值进行标识和存储。这有助于 deduplication(去除重复)和提高存储效率。
    • 管理镜像的元数据(layers, config等)。
    • 支持 OCI Image Spec。
  2. 内容存储 (Content Store):

    • 这是镜像管理的一部分,专门负责存储镜像的原始数据,即各个镜像层的文件系统内容。它通过内容哈希来索引数据。
  3. 快照管理 (Snapshot Management):

    • 负责管理容器的根文件系统。容器的根文件系统是基于其镜像层构建的,通常使用写时复制(Copy-on-Write, CoW)技术。
    • Containerd 支持多种 Snapshotter 插件,如 OverlayFS, Btrfs, ZFS 等,这些插件负责具体的文件系统操作,如创建、删除快照,以及将镜像层组合成容器可写的根文件系统。
  4. 容器执行与生命周期管理 (Container Execution & Lifecycle):

    • 接收来自上层的请求(如创建、启动、停止、删除容器)。
    • 根据 OCI Runtime Specification 生成容器的配置文件(config.json)和文件系统根目录(rootfs bundle)。
    • 调用配置的低层运行时(默认为 runc)来实际创建和启动容器进程。
    • 监控容器进程的状态。
  5. 运行时管理 (Runtime Management):

    • Containerd 本身不直接运行容器进程,它负责调用配置的低层运行时。
    • 它知道如何与不同的低层运行时交互,并管理它们的生命周期(通过 containerd-shim)。
    • 支持配置使用不同的 OCI 兼容运行时,比如 runc 或提供更强隔离的 kata-containers
  6. 容器监控与标准 I/O (Supervision & Standard I/O):

    • 通过 containerd-shim 进程,Containerd 能够监控容器主进程的运行状态,捕获其退出事件和状态码。
    • 管理容器的标准输入、输出和错误流(stdin, stdout, stderr),并将它们连接到合适的位置(例如,日志文件或客户端)。
  7. 网络与存储卷管理 (Networking & Volume Management):

    • 尽管 Containerd 提供了相关的 API 接口,但它本身并不直接实现复杂的网络配置(如创建虚拟网卡、配置路由、DNS 等)或高级存储卷管理(如挂载远程存储)。
    • 它通常将这些职责委托给外部工具或规范:
      • 网络: 通常与 CNI (Container Network Interface) 插件协同工作。Containerd 负责在容器创建时调用 CNI 插件来配置容器的网络接口,在容器删除时调用插件清理网络资源。
      • 存储卷: Containerd 负责在容器创建时按照配置将宿主机的路径或通过其他方式(如 CSI, Container Storage Interface,虽然 CSI 主要是 K8s 层面的抽象,但运行时需要支持其底层的卷挂载)准备好的存储卷挂载到容器指定的路径。

总结来说,Containerd 就像一个高效的协调者和管家,负责将容器运行所需的一切准备就绪(镜像、文件系统、配置),然后指挥低层运行时真正“启动”容器,并在其生命周期内进行管理。

第五部分:Containerd 的内部工作原理与架构

为了更好地理解 Containerd 如何工作,我们来看一下它的核心架构和组件之间的交互。

Containerd 主要由以下部分组成:

  1. Containerd Daemon (containerd): 这是核心的服务进程,作为守护进程运行在宿主机上。它提供了 gRPC API 接口供客户端调用,并负责管理内部的各种服务和插件。
  2. gRPC API: Containerd Daemon 暴露的主要接口。所有上层工具(Docker Engine, Kubelet)都通过这个 API 与 Containerd 进行通信。这个 API 是跨进程的,通常通过 Unix domain socket 进行通信。
  3. 客户端库 (Client Library): Containerd 提供了 Go 语言的客户端库,方便其他 Go 程序通过 gRPC API 与 Containerd Daemon 交互。
  4. 命令行工具 (ctr): Containerd 自带的低级别命令行客户端,可以直接通过 gRPC API 与 Containerd Daemon 交互,用于执行拉取镜像、创建/启动容器等操作。这主要是用于调试和测试。
  5. 插件系统 (Plugin System): Containerd 内部高度插件化,不同的功能模块以插件形式加载。这包括:
    • CRI Plugin: 这是 Kubernetes 与 Containerd 集成的关键。它实现了 Kubernetes 定义的 CRI 接口,接收来自 Kubelet 的 CRI 调用请求,并将其转换为对 Containerd gRPC API 的调用。
    • Snapshotter Plugins: 管理容器文件系统快照(如 OverlayFS, Btrfs, ZFS)。
    • Content Store Plugin: 管理镜像内容。
    • Runtime Plugin: 管理与低层运行时的交互(如 OCI 运行时)。
    • 其他插件:如 Event Monitor, Metadata Store 等。
  6. Containerd-shim: 这是一个非常关键的组件,它是 Containerd 实现“daemonless”容器设计的核心。
    • 当 Containerd Daemon 接收到创建并运行容器的请求后,它不会直接调用低层运行时(如 runc)来启动容器进程。
    • 相反,Containerd Daemon 会先启动一个 containerd-shim 进程。
    • 然后,Containerd Daemon 会通过 gRPC 调用,告知 containerd-shim 进程去调用配置的低层运行时(如 runc),并传递容器的 OCI 配置 bundle。
    • containerd-shim 进程调用低层运行时(如 runc),runc 会创建并启动容器进程。
    • containerd-shim 进程成为了容器主进程的父进程,而不是 Containerd Daemon。
    • containerd-shim 进程负责:
      • 保持容器进程的父进程存在,防止容器进程变成孤儿进程被 init 进程收养。
      • 收集容器的标准输入输出(stdio),并将其转发到指定位置(例如日志文件或网络)。
      • 监控容器进程的退出状态,并在容器退出时向 Containerd Daemon 报告。
    • 这种设计的好处是,即使 Containerd Daemon 崩溃或需要升级重启,containerd-shim 进程和它所管理的容器进程仍然可以继续运行,提高了系统的健壮性。containerd-shim 通常以 containerd-shim-${RUNTIME_NAME}-v${VERSION} 的形式命名,例如 containerd-shim-runc-v2
  7. 低层运行时 (Low-level Runtime – e.g., runc): 这是实际负责创建容器进程的工具。Containerd 调用 runc (或其他配置的运行时),传递 OCI bundle,runc 利用 Linux 内核的 Namespace, Cgroup 等技术创建隔离的容器进程,并在容器的文件系统中执行入口命令。runc 在启动容器进程后会退出,将控制权和进程的父子关系交给 containerd-shim

工作流程示例 (创建一个容器):

  1. 高层工具 (e.g., Docker CLI, Kubelet): 发送创建/运行容器的请求。
  2. 中介层 (e.g., Docker Daemon, Kubelet’s CRI Plugin): 接收请求,进行一些预处理(如认证、授权、调度等)。
  3. 调用 Containerd API: 中介层通过 gRPC API 调用 Containerd Daemon 的相应服务(如 Containers service, Tasks service)。
  4. Containerd Daemon 处理:
    • 检查本地是否有请求的镜像。如果没有,调用 Image service 和 Content service 从仓库拉取镜像。
    • 调用 Snapshot service,根据镜像层和容器的读写层创建一个容器的根文件系统快照。
    • 生成 OCI Runtime Specification 格式的 config.json 文件,配置容器的进程参数、环境变量、挂载点、安全选项、资源限制(cgroups)、命名空间等。
    • 创建一个容器的 OCI bundle 目录,包含 config.json 和指向根文件系统的 rootfs 符号链接。
    • 调用 Runtime service,指定使用哪个低层运行时(默认为 runc),并启动一个 containerd-shim 进程。
  5. Containerd-shim 启动: containerd-shim 进程被启动。Containerd Daemon 通过 gRPC 与 Shim 建立通信。
  6. Shim 调用低层运行时: containerd-shim 通过执行命令(如 runc create <container_id> <bundle_path>)调用低层运行时 runc,并传递 OCI bundle 的路径。
  7. 低层运行时创建容器: runc 读取 config.json,利用 Linux 内核功能(Namespace, Cgroup, Seccomp 等)创建新的隔离环境,并在容器文件系统中启动容器主进程。runc create 阶段只是创建了容器进程但并未启动其主命令。接着 Shim 会调用 runc start <container_id> 来启动容器的主命令。runc 完成其任务(创建并启动进程)后退出。
  8. Shim 监控: containerd-shim 现在是容器主进程的父进程,它开始监控容器进程的 stdout/stderr,并将它们转发到配置的位置,同时监听容器进程的退出事件。
  9. 状态报告: containerd-shim 通过 gRPC 或其他 IPC 机制向 Containerd Daemon 报告容器的状态变化(如 Running, Exited)。
  10. Containerd Daemon 更新状态: Containerd Daemon 更新其内部对该容器状态的记录,并通过 gRPC API 向更高层工具报告。

这个流程展示了 Containerd 如何协调 Shim 和低层运行时来管理容器,同时也解释了其模块化和健壮性的设计理念。

第六部分:Containerd 在容器生态系统中的地位

Containerd 在现代容器生态系统中扮演着核心的、承上启下的角色。

与 Docker 的关系:

如前所述,Containerd 诞生于 Docker,并且是现代 Docker Engine 的核心组件。对于终端用户来说,他们依然使用 docker CLI 与 docker daemon 交互。但是,当执行 docker rundocker pull 等命令时,docker daemon 实际上是将这些高级操作转换为对后台运行的 Containerd Daemon 的 gRPC 调用

Docker Engine 提供了用户友好的 CLI、镜像构建 (docker build)、Swarm 编排、更高级的网络和存储卷管理等功能,而将核心的镜像管理、容器执行等“脏活累活”交给了 Containerd。所以,你可以理解为 Docker Engine 现在是构建在 Containerd 之上的一个更完整的容器平台。

与 Kubernetes 的关系:

Containerd 在 Kubernetes 中扮演着越来越重要的角色。Kubernetes 通过其容器运行时接口 (Container Runtime Interface, CRI) 与不同的容器运行时进行通信。 CRI 是一组 gRPC API,定义了 Kubelet(Kubernetes 的节点代理)与容器运行时交互的标准方式,例如创建 Pod sandbox、创建/启动/停止/删除容器、拉取镜像等。

早期,Kubernetes 通过一个内置的 dockershim 组件来与 Docker Daemon 交互,而 Docker Daemon 内部再调用 Containerd。这个 dockershim 增加了额外的开销和维护负担。

随着 Containerd 的成熟和其 CRI 插件的完善,Kubernetes 社区决定移除内置的 dockershim。现在,Containerd 直接通过其内置的 CRI 插件实现了标准的 CRI 接口。Kubelet 可以配置为直接通过 CRI 与 Containerd Daemon 进行通信。

这带来了显著的优势:

  • 简化架构: 移除了中间层,减少了组件数量。
  • 提高效率: Kubelet 直接调用 Containerd 的 CRI API,通信路径更短,性能更好。
  • 稳定性: 减少了潜在的故障点。
  • 拥抱标准: 直接对接 CRI 标准,符合 Kubernetes 的发展方向。

因此,在现代 Kubernetes 集群中,Containerd 已经成为首选的容器运行时之一,与 CRI-O 等其他 CRI 兼容运行时共同支撑着 Kubernetes 的底层容器管理。

与 CRI-O 的对比:

CRI-O 是另一个流行的、专注于 Kubernetes CRI 的高层容器运行时。与 Containerd 不同,CRI-O 的唯一目标就是实现 CRI 接口,为 Kubernetes 提供容器运行时。它不提供像 Docker 那样的用户界面或独立的镜像构建能力。

Containerd 虽然也提供了 CRI 插件,但它的设计目标更广泛,可以作为一个独立的容器运行时核心,被多种上层系统集成(包括 Docker 和 Kubernetes)。

两者都是优秀的 CRI 实现,具体选择哪一个可能取决于特定的需求、偏好以及所使用的其他组件。但在 Kubernetes 生态中,Containerd 由于其广泛的采用和成熟度,通常是一个默认或常见的选择。

与 Runc 的关系:

正如前面提到的,Containerd 不直接与内核交互运行容器,它依赖低层运行时。Runc 是 OCI Runtime Specification 的参考实现,也是 Containerd 默认和最常用的低层运行时。Containerd 通过调用 Runc 的命令行接口(或者通过 gRPC API,取决于具体实现)来创建、启动容器进程。它们是上下层的协作关系。Containerd 提供高层管理能力,Runc 提供低层执行能力。

第七部分:为何选择 Containerd?优势与考量

选择 Containerd 作为容器运行时,尤其是在 Kubernetes 环境下,有很多理由:

优势:

  1. 标准化和 OCI 兼容: 完全遵循 OCI 规范,确保容器镜像和运行环境的互操作性。
  2. 与 Kubernetes 的原生集成: 提供成熟且高效的 CRI 插件,是 Kubernetes 官方支持和推荐的运行时之一,简化了 Kubelet 的配置和维护。
  3. 高性能和低资源占用: 专注于核心功能,架构轻量,运行时开销低,对系统资源占用少。
  4. 高可靠性和稳定性: 得益于其模块化设计和 containerd-shim 的 daemonless 特性,即使运行时主进程出现问题,运行中的容器也能保持不受影响。
  5. 良好的社区支持和成熟度: 作为 CNCF 的毕业项目,拥有活跃的社区和广泛的生产环境应用,稳定可靠。
  6. 模块化和可扩展性: 插件系统允许根据需要替换或添加不同的组件(如 Snapshotter、Runtime)。
  7. 广泛的生态系统支持: 不仅是 Kubernetes 的核心组件,也是现代 Docker Engine 的基石。

考量/潜在不足:

  1. 低级别接口: Containerd 的原生 API 和 ctr 命令行工具相对低层,不提供像 Docker CLI 那样丰富的用户体验,不适合终端用户的日常操作(需要通过 Docker 或 Kubernetes 等上层平台)。
  2. 需要与其他组件配合: Containerd 本身不提供完整的网络和存储卷解决方案,通常需要与 CNI 插件、CSI 驱动或其他卷管理工具协同工作。
  3. 调试复杂度: 在遇到问题时,可能需要理解 Containerd、containerd-shim、runc 以及 OCI 规范之间的交互,调试路径可能比单体结构更长。

总的来说,对于需要一个稳定、高效、标准化并且能够很好地与 Kubernetes 集成的底层容器运行引擎的场景,Containerd 是一个非常优秀的选择。

第八部分:如何使用 Containerd

虽然 Containerd 主要作为嵌入式组件使用,但了解如何与其交互有助于理解和调试。

  1. 作为 Docker Engine 的一部分: 这是最常见的用户场景。当你安装并运行现代 Docker 时,Containerd 已经在后台运行。你无需直接与其交互,所有的 docker 命令都会通过 Docker Daemon 调用 Containerd。
  2. 作为 Kubernetes 的容器运行时: 在部署 Kubernetes 集群时,你可以配置 Kubelet 使用 Containerd 作为其 CRI 运行时。这通常通过在 Kubelet 配置中指定 Containerd 的 gRPC endpoint 来实现。大多数 Kubernetes 发行版(如 Kubeadm, RKE, AKS, GKE, EKS 等)都支持或默认使用 Containerd。
  3. ** standalone 模式 (使用 ctr):** 你也可以单独安装和运行 Containerd Daemon。然后使用 ctr 命令行工具直接与其交互。例如:
    • ctr images pull docker.io/library/nginx:latest:拉取 Nginx 镜像。
    • ctr images list:列出本地镜像。
    • ctr containers create nginx-test docker.io/library/nginx:latest:创建一个容器(但尚未运行)。
    • ctr tasks start nginx-test:启动已创建的容器。
    • ctr tasks list:列出运行中的容器任务。
    • ctr tasks kill nginx-test:停止容器。
    • ctr containers delete nginx-test:删除容器。

使用 ctr 命令有助于开发者和运维人员深入了解 Containerd 的内部工作,进行低层调试和测试。但请注意,ctr 提供的接口比 docker CLI 更为底层和精细。

第九部分:总结与展望

Containerd 是现代容器生态系统中一个不可或缺的核心组件。它从 Docker 中剥离出来,遵循 OCI 标准,并通过其健壮、高效和模块化的设计,成功地成为了 Kubernetes 的首选运行时之一。

它扮演着高层运行时的角色,向上为 Docker、Kubernetes 等平台提供标准的容器管理接口,向下则协调低层运行时(如 runc)和操作系统内核来真正执行容器。它的 containerd-shim 设计解决了 daemonless 容器的难题,显著提升了系统的稳定性。

虽然对于普通用户来说,Containerd 可能是一个“隐形”的存在,但理解它在容器技术栈中的位置和作用,对于深入理解容器的工作原理、排查问题以及进行更高级的容器管理至关重要。

随着云原生技术的不断发展,Containerd 作为 CNCF 的毕业项目,将继续在标准化、性能优化和生态系统集成方面发挥关键作用,为容器技术的普及和发展奠定坚实的基础。它不仅仅是一个容器运行时,更是连接容器世界各个层面的重要桥梁。

通过本文,希望你对 Containerd 有了更全面和深入的认识,能够更好地理解它在容器技术栈中的重要性,并对其核心原理和工作方式有了清晰的认知。容器的世界是多层且复杂的,而 Containerd 正是其中那个低调却不可或缺的基石。


发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

滚动至顶部