containerd

Posted by OwlDawn on Sunday, May 26, 2024

Containerd

Containerd 是一款容器运行时,其功能是高度模块化的,启动后,主进程会在 grpc server 下注册一系列的 grpc service,如 CRIService、TasksService 等

Containerd 的具体功能基本都是通过调用这些 gRPC service 完成的,启动后会启动一个 gRPC server,默认监听位置为 /run/containerd/containerd.sock

CRI Service

kubelet 通过 CRI 调用 containerd 来创建 sandbox(pod)、container 等。这里的 CRI 指 containerd 的 CRIService 暴露出来的 gRPC 接口

Runtime

containerd 不负责创建或删除容器等操作,这些操作交给下层的 runtime 来做

创建容器的顺序为:

  1. containerd daemo 接收来自 client 的创建 container 的请求(可能是 kubelet 通过 cri 发起的,或者 ctr、nerdctl 这些 client 发起的)
  2. containerd daemon 准备容器所需的 rootfs 和 container 配置信息(OCI runtime config)
  3. containerd daemon 将 rootfs、配置信息发送给 runtime
  4. runtime 接收来自 containerd daemon 的创建容器的请求。runtime 执行系统调用,创建容器进程,并启动一个守护进程(containerd shim),暴露并监听一个 unix socket,同时将这个 socket 信息写到 stdout
  5. containerd daemon 从 stdout 中读到这个 unix socket 信息,并在后续通过这个 socket 与 runtime 交流,以获取 container 中的状态信息等

在具体实现中 containerd 的 runtime 分为两个部分

  • runtime shim
    • shim 是与 containerd daemon 直接沟通的结构。是一个二进制文件,在 v1.7 版本中为 containerd-shim-runc-v2,containerd daemon 会直接调用这个二进制文件,创建一个 containerd shim 进程
  • runtime engine
    • 即我们常说的狭义上的容器运行时,如 runc。以二进制形式存在,runtime engine 是真正负责调用操作系统接口以创建、启动和删除容器的模块。它只会和 runtime shim 通信

未命名文件 (1)

创建 pod 流程

RunPodSandbox

  1. kubelet 通过内置的 CRI client 调用 containerd 的 cri service 的 RunPodSandbox(gRPC call)
  2. cri service 根据 gRPC Request(RunPodSandboxRequest)中描述创建 SandboxInfo,存储到本地的 metadata 数据库中(blot 数据库,一个本地键值对存储)
  3. cri service 创建 network namespace,并调用 CNI 插件初始化 container network
  4. cri service 启动 sandbox,主要把 pause container 跑起来。这里启动 pause container 的步骤与后续启动业务 container 的步骤基本相同

CreateContainer

准备 container 的运行环境,并不会在系统中真正创建 container

  1. kubelet 通过其内置的 CRI Client 调用 containerd 的 cri service 的 CreateContainer(gRPC call)
  2. cri service 从 gRPC Request (CreateContainerRequest)的描述中,得到 sandbox id,并据此从 metadata 数据库中找到之前创建的 sandbox 对象
  3. cri service 根据 CreateContainerRequest 中的描述,创建并配置 Container 对象。其中重要的配置步骤包括
    1. 根据 snapshotter 的配置,对镜像 unpack 以构建 container 的 rootfs
    2. 生成 oci runtime spec
  4. cri service 将 Container 对象写入到 metadata 数据库中

StartContainer

  1. kubelet 通过其内置的 CRI client 调用 containerd 的 cri service 的 startContainer
  2. cri service 从 gRPC Request(StartContainerRequest)的描述,得到 sandbox id,并据此从 metadata 数据库中找到之前创建的 sandbox 对象
  3. cri service 从 metadata 数据库中找到之前创建的 container 对象
  4. cri service 创建 Task 对象。一个运行态的 container 在 containerd 的内存中是使用 Task 对象来描述的。所以。创建实际运行的 container 的过程,其实就是创建并启动 Task 的过程。具体过程:
    1. cri service 会通过 gRPC call 调用 TaskService(也是一个监听 /run/containerd/containerd.sock 的 gRPC service)尝试创建 Task 对象
    2. TaskService 调用 PlatformRuntime(PlatformRuntime 是个接口,具体的实现是 TaskManager)创建 runtime.Task
    3. TaskManager 尝试启动一个 containerd shim 进程
      1. 如果当前要创建的 container(Task)属于某个已创建的 Sandbox(比如,创建业务 container 时,本质上是向之前已经创建了 pause container 的 sandbox 中添加 container,此时,已经存在当时创建 pause container 时创建的 shim 进程),那么不创建新的 shim 进程(即,同一个 Pod sandbox 下的 container 会复用同一个 shim 进程)
      2. 否则,执行 containerd-shim-runc-v2 这个二进制,启动一个 shim 进程,该 shim 进程会启动一个 ttrpc service,并把其所监听的 unix socket 地址返回给 TaskManager
    4. TaskManager 通过 ttrpc 接口调用 shim 进程
    5. shim 进程调用 runc(或者其他的 OCI runtime engine)以创建容器进程

Snapshotter

snapshotter