LabHub
学习 学习路径 课程

GPU Operator 与时间片

containerd 的配置是怎么合起来的

在 LabHub 中继续学习

一句话总结

把配置写入文件,与该配置实际被加载,是两件不同的事。两者之间存在四道缝隙:合并规则、是否重启、Pod 终止时的清理逻辑,以及已经运行的容器。GPU 事故几乎都发生在这些缝隙中。

概念图: 两件不同的事 · 准确检查错误的位置 · 缝隙 1——Drop-in 可能被整体忽略。 · 合并不是按字段进行。

为什么需要了解这些

这是实际发生过的事故。为了启用时间切片,团队修改了 ClusterPolicy,toolkit DaemonSet 因而重新运行。从那天起,集群中的四块 GPU 就再也无人能够使用。

排查花了数小时,而其中大部分时间都用来准确检查错误的位置。执行 cat /etc/containerd/conf.d/99-nvidia.toml 时,可以看到 nvidia 运行时配置完好存在。文件在,于是人们判断“配置已经完成”,转而排查其他地方。直到第一次执行 containerd config dump,答案只有一行——已加载的运行时处理程序只有 runc

工作原理

缝隙 1——Drop-in 可能被整体忽略。 containerd 主配置可以通过 imports 引入其他文件。

version = 2
imports = ["/etc/containerd/conf.d/*.toml"]

这里有一个几乎所有人都会理解错误的地方:合并不是按字段进行。 如果某个 Drop-in 修改了一个插件的配置,该插件的全部配置都会替换为这个文件中的内容。Drop-in 没有写出的字段不会继承前一个文件的值,而会回落到默认值

Drop-in 按名称顺序读取,因此最终由最后一个提及该插件的文件接管全部配置

以下是事故节点上的实际测量结果(containerd 1.7.27)。

99-nvidia.toml 만 import              → runtimes.nvidia  6개
zz-labhub-registry.toml 만 import     → runtimes.nvidia  0개
둘 다 import                          → runtimes.nvidia  0개   ← 여기

zz-labhub-registry.toml 只有三行。因为 Harbor 使用 HTTP,所以加入它用于指定证书路径。

[plugins."io.containerd.grpc.v1.cri".registry]
  config_path = "/etc/containerd/certs.d"

其中完全没有提到运行时。但只因为它提及了 CRI 插件,并且文件名排在 99- 之后,就用这三行替换了整个 CRI 配置。nvidia 运行时集合也在此时消失。

为什么一周内都没有发现

这个故障真正可怕的地方就在这里。打开 config dump,会看到如下内容。

[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc]
  runtime_type = "io.containerd.runc.v2"
sandbox_image = "registry.k8s.io/pause:3.8"

其中有 runcsandbox_image 看起来也很合理,仿佛配置仍然有效。实际上这些全是默认值。 我们写入的值一个都没保留,但默认值与我们可能采用的值很相似,所以无法察觉。

因此,团队曾两次根据这个 dump 错误判断“Drop-in 并未合并”。两次推断都很合理,却都不正确。真正给出答案的不是推理,而是逐一移除 Drop-in 后重新生成 dump

“不要只读内容就下结论”的原则不只适用于文件。阅读 dump 时,也必须区分当前看到的值是自己写入的值,还是默认值。唯一的区分方法,就是逐一移除配置,观察结果是否变化。

缝隙 2——发送 SIGHUP 不会注册运行时处理程序。 toolkit 容器写完配置后,会向 containerd 发送 SIGHUP。然而,containerd 1.7 的重新加载不会重新初始化插件。CRI 插件在初始化时读取运行时处理程序列表,因此新加入的处理程序在进程重启前不会进入列表。于是,“toolkit Pod 已 Ready,配置文件也已更新,运行时却不存在”完全可能正常出现。

缝隙 3——删除 toolkit Pod 会恢复配置。 toolkit 容器包含终止时的清理逻辑,会移除自己添加的 nvidia 配置并恢复原状。从设计上看这是正确的——删除 Operator 后,不应在节点留下垃圾配置。问题在于事故处理中形成的操作习惯。如果先执行“删除 Pod 后重新创建”,再重启 containerd,那么到重启时配置已经被移除。顺序颠倒后,无论重复多少次都不会恢复。

缝隙 4——已运行的容器不会透露任何信息。 只有在创建容器时才会查询运行时处理程序。即使配置消失,已经运行的 GPU Pod 仍会继续正常工作。因此,故障不会立即暴露,而会在节点重启或 Pod 重建时——往往是几天后的凌晨——集中爆发。这种潜伏性使 GPU 配置事故格外昂贵。

生产现场中的常见情况

第一,恢复顺序是固定的。 正确恢复顺序始终如下:① 通过 containerd config dump 检查当前加载的处理程序 → ② 让 toolkit DaemonSet 恢复正常,并等待 Pod 重新写入配置 → ③ 然后重启 containerd → ④ 再次使用 config dump 确认。如果把第 ②、③ 步调换,就只能从头再来。

第二,自动执行节点启动后的检查。 潜伏故障无法依靠人的记忆发现。只需在每次节点启动时检查 containerd config dump 中是否存在所需处理程序,不存在就以失败结束。关键在于,读取文件的检查绝对无法发现这类事故。

第三,在文档中明确配置所有权。 如果有多个主体修改节点上的 containerd 配置(OS 镜像构建、配置管理工具、GPU Operator),它们迟早会互相覆盖。用一行文档明确谁拥有哪个文件,是从根本上减少此类事故的唯一方法。

下一次实验要做什么

后续实验会亲手制造这些缝隙。观察即使 RuntimeClass 的 handler 未经验证,Pod 也会被调度;分别制造 imports glob 不匹配导致 Drop-in 被整体忽略,以及配置版本升级到 3 后插件名称变化的情况;再亲自编写检查器,对比集群要求的处理程序与实际加载的处理程序。查看已加载状态而不是文件内容的习惯,将完整体现在这一个脚本中。