
那天下午我是被告警拉起来的一批训练任务和数据库实例几乎同时卡在ContainerCreating和Pending消息记录里到处是 RDMA 连接失败和 CSI 挂载超时的字样。群里第一反应是“存储集群挂了”有人已经开始联系机房值班查链路。说实话看到这类组合报错正常的直觉确实是先往存储端想。但那次不一样——我登录节点用ibstat检查 RDMA 网卡物理状态链路明明是正常的存储服务端的日志也干干净净。真正的问题藏在一个完全想不到的地方节点上的 RDMA 扩展资源在小半天前突然变成了 0而 CSI 挂载失败只是它引发的一连串“次生灾害”。这次排障最终指向了平台组件的污点Taint与容忍Toleration配置一句话总结就是维护节点时留下的一个污点没有回收导致承载 RDMA 资源上报和 CSI 存储插件的 DaemonSet 被驱逐后一直回不来。1. 故障现象复盘挂载报错先于告警排查却差点被带偏到存储端1.1 两类报错同时出现Pending 与 FailedMount 的现场当时集群里同时出现了两类完全不同的 Pod 状态这也是最初让所有人困惑的地方。第一类是依赖 RDMA 设备的数据处理任务。kubectl describe pod里看到的事件是典型的资源不足Events: Type Reason Age From Message ---- ------ ---- ---- ------- Warning FailedScheduling 3h default-scheduler 0/120 nodes are available: 20 node(s) insufficient rdma/rdma_shared_device.调度器明确告诉你有 20 个节点缺 RDMA 扩展资源。可是这批节点的网卡从硬件上看都活着这个“资源不足”从哪儿来的第二类是需要挂载并行存储卷的普通业务 Pod它们的报错更直接MountVolume.SetUp failed for volume pvc-f3a9c...: rpc error: code Unknown desc mount failed: failed to connect to storage server via rdma: no such device这个no such device很关键它是从存储挂载插件里抛出来的意思是插件在节点上找不到可用的 RDMA 设备。注意这里说的“设备”不一定是物理网卡消失而是某个上层可见的设备对象没了。两种现象有一个共同指向节点上的 RDMA 能力在某个层面上“消失”了。但一个是调度器层面感知不到一个是存储插件执行挂载时感知不到两条线索看起来相关又没有一个统一的解释。1.2 第一轮误判存储服务端查了个底朝天因为 CSI 挂载失败的报错太显眼大家第一轮排查基本都围着存储转。先看存储服务端进程正常看存储集群的 RDMA 监听端口正常用节点去 ping 存储的服务 IP通检查存储端的认证和导出配置没问题。甚至怀疑是不是存储 CSI Controller 的版本出了兼容性问题把 controller 重启过一轮没用。后来有人提议“既然是挂载时连不上 RDMA那去节点上看看网卡”于是值班同事登录故障节点跑了一遍ibstat返回的端口状态是Active链路速率、带宽都正常。物理层完全健康存储服务端也没毛病那这个no such device到底哪来的这里就得承认我们一开始把“CSI 挂载失败”当成了独立故障在查完全忽略了另一个正在发生的现象一批需要 RDMA 资源的任务已经 Pending 三个小时了。把这两个现象放一起看思路才打开。1.3 转折点节点眼里 RDMA 资源凭空消失了真正让排查转向的是一条很简单的命令。某位同事对故障节点执行了kubectl describe node cn-storage-07 | grep -A 12 Capacity历史截图里这个节点原本应该带着类似rdma/rdma_shared_device: 32这样的扩展资源但当时 Capacity 列表里压根没有这项。也就是说这个节点向 Kubernetes 集群上报的 RDMA 能力已经从调度器的“库存账本”里删掉了。硬件健康、上报消失中间只隔着一个东西节点上的 RDMA Device Plugin。它不运行kubelet 就拿不到设备的数量和健康状态自然会把资源从 Capacity 里移除。顺着这个思路去查platform命名空间里的 DaemonSet我当场愣住了——故障节点上根本没有对应的 Device Plugin Pod它的状态是0/1 nodes are available: 1 node(s) had untolerated taint {node.maint/rdma-check: true}到这里问题的性质就完全变了不是存储坏了也不是网卡坏了而是平台组件因为污点容忍配置不够被驱逐之后没能回到节点上。而这个组件一消失RDMA 资源上报消失连带 CSI 存储插件也瘫痪。2. RDMA 资源是如何“消失”的Device Plugin 上报链路拆解2.1 节点上的 RDMA 能力靠什么进入集群视野先说一个底层事实Kubernetes 本身不认识“RDMA 网卡”这种设备。它只认识 CPU、内存、GPU 这类内置资源像 RDMA 网卡、FPGA、SR-IOV VF 这类硬件必须借助扩展资源Extended Resource机制才能进入集群的资源池。整个链路是这样的节点上的 Device Plugin 以 DaemonSet 或裸 Pod 形式运行启动后通过 Unix Socket 跟 kubelet 通信告诉 kubelet“我这台节点上有多少块 RDMA 网卡、每块有多少可用队列”。kubelet 收到上报后把这个数字写进节点对象的status.capacity和status.allocatable里调度器再去读取这些字段做 Pod 分配。拿日常场景类比Device Plugin 是仓库的报关员kubelet 是仓库管理系统调度器是接单系统。报关员不上班仓库系统里当然查不到这批货。所以一个很容易被忽略的事实是RDMA 资源在 Kubernetes 里的可见性完全依赖 Device Plugin 这个中间代理的存活状态。它活着资源就在它死了哪怕网卡插在机器上发光发热调度器也只会认为这台节点没有 RDMA。2.2 上报者消失后资源被删除硬件明明健康账本却清零那 Device Plugin 挂掉之后kubelet 是立刻清除资源还是会有延迟这取决于 kubelet 的 Device Manager 的监听机制。Device Plugin 与 kubelet 之间通过 gRPC 维持心跳连接插件同时会通过ListAndWatch接口持续上报设备状态。一旦插件进程退出、Unix Socket 断开kubelet 的 Device Manager 会在一段很短的时间内通常是几秒到十几秒把它注册的设备全部标记为不可用并在下一轮节点状态上报时把对应的 Extended Resource 从capacity和allocatable中移除。这也是为什么describe node里 Capacity 会“凭空消失”。不是 kubelet 出错而是它的工作逻辑决定了它只相信自己能连通获取信息的那些设备。2.3 一个命令还原全过程如果只是想快速确认“资源消失是否因为 Device Plugin 不在”其实不用绕太多# 1. 确认节点当前容量 kubectl get node cn-storage-07 -o jsonpath{.status.capacity.rdma\.io/rdma_shared_device} # 输出为空或 0 # 2. 查看 Device Plugin 的 Pod 分布 kubectl -n platform get pod -o wide -l apprdma-device-plugin # 发现目标节点没有 Running 的 Pod # 3. 查看 DaemonSet 的调度事件 kubectl -n platform describe pod pending-pod-name | tail -10 # 关键信息untolerated taint我自己复盘的时候觉得这套逻辑链并不复杂难的是在纷乱的挂载错误信息里想到“该去看 Device Plugin”。因为从故障表现看存储挂载失败才是“主诉”谁会一开始想到去查一个默默无闻的 DaemonSet 呢3. 根因定位维护污点没回收DaemonSet 容忍范围成了盲区3.1 维护操作留下的 Taint定位到 Device Plugin 缺失后下一步是搞清它为什么不在节点上。我们知道 DaemonSet 本来就应该在每个匹配标签的节点上保持一个 Pod出现“节点有标签但 Pod 不在”的情况只有两类原因节点被打了污点Taint而 DaemonSet 的容忍Toleration列表里没有对应的项节点标签被修改导致节点不再匹配 DaemonSet 的 nodeSelector。先看节点标签没问题。再看污点kubectl describe node cn-storage-07 | grep -A 4 Taints输出里赫然写着Taints: node.maint/rdma-checktrue:NoExecute这是一个平台自定义的维护污点作用是排空节点上所有不容忍它的 Pod然后强制驱逐。从污点 key 的名字看八成是之前做 RDMA 链路检修时加的当时为了不让业务 Pod 继续往这些节点调度运维用自动化脚本给整批存储节点打了这个污点并触发驱逐。问题在于检修完成后脚本只做了“确认链路恢复正常”没有回收污点。于是这批节点一直带着NoExecute污点运行所有默认容忍列表里没有它的 DaemonSet Pod 都被驱逐干净并且无法再被调度回来。3.2 平台组件为什么回不来容忍配置的盲区这里要说一下 Taint/Toleration 的工作机制方便没基础的朋友理解。节点打了NoExecute污点后kubelet 会立即驱逐节点上所有没有对应容忍的 Pod而且以后调度器也不会再把新 Pod 放到这个节点上。想要让某个 Pod 留在节点上只能在 Pod 或 DaemonSet 的tolerations里显式声明“我不怕这个污点”。我们平台的 RDMA Device Plugin 和 CSI Node 插件都是 DaemonSet它们的容忍配置写的是这样tolerations: - key: node-role.kubernetes.io/control-plane operator: Exists effect: NoSchedule - key: node.kubernetes.io/not-ready operator: Exists effect: NoExecute覆盖了控制面污点和通用的节点不可用污点但谁也没想到去容忍node.maint/rdma-check这种自定义维护污点。于是发生了一连串连锁反应维护脚本给节点加上node.maint/rdma-checktrue:NoExecutekubelet 立刻驱逐节点上所有没有容忍的 Pod包括 RDMA Device Plugin 和 CSI Node 插件的 PodDaemonSet 控制器尝试重建 Pod但调度器发现节点仍有不容忍的污点Pod 一直Pendingkubelet 检测到 Device Plugin Socket 断开把rdma/rdma_shared_device资源从 Capacity 中移除CSI Node 插件不在节点kubelet 挂载存储卷时调用不到插件报no such device。3.3 完整因果链从一条 YAML 到整个集群故障把因果链写出来之后整个故障其实非常“工程化”没有任何一个环节是玄学环节状态节点 RDMA 物理网卡健康ibstat正常节点 Taintnode.maint/rdma-checktrue:NoExecute未回收RDMA Device Plugin DaemonSet容忍列表缺项Pod 被驱逐后无法回归kubelet Device Manager断开插件连接后自动移除扩展资源调度器看到的节点 Capacityrdma/rdma_shared_device消失CSI Node 插件 DaemonSet同样容忍缺项Pod 不在故障节点CSI Controller 或存储服务端健康无辜背锅污点机制本身是个好东西它提供了一种优雅的“节点排空”手段。但这次事故也暴露了一个很现实的问题污点是运行时被外部运维脚本动态加上的而容忍是静态写在 DaemonSet 模板里的。两者一旦不同步平台基础组件就成了最先遭殃的受害者。4. 修复实操补齐容忍、恢复组件、验证挂载4.1 给 DaemonSet 补上容忍的正确写法定位到根因之后修复其实不复杂给两个受影响的 DaemonSet 都补上对node.maint/rdma-check的容忍。要注意这里不能只改一个Device Plugin 和 CSI Node 插件都必须覆盖否则要么资源恢复了但挂载还是失败要么反过来。我在实际修改时用的是kubectl -n platform edit ds/rdma-device-plugin在spec.template.spec.tolerations里追加tolerations: - key: node.maint/rdma-check operator: Exists effect: NoExecute细心的朋友可能会问为什么用operator: Exists而不是指定value: true两个都行但Exists更稳妥。它表示“只要这个 key 存在不管 value 是什么我都容忍”适合这类维护污点——因为你不知道下次维护脚本会不会把 value 改成false或者maintenance。如果写成具体的value: true以后污点 value 一换又要改一轮 YAML没必要。同样的操作给storage-csi-node这个 DaemonSet 也来一遍然后触发滚动更新kubectl -n platform rollout restart ds/rdma-device-plugin ds/storage-csi-node4.2 恢复顺序很重要先组件后业务很多同行遇到这类故障手一快就去删 Pending 的业务 Pod想让它重新调度。但在资源还没恢复的时候删调度器依然找不到带 RDMA 的节点Pod 会继续 Pending没有任何意义。正确的顺序是“先让平台组件重新站起来再恢复业务”。我用kubectl -n platform get pod -o wide -l apprdma-device-plugin观察新 Pod 的调度情况。因为容忍已经补齐Pod 很快就安排到了原本被污点挡在外面的存储节点上状态转成Running。注意一个小细节Device Plugin Pod 起来之后kubelet 重新通过 Socket 完成设备注册和上报这个过程中节点 Capacity 不会立刻变化。我大概等了十几秒再检查kubectl get node cn-storage-07 -o jsonpath{.status.capacity.rdma\.io/rdma_shared_device}输出从空变成了32说明资源重新回到了调度器的账本里。CSI Node 插件也处于Running问题节点的CSINode对象重新被注册kubelet 有办法调用挂载操作了。4.3 验证清单Capacity 恢复与 CSI 挂载成功率业务恢复不是清一波 Pending 就完事我习惯按下面这套清单逐项验证验证项命令预期结果Device Plugin Pod 就绪kubectl -n platform get pod -l apprdma-device-plugin -o wide故障节点上有 Running 且 Ready 的 PodCSI Node 插件就绪kubectl -n platform get pod -l appstorage-csi-node -o wide故障节点上有 Running 且 Ready 的 Pod节点扩展资源恢复kubectl get node cn-storage-07 -o jsonpath{.status.capacity.rdma\.io/rdma_shared_device}输出等于物理网卡总量如32CSINode 对象恢复kubectl get csinode cn-storage-07 -o jsonpath{.spec.drivers}能看到存储驱动名业务 Pod 挂载成功kubectl get pod business-podRunning 状态事件里无 FailedMount对于已经 Pending 的 Pod等资源恢复后我会删几个代表性地观察确认能调度成功后再批量处理。实际场景里调度器会周期性地做调度重试一部分 Pending Pod 在资源恢复后会自动起来如果等了很久还没动手动删掉触发重建是最直接的。5. 排障方法论沉淀别再被“存储端错误”牵着走5.1 这类故障的共同套路上层错误信息最不可信这次事故给我最大的启发是多层依赖系统里的故障最外层的错误提示往往最具误导性。CSI 挂载失败明明只是一个“结果”却因为报错内容指向存储服务端把大量人力拖进了存储排查的泥潭。类似的事件在 GPU 资源消失、FPGA 资源消失、SR-IOV VF 异常的场景里也会出现。共同套路是上层应用报“设备不存在”或“资源不足”物理硬件实际是健康的问题出在中间层的“资源上报代理”身上。遇到这种“硬件没坏但上层看不见”的矛盾第一时间应该把目光放到资源上报链路上检查对应 Device Plugin 的存活状态而不是先怀疑硬件和存储。5.2 资源消失类问题的十分钟排查手册这几步骤是我踩过坑之后总结出来的分享给同样维护平台的朋友看业务 Pod 事件是FailedScheduling调度器看不到资源还是FailedMount挂载执行层出问题先分清楚看节点 Capacity直接搜扩展资源名确认资源到底有没有从节点上报中消失看 Device Plugin 分布kubectl -n platform get pod -o wide | grep device一眼就能发现故障节点上有没有对应的 Pod看 DaemonSet 的调度事件如果有untolerated taint问题基本就是污点容忍对比节点 Taint 与 DaemonSet Toleration# 快速列出所有节点的污点 kubectl get nodes -o jsonpath{range .items[*]}{.metadata.name}{\t}{.spec.taints}{\n}{end} # 快速查看某个 DaemonSet 的容忍 kubectl -n platform get ds ds-name -o jsonpath{range .spec.template.spec.tolerations[*]}{.key}{:}{.effect}{\n}{end}前后一对照缺哪条补哪条。5.3 平台组件污点容忍治理的三个建议这次事故本质上是变更管理疏漏而不是技术原理问题。维护脚本加了污点却忘了回收这种操作上的坑单纯靠人盯是盯不住的。我后来在团队里推了三件事第一统一维护污点的前缀和生命周期。所有运维脚本的临时污点统一使用某个前缀比如node.maint/并且强制脚本在结束前调用回收逻辑。回收动作要做到幂等哪怕重复执行也不会出错。第二给关键 DaemonSet 建立“容忍覆盖清单”。每一个平台基础组件都要在发布的 YAML 里显式声明支持哪些污点。定期把节点上的实际污点集合与所有 DaemonSet 容忍集合做一次求差集任何新出现的污点如果没有任何容忍项立刻报警。第三变更前做一次“污点常见面测试”。模拟在目标节点上打一个代表未来维护场景的新污点观察核心组件是否还能保持存活。这个操作成本很低但能提前暴露很多配置盲区。这次事故之后我养成了一个有点“反直觉”的习惯每次看到 CSI 挂载失败第一件事不是查存储而是先看节点上的平台组件健不健康、节点资源有没有变化。这个习惯后来帮我快速度过了好几次类似事件。说实话底层平台的稳定性往往就藏在这些不起眼的污点、容忍和资源上报配置里。硬件坏了至少能修能换配置上的盲区如果没人发现它会一直在那里等着某个维护操作把它引爆。