Runtime v2 为运行时作者集成 containerd 引入了一级 shim API。

containerd 作为守护进程,并不直接启动容器。相反,它充当更高级别的管理器
或枢纽的作用,以协调容器和内容的活动。被称作 “运行时”的程序真正来启动、停止和管理容器、无论是单个容器还是容器组(如 Kubernetes pods)。

例如,containerd 会检索容器镜像配置及其作为层的内容,使用快照器将其放置在磁盘上,设置容器的 rootfs 和配置,然后启动一个运行时来创建/启动/停止容器。

本文档介绍了 v2 运行时集成模型的主要组件,以及这些组件如何与 containerd 和 v2 运行时交互。以及如何使用和集成不同的 v2 运行时。

为了简化交互,v2 版运行时引入了第一类 v2 API,供运行时作者与 containerd 集成、取代了 v1 API。
v2 API 是最小的,其作用域仅限于容器的执行生命周期。

本文档分为以下几个部分:

  • 架构](#architecture) – 主要组件、它们的用途和关系
  • 用法](#usage) – 如何调用特定运行时,以及如何配置它们
  • 编写](#shim-authoring) – 如何编写 v2 运行时

架构

containerd与运行时通信

containerd 希望运行时能实现几种容器控制功能,如创建、启动和停止。

高级流程如下:

客户端请求 containerd 创建一个容器
containerd 布局容器的文件系统,并创建必要的配置信息
containerd 通过 API 调用运行时来创建/启动/停止容器

不过,containerd 本身实际上并不直接调用运行时来启动容器。相反,它希望调用运行时,运行时将暴露一个套接字(在类 Unix 系统上是 Unix Domain,在Windows 系统上是命名为管道的套接字),并通过 ttRPC 在该套接字上监听容器命令。

运行时将处理这些操作。至于如何处理,则完全取决于运行时的实现。
两种常见的模式是

  • 运行时使用一个二进制文件,既监听套接字,又创建/启动/停止容器
  • 一个单独的临时二进制文件,它监听套接字,并调用一个单独的运行时引擎来创建/启动/停止容器

现在采用的是分离的 “shim+engine “模式,这样可以更容易地集成执行特定运行时引擎规范(如OCI 运行时规范的不同运行时。

ttRPC协议可通过一个运行时垫片(shim)来处理,和不同的运行时引擎实现无关。只要它们实现的是 OCI 运行时规范。

最常用的运行时引擎是runc,它实现了OCI运行时规范。由于这是一个运行时引擎,它并不直接被containerd调用而是由 shim 来调用,shim 监听套接字并调用运行时引擎。

shim+engine 架构
运行时 shim

运行时 shim 实际上是由 containerd 调用的。除了提供 containerd 的通信端口和一些配置信息之外,它在启动时的选项极少。

运行时 shim 在套接字上监听来自 containerd 的 ttRPC 命令,然后调用一个单独的运行时引擎程序、通过 fork/exec 运行容器。例如,io.containerd.runc.v2 shim 会调用运行时引擎,如 runc

containerd 通过 ttRPC 连接向 shim 传递选项,其中可能包括要调用的运行时引擎二进制文件。这些是 [CreateTaskRequest](#container-level-shim-configuration)的选项

例如,io.containerd.runc.v2 shim 支持包含运行时引擎二进制文件的路径。

运行时引擎

运行时引擎本身是实际启动和停止容器的工具。

例如,在 runc 的情况下,containerd 项目提供的 shim作为可执行文件 “containerd-shim-runc-v2 “提供。它由 containerd 调用并启动 ttRPC 监听器。

然后,该shim调用实际的 runc 二进制文件,将容器配置传递给它,而 runc 二进制文件则通常通过 libcontainer->system apis 创建/启动/停止容器。

shim+engine 关系

由于每个 shim 实例都作为守护进程与 containerd 通信,同时通过调用独立的运行时来孕育容器、可以用一个 shim 来管理多个容器和调用。例如一个 containerd-shim-runc-v2 与一个 containerd 通信,它可以调用十个不同的容器。

甚至还可以为多个容器设置一个 shim,每个容器都有自己的实际运行时、因为如上所述,运行时二进制文件是作为 CreateTaskRequest 中的选项之一传递的。

containerd 不知道也不关心 shim 与容器的关系是一对一还是一对多。这完全由 shim 决定。例如,io.containerd.runc.v2 shim会根据是否存在标签分组。在实践中,这意味着由 Kubernetes 启动的、属于同一个 Kubernetes pod 的容器,将由单个shim处理,并根据 CRI 插件设置的 “io.kubernetes.cri.sandbox-id “标签分组。

流程如下

  1. containerd 接收到创建容器的请求
  2. containerd 布局容器的文件系统,并创建必要的 container config 信息
  3. containerd 调用 shim,包括容器配置,它使用这些信息来决定是启动一个新的套接字监听器(1:1 shim 到容器),还是使用一个现有的套接字监听器(1:多个)
    • 如果是已经存在,则返回现有套接字的地址并退出
    • 如果是新的套接字监听器,shim将
      1. 创建一个新进程,通过套接字监听containerd发出的 ttRPC 命令
      2. 将该套接字的地址返回给containerd并退出,向shim发送启动容器的命令
  4. shim 调用 runc 来创建/启动/停止容器

本文档后面的 流程中提供了一个很好的流程图。

使用方法

调用运行时

创建容器时,可以选择运行时-单实例或 shim+engine 运行时-及其选项。
containerd服务(containerd客户端、CRI API…),或通过调用containerd提供服务的客户端来户端的例子包括 ctrnerdctl、kubernetes、docker/moby、rancher 等。

运行时也可以通过容器更新来更改。

传递的运行时名称是一个字符串,用于向 containerd 标识运行时。如果是单独的 shim+engine,那么这个字符串就是运行时 shim。无论如何,这都是 containerd 执行并期望启动 ttRPC 监听器的二进制文件。
运行时名称可以是类似 URI 的字符串,或者从 containerd 1.6.0 开始是可执行文件的实际路径。

  1. 如果运行时名称是一个路径,则使用它作为要调用的运行时的实际路径。
  2. 如果运行时名称类似 URI,则按以下逻辑将其转换为运行时名称。

如果运行时名称是 URI-like,containerd 将使用下面的逻辑把传递的运行时从 URI-like名称转换为二进制名称:

  1. - 替换所有 .
  2. 取最后 2 个成分,例如 runc.v2 .
  3. containerd-shim 为前缀

例如,如果运行时名称是 io.containerd.runc.v2, containerd 将以 containerd-shim-runc-v2 的形式调用 shim。并期望能在正常的PATH路径上找到这个名称的二进制文件。

containerd 保留了 containerd-shim-* 前缀,这样用户就可以 ps aux | grep containerd-shim查看系统中正在运行的 shim。

例如

$ ctr --runtime io.containerd.runc.v2 run --rm docker.io/library/alpine:latest alpine

将调用 containerd-shim-runc-v2

您可以尝试使用其他名称来测试:

$ ctr run --runtime=io.foo.bar.runc2.v2.baz --rm docker.io/library/hello-world:latest hello-world /helloctr: failed to start shim: failed to resolve runtime path: runtime "io.foo.bar.runc2.v2.baz" binary not installed "containerd-shim-v2-baz": file does not exist: unknown

它接收到 io.foo.bar.runc2.v2.baz 并查找 containerd-shim-v2-baz

你还可以通过传递 --runc-binary 选项,覆盖为 shim 配置的默认运行时选项。例如”

ctr --runtime io.containerd.runc.v2 --runc-binary /usr/local/bin/runc-custom run --rm docker.io/library/alpine:latest alpine

配置运行时

您可以在 containerd 的 config.toml 配置文件中配置一个或多个运行时,方法是修改下面的部分:

[plugins."io.containerd.grpc.v1.cri".containerd.runtimes]

更多详情和示例请参阅 config.toml man page。

配置文件中的这些 “命名运行时 “仅在通过 CRI 调用时使用。
它有一个runtime_handler字段。

shim 作者

本节专为希望创建 Shim 的运行时作者而设。
它将详细介绍 API 的工作原理以及构建 shim 时的不同注意事项。

命令

容器信息通过两种方式提供给 shim。
OCI Runtime Bundle和 Create rpc 请求。

start 启动

每个 shim 必须实现一个 start 子命令。
该命令将启动新的 shim。
启动命令必须接受以下标志:

  • -namespace 容器的命名空间
  • -address containerd 的主 grpc socket 地址
  • 向容器 ID 发布事件的二进制路径
  • -id 容器的 ID

启动命令以及对 shim 的所有二进制调用都将容器的 bundle 设置为 cwd

start 命令可能有以下特定于 containerd 的环境变量设置:

  • containerd 的 ttrpc API 套接字地址
  • GRPC_ADDRESS containerd 的 grpc API 套接字(1.7 及以上版本)的地址
  • MAX_SHIM_VERSION 客户端支持的最大 shim 版本,总是 2 表示 shim v2 (1.7+)
  • SCHED_CORE 启用内核调度(如果可用) (1.6+)
  • NAMESPACE 一个可选的命名空间,Shim 在其中运行或继承该命名空间 (1.7+)

启动命令必须向 stdout 写入 shim 为其 API 服务的 ttrpc 地址,或者 (实验性的)
格式的 JSON 结构(其中协议可以是 “ttrpc “或 “grpc”):

{"version": 2,"address": "/address/of/task/service","protocol": "grpc"}

该地址将被 containerd 用来发出容器操作的 API 请求。

start 命令既可以启动一个新的 shim,也可以根据 shim 的逻辑将地址返回给现有的 shim。

delete

每个 shim 必须实现一个 delete 子命令。当 containerd 无法再通过 rpc 通信时,该命令允许 containerd 删除由 shim 创建、挂载和/或运行的任何容器资源。如果 shim 与运行中的容器一起被 SIGKILL,就会发生这种情况。当 containerd 失去与 shim 的连接时,将需要清理这些资源。这也会在 containerd 启动并重新连接到 shim 时使用。如果 bundle 仍在磁盘上,但 containerd 无法连接到 shim,则会调用删除命令。

删除命令必须接受以下标志:

  • 容器的命名空间
  • -address containerd 的主套接字地址
  • 向容器 ID 发布事件的二进制路径
  • -id 容器的ID
  • bundle 要删除的 bundle 的路径。在非 Windows 和非 FreeBSD 平台上,这将与 cwd 匹配。

除 Windows 和 FreeBSD 平台外,删除命令将在容器的捆绑包中作为其 cwd 执行。

类似命令的标志

-v

每个 shim 都应该执行一个 -v 标志。
这个类似命令的标志会打印 shim 实现的版本并退出。
输出结果不可用机器解析。

-info.

每个 shim 都应该执行一个 -info 标志。
这个类似于命令的标志会从 stdin 获取选项 protobuf,并打印 shim info protobuf(见下文)到 stdout,然后退出。

message RuntimeInfo { string name = 1; RuntimeVersion version = 2; // Options from stdin google.protobuf.Any options = 3; // OCI-compatible runtimes should use https://github.com/opencontainers/runtime-spec/blob/main/features.md google.protobuf.Any features = 4; // Annotations of the shim. Irrelevant to features.Annotations. map annotations = 5;}

主机级垫片配置

containerd 不会通过 API 为临时部件提供任何主机级配置。
如果一个 shim 需要用户在所有实例中提供主机级别的配置信息,可以设置一个 shim 特定的配置文件。

容器级垫片配置

在创建请求中,有一个通用的 *protobuf.Any 允许用户为 shim 指定容器级配置。

message CreateTaskRequest {string id = 1;...google.protobuf.Any options = 10;}

shim 作者可以为配置创建自己的 protobuf 信息,客户端可以根据需要导入并提供这些信息。

I/O

容器的 I/O 由客户端通过 Linux 上的 fifo、Windows 上的命名管道或磁盘上的日志文件提供给 shim。这些文件的路径会在初始创建的 Create rpc 和附加进程的 Exec rpc 中提供。

message CreateTaskRequest {string id = 1;bool terminal = 4;string stdin = 5;string stdout = 6;string stderr = 7;}
message ExecProcessRequest {string id = 1;string exec_id = 2;bool terminal = 3;string stdin = 4;string stdout = 5;string stderr = 6;}

使用交互式终端启动的容器将把 terminal 字段设置为 true,数据仍会以与非交互式容器相同的方式复制到文件(fifos、管道)上。

根文件系统

容器的根文件系统由 Create rpc 提供。在容器的生命周期中,Shims 负责管理文件系统挂载的生命周期。

message CreateTaskRequest {string id = 1;string bundle = 2;repeated containerd.types.Mount rootfs = 3;...}

挂载 protobuf 信息是

message Mount {// 类型定义挂载的性质。string type = 1;// 源指定挂载的名称。根据挂载类型,这// 可以是卷名或主机路径,甚至可以忽略。string source = 2;// 容器中的目标路径string target = 3;//选项指定零个或多个 fstab 样式的挂载选项。repeated string options = 4;}

Shims 负责将文件系统挂载到 bundle 的 rootfs/ 目录中。shims还负责卸载文件系统。在delete二进制调用期间,shims必须确保文件系统也被卸载。文件系统由 containerd 快照程序提供。

事件

运行时 v2 支持异步事件模型。为了让上游调用者(如 Docker)以正确的顺序获取这些事件,Runtime v2 shim 必须实现以下事件(其中 Compliance=MUST)。这就避免了 shim 和 shim 客户端之间的竞赛条件,例如,对 Start 的调用会在返回 Start 调用的结果之前发出 TaskExitEventTopic 信号。有了 Runtime v2 shim 的这些保证,在 shim 发布TaskExitEventTopic之前,Start调用必须已发布异步事件TaskStartEventTopic

任务
主题合规性说明
runtime.TaskCreateEventTopicMUST任务被成功启动时
runtime.TaskStartEventTopicMUST (follow TaskCreateEventTopic)任务被成功启动时
runtime.TaskExitEventTopicMUST (follow TaskStartEventTopic)任务按预期或意外退出时
runtime.TaskDeleteEventTopicMUST (follow TaskExitEventTopic or TaskCreateEventTopic 如果已启动)任务从shim中删除时
runtime.TaskPausedEventTopicSHOULD任务被成功暂停时
runtime.TaskResumedEventTopicSHOULD (follow TaskPausedEventTopic)任务被成功回复时
runtime.TaskCheckpointedEventTopicSHOULD任务被检查点时
runtime.TaskOOMEventTopicSHOULD如果闪存收集到 “内存不足 “事件
Execs
主题合规描述
runtime.TaskExecAddedEventTopicMUST (follow TaskCreateEventTopic )exec被成功添加时
runtime.TaskExecStartedEventTopicMUST (follow TaskExecAddedEventTopic)exec被成功启动时
runtime.TaskExitEventTopicMUST (follow TaskExecStartedEventTopic)当执行程序(除初始执行程序外)在预期或意外情况下退出时
runtime.TaskDeleteEventTopicSHOULD (follow TaskExitEventTopic or TaskExecAddedEventTopic 从未启动过)当执行程序从shim中移除时
流程

下面的序列图显示了执行 ctr run 命令时的操作流程。

日志

Shims 可通过 STDIO URI 支持可插入的日志记录。
目前支持的日志记录方案有

  • fifo – Linux
  • 二进制 – Linux 和 Windows
  • 文件 – Linux 和 Windows
  • npipe – Windows

二进制日志记录能够将容器的 STDIO 转发到外部二进制文件以供使用。
将容器的 STDOUT 和 STDERR 转发到 journald 的日志记录驱动示例如下:

package mainimport ("bufio""context""fmt""io""sync""github.com/containerd/containerd/v2/core/runtime/v2/logging""github.com/coreos/go-systemd/journal")func main() {logging.Run(log)}func log(ctx context.Context, config *logging.Config, ready func() error) error {// construct any log metadata for the containervars := map[string]string{"SYSLOG_IDENTIFIER": fmt.Sprintf("%s:%s", config.Namespace, config.ID),}var wg sync.WaitGroupwg.Add(2)// forward both stdout and stderr to the journalgo copy(&wg, config.Stdout, journal.PriInfo, vars)go copy(&wg, config.Stderr, journal.PriErr, vars)// signal that we are ready and setup for the container to be startedif err := ready(); err != nil {return err}wg.Wait()return nil}func copy(wg *sync.WaitGroup, r io.Reader, pri journal.Priority, vars map[string]string) {defer wg.Done()s := bufio.NewScanner(r)for s.Scan() {journal.Send(s.Text(), pri, vars)}}

其他

不支持的 rpcs

如果 Shim 没有或无法实现 rpc 调用,则必须返回 github.com/containerd/containerd/errdefs.ErrNotImplemented 错误。

调试和 Shim 日志

unix 上的 fifo 或 Windows 上的命名管道将提供给 shim。它可以位于 shim 的 cwd 中,名为 “log”。shims 可以使用现有的 github.com/containerd/log 软件包来记录调试信息。信息会自动在容器 d 的守护进程日志中输出,并设置正确的字段和运行时间。

ttrpc

ttrpc是垫片支持的协议之一。像生成客户端一样,它可与标准 protobufs 和 GRPC 服务一起使用。grpc 和 ttrpc 之间的唯一区别是wire协议。ttrpc 删除了 http 栈,以节省内存和二进制文件大小,从而保持较小的shim。建议在你的 shim 中使用 ttrpc,但 grpc 支持目前只是一个实验性功能。