ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

K8s节点污点致Device Plugin离线,RDMA扩展资源消失故障排查

K8s节点污点致Device Plugin离线,RDMA扩展资源消失故障排查 1. 表象RDMA资源神秘消失新任务卡在CSI挂载超时1.1 工单里的两个不相干故障事情发生在一次节点维护后。我们有一个专门跑分布式训练的RDMA节点池上面挂着两张HCA网卡通过Device Plugin以rdma/hca扩展资源的方式提供给K8s调度。周一上午业务团队反馈新提交的训练任务全部卡在ContainerCreating我看了一眼事件发现报错集中在PVC挂载上kubelet打出了典型的CSI超时记录Warning FailedMount 4m30s kubelet MountVolume.SetUp failed for volume pvc-xxxx : rpc error: code DeadlineExceeded desc context deadline exceeded与此同时另一个同事在检查节点的时候发现更诡异的情况节点上的rdma/hca资源从Allocatable里消失了。注意不是数量变少是从调度器的视角直接查无此资源。业务Pod声明的resources.requests[rdma/hca]: 1压根儿没地方落地新任务当然起不来。两个症状放到一起第一反应就是RDMA设备坏了驱动挂了或者Device Plugin崩了。毕竟一个是资源上报消失一个是存储挂载失败看起来都和节点硬件有关。我当时也是这么想的差一点就让同事去机房换网卡了。1.2 初查阶段硬件没坏重装Device Plugin只是诈尸按惯例先做硬件排查。lspci能看到HCA设备ibstat也能正常读到端口状态和链路速率说明网卡物理上是好的。接着查内核日志dmesg里没有任何关于Mellanox驱动的报错RDMA协议栈的状态也正常。硬件和驱动基本可以排除。然后我们决定重启Device Plugin。这个动作很有效果但也极具迷惑性Device Plugin的Pod在目标节点上重新跑起来之后rdma/hca资源短暂地从某个节点上回来了看起来一切恢复正常。可是业务Pod依然起不来CSI挂载的超时错误还在继续刷屏。再仔细一看资源回来只是一瞬间的事。因为Device Plugin恢复后kubelet会重新注册并上报设备但紧接着这个Pod又因为某种原因被调度器从节点上拿掉了资源再次归零。这种诈尸式恢复把我们引向了完全错误的方向以为问题是插件本身不稳定反复去查镜像版本、socket通信、权限配置折腾了大半天。1.3 真正的破绽目标节点上少了两个DaemonSet Pod转机出现在我把视线从资源挪到Pod的时候。我习惯性地敲了一条命令kubectl get pods -A -o wide | grep -E rdma|csi|device-plugin目标节点那一行怎么都搜不到Device Plugin的Pod同时CSI的Node端组件我们用的是某个支持RDMA共享存储的CSI Driver也不在节点上。更明显的破绽是CSI节点插件的Pod状态是Pending事件里写的一清二楚Events: Type Reason Age From Message Warning FailedScheduling 4m9s default-scheduler 0/6 nodes are available: 2 node(s) had untolerated taint {dedicatedrdma: NoSchedule}, ...到这里我才反应过来不是资源消失了是负责上报资源的管家根本没住在这台机器上。之所以RDMA资源会从节点上消失是因为Device Plugin压根没在这个节点上运行kubelet收不到设备上报自然就把这块资源从调度池里抹掉了。而CSI挂载失败也是同一个原因——节点上连负责执行挂载动作的CSI Node组件都没有kubelet的挂载请求根本无人响应超时是必然的。问题的根子指向了平台组件和节点污点之间的关系。2. 机制扩展资源消失的本质是Device Plugin不再汇报心跳2.1 RDMA设备如何变身K8s可调度资源先把底层机制理清楚。K8s本身不认识RDMA网卡也不认识任何厂商的HCA设备。要让调度器知道某个节点上有多少张网卡可以分配给Pod必须通过Device Plugin框架来翻译。流程大致是这样的Device Plugin以一个DaemonSet或裸Pod的方式跑在节点上。启动后插件通过/var/lib/kubelet/device-plugins/kubelet.sock这个Unix Socket向kubelet注册自己上报协议版本、资源名称和设备列表。kubelet的Device Manager收到注册信息后把设备列表记录到节点状态里。之后一直保持ListAndWatch的流式连接实时上报设备健康状态。调度器读取节点Allocatable中的扩展资源决定Pod是不是能调度到这个节点。注意扩展资源和CPU、内存这种内置资源有本质区别内置资源的数量是kubelet启动时从操作系统采集的不依赖额外组件而rdma/hca这种扩展资源完全依赖Device Plugin上报。没有一个活的插件在节点上跑kubelet就不知道节点存在这些设备。所以RDMA资源消失准确的说法是kubelet失去了对RDMA设备的感知调度器自然也就拿不到资源数量。2.2 kubelet发现Plugin掉线后Allocatable发生了什么Device Plugin和kubelet之间的ListAndWatch连接是长连接。正常情况下两边保持心跳一旦插件Pod挂掉或者被调度到别的节点连接就会断开。kubelet的Device Manager会把这个插件标记为Unhealthy并在下一次同步节点状态时把该插件上报的所有设备从可分配列表里拿掉。体现在API上就是kubectl describe node cn-gpu-02 | sed -n /Capacity/,/Allocatable/p你会看到Capacity里可能还留着类似rdma/hca: 2的痕迹但Allocatable里这个键要么直接消失要么显示为rdma/hca: 0。调度器只看Allocatable所以在它眼里这个节点就是没有RDMA资源。我特意强调这个细节是因为排障时很多人只盯着Capacity看觉得硬件还在、资源还在就想不通为什么调度不上去。其实调度器根本不看Capacity它只认Allocatable。凡是扩展资源一旦上报源断了Allocatable必然清零。2.3 一张命令清单确认资源断层位置这类问题的排查思路其实就一句话确定资源是从哪一环断掉的。我从这次故障里总结出一组快速定位命令按顺序执行就能把链路摸清楚。# 1. 先看节点Allocatable里资源键还在不在 kubectl describe node cn-gpu-02 | sed -n /Allocatable/,/System Info/p # 2. 看Device Plugin Pod是否在目标节点上Running kubectl get pods -n kube-system -o wide | grep -i device # 3. 看CSI Node组件是否在目标节点上Running kubectl get pods -n csi-system -o wide | grep -i node # 4. 如果Pod Pending直接describe看调度事件 kubectl describe pod -n csi-system csi-node-pod-name | grep -A10 Events # 5. 看kubelet日志里有没有device plugin摘除的记录 journalctl -u kubelet --since-30m | grep -i device plugin | tail -50这套组合拳打下来资源消失的链路基本就清楚了大概率不是kubelet不认账而是Device Plugin压根没跑在节点上。做运维排查的时候最怕的就是在节点状态这个表层现象上反复折腾忘记去看底层的Pod调度状态。3. 根因平台组件被污点拒之门外CSI挂载只是第二个受害者3.1 污点的三种效果以及DaemonSet默认的容忍边界K8s的污点Taint和容忍Toleration机制本质上是节点挑选Pod的反向筛选。节点可以打上污点Pod必须声明对应的容忍才能被调度上去。污点有三种效果差别很关键NoSchedule不允许新Pod调度上来已经在跑的Pod不受影响。PreferNoSchedule尽量不调度但不是硬限制。NoExecute新Pod不能调度已经运行且不满足容忍的Pod会被驱逐。我们这次打的是dedicatedrdma:NoSchedule目的是让RDMA节点池只给特定业务用。业务方确实在Deployment里加了匹配的tolerations所以他们的训练任务能正常调度。问题出在平台组件身上。很多人不知道一个细节DaemonSet创建的Pod默认只容忍一小部分系统污点比如node.kubernetes.io/not-ready、node.kubernetes.io/unreachable、node.kubernetes.io/disk-pressure这类。对于用户自定义的污点比如dedicatedrdmaDaemonSet默认是没有任何容忍的。也就是说节点打上自定义污点之后DaemonSet的新Pod照样会被调度器拒之门外。这和它是不是平台组件没关系和它是不是DaemonSet也没关系——调度器不认平台组件这个概念它只看容忍规则。3.2 为什么打污点后一切正常重启后才会爆炸这是整个故障里最迷惑人的地方也是时间差陷阱的核心。打污点用的是NoSchedule对于已经在节点上运行的Pod它不驱逐。所以打污点那一刻平台上所有DaemonSet都还在原地好好跑着Device Plugin还在上报CSI Node也还在一切都正常。这时候如果有人验证一下资源甚至会得出打污点不影响平台组件的错误结论。真正的爆炸点发生在这些平台Pod因为某种原因需要重建的时候。我们的场景是节点维护重启后或者DaemonSet滚动更新镜像后旧Pod被终止新Pod要调度到节点上——结果发现节点上有污点新Pod进不来。节点上再也没有Device Plugin在跑了资源清零也没有CSI Node在跑了挂载失败。这个时间差把问题伪装成了重启后资源丢失让人很容易往驱动、硬件、初始化顺序的方向去查。实际上呢是节点重启后平台组件再也回不来了。如果当时打的是NoExecute问题会立刻暴露反而好查很多。NoSchedule这种温和的方式恰恰是最坑的——它会延迟故障爆发直到某个后续触发条件出现。3.3 平台组件完整清单除了Device Plugin还有谁可能中招这次故障里我们损失了两个平台组件但巡检的时候我顺手把整个集群的平台组件都过了一遍发现中招名单远比想象中长。只要是DaemonSet部署、需要跑在专用节点上的组件都可能踩同一个坑。我把常见的平台组件按缺失后的表现整理成了表格方便对照排查组件类型典型部署方式缺失/不可用时的表现RDMA Device PluginDaemonSet扩展资源从Allocatable消失调度器无法分配设备CSI Node DriverDaemonSetPod挂载卷超时kubelet报DeadlineExceededCNI网络插件DaemonSetPod网络初始化失败容器无IP或路由异常监控ExporterDaemonSet节点指标缺失告警图表出现空洞日志采集AgentDaemonSet节点日志断层采集端延迟升高本地存储ProvisionerDaemonSetLocal PV卷无法创建PVC卡Pending表格里每一行对应的都是一类真实故障场景。尤其是CNI插件如果被污点排斥在节点之外那整个节点的Pod都起不来表面上看问题会更大。这次还算幸运CNI没放在RDMA专属节点池上。3.4 拉出日志和事件把证据链补齐为了让根因更扎实我把三条证据链分别拉了出来第一条是调度器的事件。CSI Node Pod的Pending事件明明白白写着had untolerated taint这是最直接的证据。第二条是kubelet的挂载日志。在目标节点上执行journalctl -u kubelet能看到它一直在重试连接CSI的Socketfailed to mount volume pvc-xxxx (volumeAttachmentName ...) maybe the CSI driver isnt installed on the node? socket path /var/lib/kubelet/plugins/csi.xxx.com/csi.sock: no such file or directorysocket不存在这句很关键——不是连接超时是压根没有进程在那里监听。这直接指向节点上少了CSI Node组件而不是CSI服务本身卡壳。第三条是Device Plugin的日志。插件Pod不在目标节点上但它在其他节点的日志是正常的注册流程、设备列表上报都完好。这排除了Devcie Plugin代码层面的问题把矛盾集中在Pod根本不在目标节点这一个事实。三条证据把故障链路完整闭合节点污点 - 平台DaemonSet无容忍 - 组件无法调度到目标节点 - Device Plugin缺失导致资源消失、CSI Node缺失导致挂载失败。4. 修复补容忍度、管好调度策略让平台组件随时可调度4.1 先止血给CSI和Device Plugin的DaemonSet打patch故障定位到这一步修复其实很直接。先止血把两个平台的DaemonSet补上容忍配置。给CSI Node Driver的DaemonSet打补丁kubectl -n csi-system patch daemonset csi-node-plugin -p { spec: { template: { spec: { tolerations: [ { key: dedicated, operator: Equal, value: rdma, effect: NoSchedule }, { key: dedicated, operator: Equal, value: rdma, effect: NoExecute, tolerationSeconds: 300 } ] } } } }Device Plugin的DaemonSet用同样的方式处理。补完容忍后DaemonSet controller会立刻在目标节点上创建新Pod。这时候你会看到两个有意思的现象Device Plugin Pod调度上去后几秒钟之类rdma/hca资源重新出现在节点Allocatable里。CSI Node Pod就绪后卡在ContainerCreating的业务Pod开始正常完成挂载进入Running。整个过程不需要重建节点也不需要重启任何业务。平台组件一回来资源上报和存储挂载自动恢复。4.2 标准配置精确容忍比operator: Exists更可控止血之后要做的是把这种修复固化成模板。这里我要强调一个设计选择容忍度建议精确匹配而不是无脑容忍一切。有些发行版默认给系统组件配operator: Exists表示容忍节点上的所有污点。这样做的确一劳永逸但副作用很隐蔽平台组件会调度到任何带污点的节点上包括控制平面节点、GPU独占节点、其他业务专属节点。一旦平台组件的nodeSelector写得不够精确就可能出现平台组件污染专用节点的情况反而不利于资源隔离。我建议用精确匹配的方式而且要和节点的污点约定对齐tolerations: - key: dedicated operator: Equal value: rdma effect: NoSchedule - key: dedicated operator: Equal value: rdma effect: NoExecute tolerationSeconds: 300这里的tolerationSeconds: 300值得单独说一句。NoExecute污点如果被永久容忍意味着即使节点被标记为需要驱逐平台组件也会被强制留下。为了安全NoExecute的容忍设一个有限的秒数比如300秒这样平台组件在节点故障时能相对平滑地撤离、重启而不是死磕在一个坏节点上。对应的DaemonSet的nodeSelector或nodeAffinity也应该同步约束确保它只跑在真正需要它的节点上。比如Device Plugin只会跑在带rdma.xxx.com/enabledtrue标签的节点上CSI Node插件只跑在存储可达的节点上。容忍和亲和性两者结合起来才能既进得去又不乱跑。4.3 治本平台组件调度规则的变更预检修好组件之后我牵头做了一个很小的治理动作写一个调度预检脚本在给节点打污点之前先检查这个节点上所有DaemonSet的容忍度是否覆盖新的污点。脚本的核心逻辑很简单#!/usr/bin/env bash # 节点名作为入参 node$1 # 取出节点上已有的污点 taints$(kubectl get node $node -o jsonpath{.spec.taints[*]}) echo Node $node taints: $taints # 遍历集群里所有DaemonSet打印名称和tolerations人工核对 for ds in $(kubectl get ds -A -o jsonpath{range .items[*]}{.metadata.namespace}/{.metadata.name}{\n}{end}); do ns${ds%/*} name${ds#*/} echo ---- $ds ---- kubectl -n $ns get daemonset $name -o jsonpath{.spec.template.spec.tolerations} | python3 -m json.tool done脚本本身不拦截操作但它把打污点前要确认平台组件容忍度这件事变成了一个明确的动作。跑一遍谁没配置容忍、谁会被新污点挡住一目了然。更进一步的治理方案是把所有平台组件的调度策略收敛到一套统一的values配置文件里。比如用Helm或者ArgoCD管理这些DaemonSet把tolerations、nodeSelector、affinity统一声明禁止临时手动改。这样以后调整节点池策略只需要改一个配置入口不会再有某个组件的容忍度漏配。5. 经验把平台组件调度状态纳入每一次节点变更的必查项5.1 关键监控指标别等业务投诉才发现组件离线这次故障最让我后怕的地方在于从平台组件掉线到业务明显报错中间有将近一个小时的窗口期。这一小时里监控面板上居然没有一条告警主动跳出来。原因是我们的告警主要围绕节点状态、Pod状态、业务可用性来设缺少对平台组件在特定节点上缺失的感知。kubectl get pods看起来一切正常因为DaemonSet在其他节点上都是Ready的只有目标节点是Pending——这种局部空缺很难被全局视图发现。事后我加了几个针对性的告警规则核心思路是盯住DaemonSet的Ready数量是否小于Desired数量- alert: DaemonSetPodUnavailable expr: | sum(kube_daemonset_status_desired_number_scheduled) - sum(kube_daemonset_status_number_ready) by (namespace, daemonset) 0 for: 5m labels: severity: warning annotations: summary: DaemonSet {{ $labels.namespace }}/{{ $labels.daemonset }} 存在未就绪Pod另外针对RDMA这类扩展资源我还加了资源数量突降的告警。监控kube_node_status_allocatable里扩展资源键的变化一旦从大于0变成0立即触发告警。这个告警的价值在于它能提前于业务报错发现问题毕竟资源消失到业务Pod开始失败往往还有一段缓冲时间。5.2 打污点/重启节点前5分钟自查清单经验都是拿事故换来的。这次排障之后我把节点变更相关操作沉淀成了一份自查清单每次动节点都要过一遍。清单不长但能拦住大部分同类问题给节点打污点前先跑一遍上面的预检脚本确认节点上所有DaemonSet的容忍度覆盖新污点。更新平台组件Device Plugin、CSI、CNI镜像前确认目标节点池当前有哪些污点镜像里的DaemonSet配置是否匹配。节点重启维护后不要只看节点状态是Ready就收工要确认关键DaemonSet Pod在节点上处于Running状态。任何节点状态正常但业务异常的情况第一件事跑kubectl get pods -A -o wide | grep Pending把Pending的Pod当成最高优先级线索。这份清单看起来普通但每一条背后都是一次真实的线上故障。尤其最后一条排查效率的提升是实打实的——从逐个检查硬件、驱动、存储到全集群扫一遍Pending Pod定位速度完全不是一个量级。5.3 如果重来一次我会怎么快速定位复盘的时候我一直在想如果下次再遇到类似的资源消失类问题我应该怎么在最短时间内抓住要害。排掉运气成分最优路径大概是这样的第一步看节点的Allocatable确认资源是数量变少了还是键值直接消失。数量变少倾向硬件故障键值消失基本可以断定Device Plugin上报链路断了。第二步立刻检查对应平台组件的Pod是否在目标节点上。这一步可以和第一步同时进行因为扩展资源消失和DaemonSet Pod丢失在时间上往往是同步的。第三步如果平台Pod不在目标节点别犹豫直接看它的调度事件十有八九是污点或者亲和性规则排斥。这一步已经足够定位根因不会再有时间去折腾驱动和硬件。这三步做完最短可能只需几分钟就能确认故障方向。回头看看我们当时的做法——先查硬件、再看驱动、然后重启插件——等于在错误的层次上浪费了大半天。调度层面的问题就要用调度层面的手段去解决。最后再说一个实操层面的小技巧。处理这类平台组件被污点排斥问题时给DaemonSet补容忍只是第一步补完之后记得看一眼DaemonSet的滚动更新状态和Pod分布。我在实际修复中就遇到过补完容忍但部分节点因为镜像拉取失败依然Pending的情况那是另一个坑了。平台组件的运维本质上还是DaemonSet运维节点状态、资源占用、镜像分发每一项都值得盯住。这次故障给我最大的教训就是平台组件在你不需要它的时候永远沉默一旦你动了节点、动了污点、动了调度策略它就会用最隐蔽的方式让你付出代价。
返回列表