TT Lab
开始
学习 学习路径 课程

GPU Operator 与时间片

同样的消息,不同的原因 — GPU 故障分类表

在 TT Lab 中继续学习

一句话总结

收到“GPU Pod 起不来”的报告时,先按 Pod 对象是否存在分出两路;如果存在,再按是 Pending 还是 Running 分一次,然后对照节点 status 和 Pod spec,把原因收敛到七种之一。调度器留下的语句,在三种不同的原因下会一字不差地完全相同,所以光读消息是不够的。

为什么需要它

运维 GPU 集群时,最常收到的咨询永远是同一句话:“我的任务跑不起来。”可是在这一句话背后,实际的状态至少有这么多种不同。

这三种情况需要调查的地方完全不同。第一种在准入(配额、RuntimeClass、策略),第二种在调度器,第三种在 Pod spec 和节点的运行时。可是报告的人不会替你区分这三种。所以接到报告的一方,必须有在最初 3 分钟内决定分支的流程。 没有这个流程,就会把 device plugin 日志翻上一个小时,才发现“啊,原来 Pod 压根就没有创建”。

还有一个更糟的情况。Kubernetes 调度器通常会很贴心地留下失败原因,但是对于扩展资源,这份贴心反而起到了遮蔽原因的作用。 下面三种情况下出现的语句是一样的。

三种情况全都是 Insufficient nvidia.com/gpu。原因分别是“把插件恢复起来”“修复节点”“等待空位或增加空位”,完全不同,而消息却无法区分。仅凭这一个事实,就是本模块存在的理由。

区分症状的两个首要问题

问题 1——Pod 对象是否存在。

kubectl get pod <이름> -n <네임스페이스> -o json

(占位符依次为 Pod 名称与命名空间)

如果不存在,调度器就与这件事毫无关系。对象必须通过 API 服务器的准入才会生成,就 GPU 而言,准入拦截的两种典型情况是 ResourceQuota 超限和引用不存在的 RuntimeClass。两者都只会在运行 kubectl apply 的人的终端上打印错误,集群里不会留下任何痕迹。如果报告的人已经关掉了那个屏幕,最快的办法就是重新应用同一份清单来复现错误。

问题 2——spec.nodeName 是否为空。

kubectl get pod <이름> -n <네임스페이스> -o jsonpath='{.spec.nodeName}'

(占位符与上面相同)

如果为空,就是调度问题。如果已经有值但仍有问题,那就是节点上的问题(运行时 handler、驱动,或者没有写请求的清单)。看这个字段而不是 kubectl get pod 的 STATUS 列,是因为 Pending 这一个词既涵盖了“没能选出节点”,也涵盖了“选出了节点,却无法创建容器”。

原因会在哪里暴露

原因 决定性证据所在位置 查看命令
超出配额 没有 Pod。创建时的错误信息 kubectl get resourcequota -n <ns> -o json
没有 RuntimeClass 没有 Pod。创建时的错误信息 kubectl get runtimeclass
没有写请求 spec.containers[].resources.limits 中没有 nvidia.com/gpu kubectl get pod -o json
标签不匹配 满足 spec.nodeSelector 的节点有 0 个 kubectl get nodes --show-labels
污点 候选节点的 spec.taints 无法被 Pod 的 tolerations 容忍 kubectl get node -o json
资源未上报 候选节点的 status.capacity 中根本没有资源名称 kubectl get node -o json
allocatable 为 0 capacity 为正数,但 status.allocatable 为 0 同一输出的另一列
空位耗尽 allocatable 为正数,但该节点上的 Pod 请求之和恰好等于它 对 kubectl get pods -A -o json 求和

这张表的用处不在于“看什么”,而在于按什么顺序看。顺序是从上到下。前面的为真,后面的就不必看;不遵守顺序,就会得出离谱的结论。例如,nodeSelector 与任何节点都不匹配的 Pod,污点和资源本来就没有可检查的对象。然而如果先采取“把污点删掉也起不来”之类的措施,只会把好好的集群配置弄坏。

最后一行(空位耗尽)是唯一需要计算的。只看节点对象是看不出来的,必须把放置在该节点上的 Pod 收集起来,把 GPU 请求加在一起。这里有一个陷阱:以 Succeeded 或 Failed 结束的 Pod 并不占着空位,所以必须从合计中去掉。不去掉的话,就会把实际上空着的节点误判为“满了”。

因为是扩展资源而产生的情况

nvidia.com/gpu 与 cpu、memory 不同,是 kubelet 自己无法统计的扩展资源。官方文档对扩展资源的规则明确规定了两条:不支持超额分配,requests 与 limits 必须相同,并且值必须是整数。所以读取 GPU 请求时,只看 limits 就够了。反过来说,不存在半块或 1.5 块这样的值,也没有缺多少就给多少这样的行为。 请求要么被整体满足,要么 Pod 就等待,二者必居其一。

而且扩展资源的成立与“设备是否存在”无关。只要节点 status 里写着数字,调度器就会把 Pod 派到那个节点。反过来,即使插着八块卡,只要 status 里没有数字,在调度器看来,这个节点就是没有 GPU 的节点。调查时必须牢牢记住:不是物理事实,而是 Node 对象,才是调度器唯一的真相。

在现场相遇的样子

第一,只读 kubectl describe 就断定原因的习惯,代价最高。 如前所见,三种原因给出同一条语句。曾经有一个组织,在夜间看到“Insufficient nvidia.com/gpu”就自动扩充节点,并为此挂了脚本,而实际原因是某一个节点的 allocatable 降到了 0。节点增加了,那个节点依然是 0,只是成本增加了。

第二,由人来判定的话,顺序是守不住的。 所以这种判定最好固化成工具。输入是一个 Pod,输出是一个表示原因的词。有了这样的工具,一线响应人员在接到报告的当场就能直接回答“这是配额问题”,判定标准也不会因人而异。本模块的实验要制作的,正是这样的工具。

第三,输出必须是一个词才有用。 如果输出的是一句话,人就得重新阅读,也无法接入自动化。像 no-gpu-node、allocatable-zero、exhausted 这样,选定下一步行动只有一个的词汇,它就可以直接成为告警路由的键。确定原因词汇的工作,其实就是确定运维流程的工作。

第四,本环境诚实的局限。 实验集群里既没有 GPU,也没有 device plugin。所以“设备插件挂了”这一状态,不是靠把插件杀掉,而是靠不在节点 status 中写入资源来造出来的。这不是模拟,而是同一种状态——因为在真实故障中,调度器看到的也只有 status。不过,“Pod 是 Running,容器里却看不到设备”的后半部分,由于容器并不真正运行,所以无法确认。这一分支只判定到 Pod spec 中没有资源请求这一事实为止。

参考文档

下一项实验要做什么

在 kwok 启动的真实调度器上,把七种状态逐一制造出来。搭建上报完好的节点、根本没有资源名称的节点、只有 capacity 而 allocatable 为 0 的节点,分别往上面放 Pod,亲眼看到三种原因给出同一条语句。 再加上只有污点不同的 Pod 和只有 nodeSelector 不同的 Pod,并经历在准入时被拒绝、连对象都不会生成的两种情况(RuntimeClass、配额)。最后制作一个只看 kubectl get -o json 就用一个词回答原因的分类器,并把它接到七个 Pod 上全部运行一遍。