LabHub
学习 学习路径 课程

GPU Operator 与时间片

运行时关卡 — 不被校验的字符串与真正加载的东西的对照

在 LabHub 中继续学习

目标

同时处理 runtime gate 的两端。在集群侧,观察 RuntimeClass 会选择什么、不会验证什么;在节点侧,制造两种配置被悄然忽略的情况。最后亲手编写一个对照检查两端的 checker。

为什么重要

如果 Pod 已调度到节点,却在创建容器时失败,问题就在 runtime 侧。RuntimeClasshandler 只是一个字符串,**apiserver 不会验证它。**因为每个节点的 containerd 配置可能不同,原本就不存在能够统一验证的主体。因此,即便 handler 拼错,资源也能应用并被调度,直到该节点实际创建容器时才暴露问题。

节点侧还有两种静默失败。若 imports glob 与文件名不匹配,containerd 会不发一言地跳过;配置版本升到 3 后,plugin 名称本身会变化,使用旧名称编写的配置会被整体忽略。两种情况下,**文件中都原样写着期望的内容。**所以检查必须查看实际加载的配置,而不是文件;并且只有将该判定与集群中的 RuntimeClass 列表相互对照,才真正有用。本实验的最终产物正是这样的 checker。

环境

此 Pod 中没有真实 containerd。因此,配置使用 TOML parser 处理;checker 要能够接收 dump 参数,以便评分器使用两种 dump 实际运行。集群侧则是由 kwok 启动的真实 apiserver 和 scheduler,所以 RuntimeClass 注册与 overhead 计算都是真实行为。工作目录为 /root/gpurt,其下使用 k8s/etc/bin/fixtures/out/

步骤

  1. 创建 gpu-rt namespace 和两个 RuntimeClass。
  2. 观察拼错的 handler 仍能使 Pod 被调度,并记录到 out/unvalidated.txt
  3. 创建两个 cpu 为 1 的节点,观察 RuntimeClass overhead 如何导致不同结果。
  4. 创建无法匹配 drop-in 的 imports glob,并记录到 out/imports.txt
  5. 用 version 3 格式将相同配置重写到 etc/config-v3.toml
  6. 使用 bin/rc-crosscheck.sh 编写对照集群与 dump 的 checker。
  7. 创建更改默认 runtime 的 drop-in,并在 out/blast.txt 记录 blast radius。
  8. 模拟一个只加载了部分 handler 的 dump,用 checker 检查并保存到 out/crosscheck.txt

参考

创建用于选择 handler 的对象

创建 gpu-rt namespace,并在 /root/gpurt/k8s/runtimeclasses.yaml 中写入两个 RuntimeClass 后应用。它们的名称和 handler 分别为 nvidianvidia-cdi

RuntimeClass 是 cluster-scoped object,handler 值必须与节点 containerd 配置中的 runtimes.<이름> 匹配。kubelet 创建 Pod 时会把这个字符串放入 CRI request,containerd 再从自己的配置中寻找同名项。apiVersionnode.k8s.io/v1

拼错的 handler 也能通过 apiserver

/root/gpurt/k8s/runtimeclass-typo.yaml 中写入 handler 被故意拼错的 RuntimeClass nvidia-typo,以及使用它的 Pod probe-typo,并一同应用到 gpu-rt。将确认到的事实以三行写入 /root/gpurt/out/unvalidated.txtAPI_VALIDATES_HANDLERFAILS_ATVISIBLE_TO

apiserver 不会验证 handler 字符串,因为各节点的 containerd 配置可能不同,原本就没有统一验证主体。因此,拼写错误也能应用并调度。只有节点实际创建容器的瞬间才会暴露不匹配,而且只有节点知道这一事实。三行的值分别填写 no、失败发生的阶段,以及知晓它的主体,后两项都用一个单词。

RuntimeClass overhead 改变调度结果

使用 /root/gpurt/k8s/node-tight.yaml 创建两个 cpu 为 1 的 kwok 节点(gpu-tight-agpu-tight-b)。然后在 /root/gpurt/k8s/overhead.yaml 中设置 overhead.podFixed 为 cpu 250m、memory 128Mi 的 RuntimeClass nvidia-overhead;再写入两个各请求 cpu 900m 的 Pod 并应用。让 fits 不使用 RuntimeClass 放到 a 节点,让 over 使用该 RuntimeClass 放到 b 节点。

