Runtime v2 为运行时作者集成 containerd 引入了一级 shim API。
containerd 作为守护进程,并不直接启动容器。相反,它充当更高级别的管理器
或枢纽的作用,以协调容器和内容的活动。被称作 "运行时"的程序真正来启动、停止和管理容器、无论是单个容器还是容器组(如 Kubernetes pods)。
例如,containerd 会检索容器镜像配置及其作为层的内容,使用快照器将其放置在磁盘上,设置容器的 rootfs 和配置,然后启动一个运行时来创建/启动/停止容器。
本文档介绍了 v2 运行时集成模型的主要组件,以及这些组件如何与 containerd 和 v2 运行时交互。以及如何使用和集成不同的 v2 运行时。
为了简化交互,v2 版运行时引入了第一类 v2 API,供运行时作者与 containerd 集成、取代了 v1 API。
v2 API 是最小的,其作用域仅限于容器的执行生命周期。
本文档分为以下几个部分:
containerd 希望运行时能实现几种容器控制功能,如创建、启动和停止。
高级流程如下:
客户端请求 containerd 创建一个容器
containerd 布局容器的文件系统,并创建必要的配置信息
containerd 通过 API 调用运行时来创建/启动/停止容器
不过,containerd 本身实际上并不直接调用运行时来启动容器。相反,它希望调用运行时,运行时将暴露一个套接字(在类 Unix 系统上是 Unix Domain,在Windows 系统上是命名为管道的套接字),并通过 ttRPC 在该套接字上监听容器命令。
运行时将处理这些操作。至于如何处理,则完全取决于运行时的实现。
两种常见的模式是
现在采用的是分离的 "shim+engine "模式,这样可以更容易地集成执行特定运行时引擎规范(如OCI 运行时规范的不同运行时。
ttRPC协议可通过一个运行时垫片(shim)来处理,和不同的运行时引擎实现无关。只要它们实现的是 OCI 运行时规范。
最常用的运行时引擎是runc,它实现了OCI运行时规范。由于这是一个运行时引擎,它并不直接被containerd调用而是由 shim 来调用,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 实例都作为守护进程与 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 "标签分组。
流程如下
runc
来创建/启动/停止容器本文档后面的 流程中提供了一个很好的流程图。
创建容器时,可以选择运行时-单实例或 shim+engine 运行时-及其选项。
containerd服务(containerd客户端、CRI API…),或通过调用containerd提供服务的客户端来户端的例子包括 ctr
、nerdctl
、kubernetes、docker/moby、rancher 等。
运行时也可以通过容器更新来更改。
传递的运行时名称是一个字符串,用于向 containerd 标识运行时。如果是单独的 shim+engine,那么这个字符串就是运行时 shim。无论如何,这都是 containerd 执行并期望启动 ttRPC 监听器的二进制文件。
运行时名称可以是类似 URI 的字符串,或者从 containerd 1.6.0 开始是可执行文件的实际路径。
如果运行时名称是 URI-like,containerd 将使用下面的逻辑把传递的运行时从 URI-like名称转换为二进制名称:
-
替换所有 .
runc.v2
.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 /hello
ctr: 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 的运行时作者而设。
它将详细介绍 API 的工作原理以及构建 shim 时的不同注意事项。
容器信息通过两种方式提供给 shim。
OCI Runtime Bundle和 Create
rpc 请求。
start
启动每个 shim 必须实现一个 start
子命令。
该命令将启动新的 shim。
启动命令必须接受以下标志:
-namespace
容器的命名空间-address
containerd 的主 grpc socket 地址-id
容器的 ID启动命令以及对 shim 的所有二进制调用都将容器的 bundle 设置为 cwd
。
start 命令可能有以下特定于 containerd 的环境变量设置:
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
容器的IDbundle
要删除的 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 由客户端通过 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.TaskCreateEventTopic |
MUST | 任务被成功启动时 |
runtime.TaskStartEventTopic |
MUST (follow TaskCreateEventTopic ) |
任务被成功启动时 |
runtime.TaskExitEventTopic |
MUST (follow TaskStartEventTopic ) |
任务按预期或意外退出时 |
runtime.TaskDeleteEventTopic |
MUST (follow TaskExitEventTopic or TaskCreateEventTopic 如果已启动) |
任务从shim中删除时 |
runtime.TaskPausedEventTopic |
SHOULD | 任务被成功暂停时 |
runtime.TaskResumedEventTopic |
SHOULD (follow TaskPausedEventTopic ) |
任务被成功回复时 |
runtime.TaskCheckpointedEventTopic |
SHOULD | 任务被检查点时 |
runtime.TaskOOMEventTopic |
SHOULD | 如果闪存收集到 "内存不足 "事件 |
主题 | 合规 | 描述 |
---|---|---|
runtime.TaskExecAddedEventTopic |
MUST (follow TaskCreateEventTopic ) |
exec被成功添加时 |
runtime.TaskExecStartedEventTopic |
MUST (follow TaskExecAddedEventTopic ) |
exec被成功启动时 |
runtime.TaskExitEventTopic |
MUST (follow TaskExecStartedEventTopic ) |
当执行程序(除初始执行程序外)在预期或意外情况下退出时 |
runtime.TaskDeleteEventTopic |
SHOULD (follow TaskExitEventTopic or TaskExecAddedEventTopic 从未启动过) |
当执行程序从shim中移除时 |
下面的序列图显示了执行 ctr run
命令时的操作流程。
Shims 可通过 STDIO URI 支持可插入的日志记录。
目前支持的日志记录方案有
二进制日志记录能够将容器的 STDIO 转发到外部二进制文件以供使用。
将容器的 STDOUT 和 STDERR 转发到 journald
的日志记录驱动示例如下:
package main
import (
"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 container
vars := map[string]string{
"SYSLOG_IDENTIFIER": fmt.Sprintf("%s:%s", config.Namespace, config.ID),
}
var wg sync.WaitGroup
wg.Add(2)
// forward both stdout and stderr to the journal
go 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 started
if 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)
}
}
如果 Shim 没有或无法实现 rpc 调用,则必须返回 github.com/containerd/containerd/errdefs.ErrNotImplemented
错误。
unix 上的 fifo 或 Windows 上的命名管道将提供给 shim。它可以位于 shim 的 cwd
中,名为 “log”。shims 可以使用现有的 github.com/containerd/log
软件包来记录调试信息。信息会自动在容器 d 的守护进程日志中输出,并设置正确的字段和运行时间。
ttrpc是垫片支持的协议之一。像生成客户端一样,它可与标准 protobufs 和 GRPC 服务一起使用。grpc 和 ttrpc 之间的唯一区别是wire协议。ttrpc 删除了 http 栈,以节省内存和二进制文件大小,从而保持较小的shim。建议在你的 shim 中使用 ttrpc,但 grpc 支持目前只是一个实验性功能。