containerd Shim Capabilities 机制详解:通过 Bootstrap 扩展声明运行时能力
【免费下载链接】containerdAn open and reliable container runtime项目地址: https://gitcode.com/GitHub_Trending/co/containerd
导读
Shim Capabilities 是 containerd 运行时 v2 架构中一套能力协商机制:shim 在启动时通过BootstrapResult附加类型化扩展(typed extension),主动告诉 containerd 自己能够独立完成哪些挂载类型(mount type)与挂载变换(transform),从而让 mount manager 不再重复代劳。本文以 docs/shim-capabilities.md 为主体,结合 bootstrap.proto、mount.proto 与 core/runtime/v2/task_mounts.go 等源码,讲解该扩展的协议格式、注册表语义、链式变换的"后缀规则"以及旧注解的迁移路径,帮助 shim 开发者与容器运行时集成方正确声明自身能力。
一、能力声明的载体:BootstrapResult 扩展
在 containerd 2.3 引入的引导协议(bootstrap protocol)中,shim 的start子命令从 stdin 读取BootstrapParams,向 stdout 写出BootstrapResult。BootstrapResult除了携带监听地址(address)与通信协议(protocol,可取ttrpc或grpc)之外,还预留了一个可选字段:
message BootstrapResult { ... // Optional: Typed detail describing what this shim instance is able to // do, so that containerd can adjust its own behavior. repeated Extension extensions = 6; }每个Extension内部就是一个google.protobuf.Any,shim 可以将任意类型化的结构化数据打包进去。containerd 侧的解析逻辑如下(见 core/runtime/v2/task_mounts.go):
func mountCapabilitiesExtension(bootstrap *bootapi.BootstrapResult) (*apitypes.MountCapabilities, error) { var caps apitypes.MountCapabilities found, err := bootstrap.FindExtension(&caps) if err != nil { return nil, err } if !found { return nil, nil } return &caps, nil }向后兼容的关键设计
containerd 会忽略无法识别的扩展类型。这意味着 shim 可以无条件地附加扩展:当它运行在一个不认识该扩展的旧版 containerd 上时,旧守护进程只是继续保持原有行为,不会因此出错。这一点在bootstrap.proto的注释中亦有明确约定("containerd MUST ignore an extension whose type it does not recognize"),相关测试可见 core/runtime/v2/shim_test.go 中的TestBootstrapParamsUnknownExtension。
扩展的查找与注册
BootstrapResult携带的extensions字段是注册表式(registry-style)的设计:shim 通过AddExtension附加,containerd 通过FindExtension按类型 URL 查找,整个过程对协议本身零侵入。引入一种新的能力类型不需要修改核心协议,只需要双方约定一个新的类型 URL。
二、MountCapabilities 扩展详解
当前文档注册表中的第一种、也是截至本文写作时文档记录的能力扩展是containerd.types.MountCapabilities。它描述的是:shim 自己执行了某些挂载类型或变换,mount manager 不得再代为执行。典型场景是 VM 类运行时——它可以把磁盘镜像文件直接传给 guest,而不是让宿主机先建立 loop 设备,从而获得更优的性能。
2.1 Proto 定义与字段语义
定义见 api/types/mount.proto:
message MountCapabilities { // Types are the mount types the sender performs itself, such as "erofs" or // "loop". These are base mount types, with any transform prefixes removed. repeated string types = 1; // Transforms are the mount transforms the sender applies itself, such as // "format", "mkfs" or "mkdir". // // A transform is named on its own, without the "/<mount-type>" suffix that // appears in a mount type, so "format" covers the mount types "format/bind" // and "format/mkdir/overlay" alike. repeated string transforms = 2; }两个字段的命名规则非常关键:
| 字段 | 取值示例 | 语义 |
|---|---|---|
types | "erofs"、"loop" | 基础挂载类型,必须去掉变换前缀。即只写overlay,不写format/mkdir/overlay |
transforms | "format"、"mkfs"、"mkdir" | 变换名本身,不带/<mount-type>后缀。声明format即同时覆盖format/bind和format/mkdir/overlay等一切以format/开头的挂载链 |
由于两个字段都是开放字符串(open ended strings),未来新增挂载类型或变换同样不需要修改协议。
2.2 空扩展的含义
附加一个两个字段都为空的MountCapabilities扩展,表示shim 除了普通系统挂载之外不处理任何额外内容——mount manager 将照常完成全部工作。这在源码中表现为:mountClaimOpts遍历caps.Types与caps.Transforms,为空时不会产生任何激活选项(见 core/runtime/v2/task_mounts.go)。
2.3 containerd 侧的翻译
containerd 会把扩展内容翻译为 mount manager 的激活选项(activation options):
opts := make([]mount.ActivateOpt, 0, len(caps.Types)+len(caps.Transforms)) for _, t := range caps.Types { opts = append(opts, mount.WithAllowMountType(t)) } for _, t := range caps.Transforms { opts = append(opts, mount.WithAllowTransform(t)) }即types→mount.WithAllowMountType,transforms→mount.WithAllowTransform。这两个选项定义在 core/mount/manager.go:
WithAllowMountType(mountType):即使存在该类型的自定义 handler,这些挂载也不应被执行——除非为了支撑后续挂载而必须执行;WithAllowTransform(transform):该变换由调用者自己执行,mount manager 不执行——除非后续挂载依赖它。
翻译的正确性由 core/runtime/v2/task_mounts_test.go 中的TestTaskMountControllerActivate覆盖:例如声明Types: ["erofs", "loop"]、Transforms: ["format", "mkfs"]后,断言AllowMountTypes == ["erofs", "loop"]、AllowTransforms == ["format", "mkfs"]。
三、链式变换的"后缀规则":只能认领链的后缀
这是 MountCapabilities 语义中最容易踩坑、也最需要理解的部分。
3.1 变换从外向内执行
在 docs/mounts.md 描述的挂载管理模型中,变换通过前缀链式书写:<transformer1>/<transformer2>/<mount-type>,例如format/mkdir/overlay。变换的执行顺序是从外向内(outside-in):外层变换的输出是内层变换的输入。因此一个被认领(claimed)的变换只有作为其所在链的后缀时才会被尊重。
以format/mkdir/overlay为例:
- 认领
mkdir:mount manager 会先执行format,然后把mkdir/overlay交还给 shim 自行完成; - 只认领
format:什么都不发生,因为mkdir的输入依赖format已经运行完毕,mkdir无法在format未运行的情况下执行; - 一个想自己执行
format的 shim,必须把链中format之后的所有变换一并认领,即同时认领format与mkdir。
这套行为在 mount manager 的规划逻辑中有精确的测试用例(core/mount/manager/plan_test.go):
| 测试用例 | 激活选项 | 行为 |
|---|---|---|
| 什么都不认领 | 无 | manager 执行整条链(applyCount = 2) |
认领最内层mkdir | WithAllowTransform("mkdir") | manager 执行到format为止,mkdir/overlay留给调用者(applyCount = 1) |
只认领最外层format | WithAllowTransform("format") | 不被尊重,manager 仍执行整条链(applyCount = 2) |
| 两个变换都认领 | WithAllowTransform("format")+WithAllowTransform("mkdir") | manager 一个都不执行(applyCount = 0) |
| 认领完整字面类型 | WithAllowMountType("format/mkdir/overlay") | 等价于认领链中所有变换(applyCount = 0) |
TestPlanActivationClaimedGapInMiddle还覆盖了三变换链format/mkfs/mkdir/overlay中认领format与mkdir、而中间的mkfs未被认领的情形:由于mkfs未认领,它强制 manager 执行到它为止(applyCount = 2),只有它之后的mkdir才真正留给调用者。这一"中间有空缺(gap)则空缺之前都归 manager"的语义与文档描述完全一致。
3.2format变换的特殊性
format变换负责用 Go 模板解析挂载参数,例如{{ mount 0 }}会引用 mount manager 内部挂载点(参见 docs/mounts.md 中source/target/mount/overlay四个模板值)。这些模板引用的挂载点是mount manager 内部私有的状态,shim 无法凭空构造,因此:
shim 只能在覆盖链剩余部分的完整后缀中认领
format,绝不能单独认领format。
单独认领format在语义上不可能成立——format的输出是内层变换的输入,如果format由 shim 完成,那么后续变换的输入对 manager 而言是不存在的挂载点,链条无法继续。
四、迁移路径:废弃的 runtime-allow-mounts 注解
MountCapabilities扩展是在containerd 2.4中加入的,它取代了已废弃的运行时信息注解containerd.io/runtime-allow-mounts。为了平滑迁移:
- 扩展优先:只要 shim 在 bootstrap 结果中附加了
MountCapabilities扩展,就以扩展为准,完全不 consult 旧注解(测试"extension present takes precedence, legacy is not consulted"明确验证了这一点); - 注解兜底:未附加扩展的 shim,containerd 仍会去查询其
RuntimeInfo注解作为迁移路径; - 已知例外:
io.containerd.runc.v2与io.containerd.runhcs.v1这两个运行时确定从未设置过该注解,因此会直接跳过查询(见 core/runtime/v2/task_mounts_deprecated.go 的deprecatedNoAnnotationRuntimes映射,测试"well known default runtimes are never queried for the annotation"亦有覆盖)。
旧注解的值是一个逗号分隔列表,其中<transform>/*形式的条目表示变换,其余表示挂载类型,解析逻辑见deprecatedParseAllowedMounts:
func deprecatedParseAllowedMounts(v string) *apitypes.MountCapabilities { caps := &apitypes.MountCapabilities{} for entry := range strings.SplitSeq(v, ",") { if entry == "" { continue } if transform, ok := strings.CutSuffix(entry, "/*"); ok { caps.Transforms = append(caps.Transforms, transform) continue } caps.Types = append(caps.Types, entry) } return caps }例如注解值block,format/*会被解析为Types: ["block"]、Transforms: ["format"]。此外,旧路径对成功查询结果做了进程级缓存(sync.Map,runtime 名 → capabilities),失败查询不缓存以便下次重试;当旧注解被命中时,containerd 会记录一条 warn 日志,提示 shim 应改用MountCapabilities扩展。
安全失败方向
无论扩展解析失败(例如类型 URL 匹配但二进制内容损坏)、旧注解查询失败,还是完全没有声明,mountClaimOpts都遵循**"什么都不认领"的安全方向**:即 mount manager 承担全部挂载工作,而不是冒险把挂载留给可能没有实现它的 shim。测试用例"a malformed extension claims nothing rather than failing"与"a failed legacy lookup claims nothing rather than failing"均验证了这一点。
五、工作流程与整体关系
将上述机制放入完整链路中看:
- containerd 启动 shim(
containerd-shim-xxx start),通过 stdin 传递BootstrapParams; - shim 完成初始化后向 stdout 写出
BootstrapResult,其中extensions字段附带上containerd.types.MountCapabilities; - containerd 读取结果,通过
FindExtension提取扩展,若存在则由mountClaimOpts翻译为WithAllowMountType/WithAllowTransform激活选项; taskMountController.Activate在激活任务 rootfs 时带上这些选项调用 mount manager(见 core/runtime/v2/task_mounts.go),并把ActivationInfo.System(仍需 shim 自行完成的挂载)返回给 shim;- shim 在 rootfs 中就位后启动容器。
关键点在于:shim 必须先于其挂载被激活而启动,这样它的能力声明才能被纳入激活决策(docs/mounts.md 的 "Support with containerd shims" 一节明确说明:"The shim is started before its mounts are activated, so that what it advertises can be taken into account.")。此外,若挂载 manager 插件未配置(manager == nil),Activate会原样返回 rootfs,能力协商自然退化为无操作。
六、给 shim 开发者的实操建议
综合文档与源码,为 shim 附加能力扩展时请遵循以下要点:
- 协议层面:在写出的
BootstrapResult中调用AddExtension(&apitypes.MountCapabilities{...});扩展机制由pkg/shim自动处理,新协议失败时会回退到旧机制,无需 shim 自行判断。 - 命名规则:
types只写基础挂载类型(如erofs、loop、block),transforms只写变换名(如format、mkfs、mkdir),不要写format/mkdir/overlay这样的完整链式类型(除非你想表达"整条链都归我"——此时认领完整字面类型与认领全部变换等价)。 - 后缀规则:认领变换时务必从链的最内层(最后一个变换)开始认领;想自己执行
format,就必须同时认领其后所有变换。只认领链中间的变换不会生效。 format特殊对待:由于format依赖 manager 内部挂载点的模板解析,它只能在覆盖整条链剩余部分的后缀中被认领,绝不能单独认领。- 迁移:新 shim 一律使用
MountCapabilities扩展;仍在使用containerd.io/runtime-allow-mounts注解的 shim 应尽快迁移(注解按<transform>/*区分变换),迁移完成前 containerd 2.4+ 会继续兼容查询,但io.containerd.runc.v2与io.containerd.runhcs.v1除外。
参考资料
- docs/shim-capabilities.md:本文主体,能力扩展注册表与语义的权威定义
- docs/runtime-v2.md:引导协议(bootstrap protocol)与 shim 编写总览
- docs/mounts.md:mount manager、挂载类型与变换、以及与运行时的关系
- api/runtime/bootstrap/v1/bootstrap.proto:
BootstrapParams/BootstrapResult/Extension定义 - api/types/mount.proto:
MountCapabilities消息定义 - core/runtime/v2/task_mounts.go:扩展解析与激活选项翻译的实现
- core/runtime/v2/task_mounts_deprecated.go:废弃注解的迁移查询与解析
- core/runtime/v2/task_mounts_test.go 与 core/mount/manager/plan_test.go:能力翻译与后缀规则的测试验证
- core/mount/manager.go:
WithAllowMountType/WithAllowTransform激活选项定义
【免费下载链接】containerdAn open and reliable container runtime项目地址: https://gitcode.com/GitHub_Trending/co/containerd
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考