RuntimeClass 的 overhead 是该 runtime 为每个 Pod 额外消耗的资源。apiserver 会把它填入 Pod 的 spec.overhead,scheduler 则把它加到 container request 后计算容量。因此,同样请求 900m,加上 overhead 后会变成 1150m,无法放入 cpu 为 1 的节点。使用两个独立节点,是为了避免先进入的 Pod 占用容量而模糊比较。kwok 节点必须带有 kwok.x-k8s.io/node: fake annotation 才会受管理。

imports glob 不匹配时静默跳过

/root/gpurt/etc/config.toml 中创建事故节点的主配置。包含 version = 2,在 CRI plugin 下包含 default_runtime_name = "runc"runtimes.runc,并设置一个无法匹配 drop-in 的 imports glob。drop-in 写在 /root/gpurt/etc/conf.d/99-nvidia.toml,内容为 runtimes.nvidia。将检查结果以四行写入 /root/gpurt/out/imports.txt

glob 未匹配到任何内容并不是错误,而是正常结果,因此 containerd 不会留下日志。若扩展名写成 .conf,文件却以 .toml 落盘,该配置将永远不会被读取。imports.txt 的四行是 GLOBMATCHEDDROPIN_EXISTSLOADED_NVIDIAGLOB 原样写入配置中的 glob,MATCHED 写该 glob 实际匹配的文件数。使用 ls <글롭> 检查。

改写为 containerd 2.x 的 config version 3

/root/gpurt/etc/config-v3.toml 中使用 version 3 格式写入相同内容。必须包含 version = 3,plugin 名称为 io.containerd.cri.v1.runtimedefault_runtime_nameruncruntimes 中同时包含 runcnvidia,且 nvidia 中包含 options.BinaryName。这次 imports glob 必须实际匹配 drop-in。

containerd 2.x 使用 config version 3,此时 plugin 名称本身也会变化。如果只提高版本,却仍保留旧名称(io.containerd.grpc.v1.cri),该配置会被整体忽略,却不会报错。这与上一步是同类静默失败。写完后不要靠目视判断,请用 parser 读取确认。执行 python3 -c "import tomllib,sys;print(tomllib.load(open(sys.argv[1],'rb')))" <파일> 即可。

对照集群所需 handler 与实际加载内容

创建 /root/gpurt/bin/rc-crosscheck.sh。若传入 dump 文件参数,则读取该文件;否则读取 containerd config dump 的结果。对于集群 RuntimeClass 所需但 dump 中不存在的每个 handler,逐行输出 MISSING=<핸들러>,并以退出码 1 结束。一个都不缺时输出 OK,并以 0 结束。

关键在于判定依据是实际加载的配置,而不是文件。读取 /etc/containerd/config.toml 的检查从原理上就无法发现这次事故,因为文件中原样写有期望内容,只有加载结果不同。dump 的 runtime 名称可能因版本不同而位于 io.containerd.grpc.v1.criio.containerd.cri.v1.runtime 下,因此两者都要检查。集群侧使用 kubectl get runtimeclass -o jsonpath='{range .items[*]}{.handler}{"\n"}{end}' 提取,TOML 解析使用 python3tomllib。评分器会用两种 dump 实际运行此脚本。

更改默认 runtime 会改变 blast radius

/root/gpurt/etc/conf.d/50-default.toml 中创建一个 drop-in,把 CRI plugin 的 default_runtime_name 设为 nvidia。将结果以四行写入 /root/gpurt/out/blast.txtDEFAULT_RUNTIMEAFFECTEDNEEDS_RUNTIMECLASSBLAST_RADIUS

toolkit 部署中常见把默认 runtime 整体改为 nvidia。这样每个 Pod 无需写 runtimeClassName,较为方便,但不使用 GPU 的 Pod 也会经过该 runtime。一旦配置损坏,无法启动的就不只是 GPU Pod,而是该节点的所有 Pod。四行的值依次用一个单词填写 runtime 名称、受影响 Pod 的范围、每个 Pod 是否需要 RuntimeClass,以及事故影响范围。

用实际 dump 运行所编写的 checker

/root/gpurt/fixtures/dump-node1.toml 中模拟一个只加载部分 handler的节点 dump。然后让第 6 步的脚本检查该文件,并将输出保存到 /root/gpurt/out/crosscheck.txt

此 fixture 模拟“文件中全部写有,但只加载了一部分”的节点。必须包含 runc,同时必须缺少集群 RuntimeClass 所需的部分 handler。若全部包含,就不存在不匹配,本步骤也失去意义。评分器会自行对照该 fixture 与集群,计算缺失 handler,再与输出比较。