未回收引發(fā)RDMA資源消失與CSI掛載故障排查)
那天下午我是被告警拉起來的一批訓(xùn)練任務(wù)和數(shù)據(jù)庫實(shí)例幾乎同時卡在ContainerCreating和Pending消息記錄里到處是 RDMA 連接失敗和 CSI 掛載超時的字樣。群里第一反應(yīng)是“存儲集群掛了”有人已經(jīng)開始聯(lián)系機(jī)房值班查鏈路。說實(shí)話看到這類組合報錯正常的直覺確實(shí)是先往存儲端想。但那次不一樣——我登錄節(jié)點(diǎn)用ibstat檢查 RDMA 網(wǎng)卡物理狀態(tài)鏈路明明是正常的存儲服務(wù)端的日志也干干凈凈。真正的問題藏在一個完全想不到的地方節(jié)點(diǎn)上的 RDMA 擴(kuò)展資源在小半天前突然變成了 0而 CSI 掛載失敗只是它引發(fā)的一連串“次生災(zāi)害”。這次排障最終指向了平臺組件的污點(diǎn)Taint與容忍Toleration配置一句話總結(jié)就是維護(hù)節(jié)點(diǎn)時留下的一個污點(diǎn)沒有回收導(dǎo)致承載 RDMA 資源上報和 CSI 存儲插件的 DaemonSet 被驅(qū)逐后一直回不來。1. 故障現(xiàn)象復(fù)盤掛載報錯先于告警排查卻差點(diǎn)被帶偏到存儲端1.1 兩類報錯同時出現(xiàn)Pending 與 FailedMount 的現(xiàn)場當(dāng)時集群里同時出現(xiàn)了兩類完全不同的 Pod 狀態(tài)這也是最初讓所有人困惑的地方。第一類是依賴 RDMA 設(shè)備的數(shù)據(jù)處理任務(wù)。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.調(diào)度器明確告訴你有 20 個節(jié)點(diǎn)缺 RDMA 擴(kuò)展資源??墒沁@批節(jié)點(diǎn)的網(wǎng)卡從硬件上看都活著這個“資源不足”從哪兒來的第二類是需要掛載并行存儲卷的普通業(yè)務(wù) 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很關(guān)鍵它是從存儲掛載插件里拋出來的意思是插件在節(jié)點(diǎn)上找不到可用的 RDMA 設(shè)備。注意這里說的“設(shè)備”不一定是物理網(wǎng)卡消失而是某個上層可見的設(shè)備對象沒了。兩種現(xiàn)象有一個共同指向節(jié)點(diǎn)上的 RDMA 能力在某個層面上“消失”了。但一個是調(diào)度器層面感知不到一個是存儲插件執(zhí)行掛載時感知不到兩條線索看起來相關(guān)又沒有一個統(tǒng)一的解釋。1.2 第一輪誤判存儲服務(wù)端查了個底朝天因?yàn)?CSI 掛載失敗的報錯太顯眼大家第一輪排查基本都圍著存儲轉(zhuǎn)。先看存儲服務(wù)端進(jìn)程正常看存儲集群的 RDMA 監(jiān)聽端口正常用節(jié)點(diǎn)去 ping 存儲的服務(wù) IP通檢查存儲端的認(rèn)證和導(dǎo)出配置沒問題。甚至懷疑是不是存儲 CSI Controller 的版本出了兼容性問題把 controller 重啟過一輪沒用。后來有人提議“既然是掛載時連不上 RDMA那去節(jié)點(diǎn)上看看網(wǎng)卡”于是值班同事登錄故障節(jié)點(diǎn)跑了一遍ibstat返回的端口狀態(tài)是Active鏈路速率、帶寬都正常。物理層完全健康存儲服務(wù)端也沒毛病那這個no such device到底哪來的這里就得承認(rèn)我們一開始把“CSI 掛載失敗”當(dāng)成了獨(dú)立故障在查完全忽略了另一個正在發(fā)生的現(xiàn)象一批需要 RDMA 資源的任務(wù)已經(jīng) Pending 三個小時了。把這兩個現(xiàn)象放一起看思路才打開。1.3 轉(zhuǎn)折點(diǎn)節(jié)點(diǎn)眼里 RDMA 資源憑空消失了真正讓排查轉(zhuǎn)向的是一條很簡單的命令。某位同事對故障節(jié)點(diǎn)執(zhí)行了kubectl describe node cn-storage-07 | grep -A 12 Capacity歷史截圖里這個節(jié)點(diǎn)原本應(yīng)該帶著類似rdma/rdma_shared_device: 32這樣的擴(kuò)展資源但當(dāng)時 Capacity 列表里壓根沒有這項(xiàng)。也就是說這個節(jié)點(diǎn)向 Kubernetes 集群上報的 RDMA 能力已經(jīng)從調(diào)度器的“庫存賬本”里刪掉了。硬件健康、上報消失中間只隔著一個東西節(jié)點(diǎn)上的 RDMA Device Plugin。它不運(yùn)行kubelet 就拿不到設(shè)備的數(shù)量和健康狀態(tài)自然會把資源從 Capacity 里移除。順著這個思路去查platform命名空間里的 DaemonSet我當(dāng)場愣住了——故障節(jié)點(diǎn)上根本沒有對應(yīng)的 Device Plugin Pod它的狀態(tài)是0/1 nodes are available: 1 node(s) had untolerated taint {node.maint/rdma-check: true}到這里問題的性質(zhì)就完全變了不是存儲壞了也不是網(wǎng)卡壞了而是平臺組件因?yàn)槲埸c(diǎn)容忍配置不夠被驅(qū)逐之后沒能回到節(jié)點(diǎn)上。而這個組件一消失RDMA 資源上報消失連帶 CSI 存儲插件也癱瘓。2. RDMA 資源是如何“消失”的Device Plugin 上報鏈路拆解2.1 節(jié)點(diǎn)上的 RDMA 能力靠什么進(jìn)入集群視野先說一個底層事實(shí)Kubernetes 本身不認(rèn)識“RDMA 網(wǎng)卡”這種設(shè)備。它只認(rèn)識 CPU、內(nèi)存、GPU 這類內(nèi)置資源像 RDMA 網(wǎng)卡、FPGA、SR-IOV VF 這類硬件必須借助擴(kuò)展資源Extended Resource機(jī)制才能進(jìn)入集群的資源池。整個鏈路是這樣的節(jié)點(diǎn)上的 Device Plugin 以 DaemonSet 或裸 Pod 形式運(yùn)行啟動后通過 Unix Socket 跟 kubelet 通信告訴 kubelet“我這臺節(jié)點(diǎn)上有多少塊 RDMA 網(wǎng)卡、每塊有多少可用隊(duì)列”。kubelet 收到上報后把這個數(shù)字寫進(jìn)節(jié)點(diǎn)對象的status.capacity和status.allocatable里調(diào)度器再去讀取這些字段做 Pod 分配。拿日常場景類比Device Plugin 是倉庫的報關(guān)員kubelet 是倉庫管理系統(tǒng)調(diào)度器是接單系統(tǒng)。報關(guān)員不上班倉庫系統(tǒng)里當(dāng)然查不到這批貨。所以一個很容易被忽略的事實(shí)是RDMA 資源在 Kubernetes 里的可見性完全依賴 Device Plugin 這個中間代理的存活狀態(tài)。它活著資源就在它死了哪怕網(wǎng)卡插在機(jī)器上發(fā)光發(fā)熱調(diào)度器也只會認(rèn)為這臺節(jié)點(diǎn)沒有 RDMA。2.2 上報者消失后資源被刪除硬件明明健康賬本卻清零那 Device Plugin 掛掉之后kubelet 是立刻清除資源還是會有延遲這取決于 kubelet 的 Device Manager 的監(jiān)聽機(jī)制。Device Plugin 與 kubelet 之間通過 gRPC 維持心跳連接插件同時會通過ListAndWatch接口持續(xù)上報設(shè)備狀態(tài)。一旦插件進(jìn)程退出、Unix Socket 斷開kubelet 的 Device Manager 會在一段很短的時間內(nèi)通常是幾秒到十幾秒把它注冊的設(shè)備全部標(biāo)記為不可用并在下一輪節(jié)點(diǎn)狀態(tài)上報時把對應(yīng)的 Extended Resource 從capacity和allocatable中移除。這也是為什么describe node里 Capacity 會“憑空消失”。不是 kubelet 出錯而是它的工作邏輯決定了它只相信自己能連通獲取信息的那些設(shè)備。2.3 一個命令還原全過程如果只是想快速確認(rèn)“資源消失是否因?yàn)?Device Plugin 不在”其實(shí)不用繞太多# 1. 確認(rèn)節(jié)點(diǎn)當(dāng)前容量 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 # 發(fā)現(xiàn)目標(biāo)節(jié)點(diǎn)沒有 Running 的 Pod # 3. 查看 DaemonSet 的調(diào)度事件 kubectl -n platform describe pod pending-pod-name | tail -10 # 關(guān)鍵信息untolerated taint我自己復(fù)盤的時候覺得這套邏輯鏈并不復(fù)雜難的是在紛亂的掛載錯誤信息里想到“該去看 Device Plugin”。因?yàn)閺墓收媳憩F(xiàn)看存儲掛載失敗才是“主訴”誰會一開始想到去查一個默默無聞的 DaemonSet 呢3. 根因定位維護(hù)污點(diǎn)沒回收DaemonSet 容忍范圍成了盲區(qū)3.1 維護(hù)操作留下的 Taint定位到 Device Plugin 缺失后下一步是搞清它為什么不在節(jié)點(diǎn)上。我們知道 DaemonSet 本來就應(yīng)該在每個匹配標(biāo)簽的節(jié)點(diǎn)上保持一個 Pod出現(xiàn)“節(jié)點(diǎn)有標(biāo)簽但 Pod 不在”的情況只有兩類原因節(jié)點(diǎn)被打了污點(diǎn)Taint而 DaemonSet 的容忍Toleration列表里沒有對應(yīng)的項(xiàng)節(jié)點(diǎn)標(biāo)簽被修改導(dǎo)致節(jié)點(diǎn)不再匹配 DaemonSet 的 nodeSelector。先看節(jié)點(diǎn)標(biāo)簽沒問題。再看污點(diǎn)kubectl describe node cn-storage-07 | grep -A 4 Taints輸出里赫然寫著Taints: node.maint/rdma-checktrue:NoExecute這是一個平臺自定義的維護(hù)污點(diǎn)作用是排空節(jié)點(diǎn)上所有不容忍它的 Pod然后強(qiáng)制驅(qū)逐。從污點(diǎn) key 的名字看八成是之前做 RDMA 鏈路檢修時加的當(dāng)時為了不讓業(yè)務(wù) Pod 繼續(xù)往這些節(jié)點(diǎn)調(diào)度運(yùn)維用自動化腳本給整批存儲節(jié)點(diǎn)打了這個污點(diǎn)并觸發(fā)驅(qū)逐。問題在于檢修完成后腳本只做了“確認(rèn)鏈路恢復(fù)正?!睕]有回收污點(diǎn)。于是這批節(jié)點(diǎn)一直帶著NoExecute污點(diǎn)運(yùn)行所有默認(rèn)容忍列表里沒有它的 DaemonSet Pod 都被驅(qū)逐干凈并且無法再被調(diào)度回來。3.2 平臺組件為什么回不來容忍配置的盲區(qū)這里要說一下 Taint/Toleration 的工作機(jī)制方便沒基礎(chǔ)的朋友理解。節(jié)點(diǎn)打了NoExecute污點(diǎn)后kubelet 會立即驅(qū)逐節(jié)點(diǎn)上所有沒有對應(yīng)容忍的 Pod而且以后調(diào)度器也不會再把新 Pod 放到這個節(jié)點(diǎn)上。想要讓某個 Pod 留在節(jié)點(diǎn)上只能在 Pod 或 DaemonSet 的tolerations里顯式聲明“我不怕這個污點(diǎn)”。我們平臺的 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覆蓋了控制面污點(diǎn)和通用的節(jié)點(diǎn)不可用污點(diǎn)但誰也沒想到去容忍node.maint/rdma-check這種自定義維護(hù)污點(diǎn)。于是發(fā)生了一連串連鎖反應(yīng)維護(hù)腳本給節(jié)點(diǎn)加上node.maint/rdma-checktrue:NoExecutekubelet 立刻驅(qū)逐節(jié)點(diǎn)上所有沒有容忍的 Pod包括 RDMA Device Plugin 和 CSI Node 插件的 PodDaemonSet 控制器嘗試重建 Pod但調(diào)度器發(fā)現(xiàn)節(jié)點(diǎn)仍有不容忍的污點(diǎn)Pod 一直Pendingkubelet 檢測到 Device Plugin Socket 斷開把rdma/rdma_shared_device資源從 Capacity 中移除CSI Node 插件不在節(jié)點(diǎn)kubelet 掛載存儲卷時調(diào)用不到插件報no such device。3.3 完整因果鏈從一條 YAML 到整個集群故障把因果鏈寫出來之后整個故障其實(shí)非常“工程化”沒有任何一個環(huán)節(jié)是玄學(xué)環(huán)節(jié)狀態(tài)節(jié)點(diǎn) RDMA 物理網(wǎng)卡健康ibstat正常節(jié)點(diǎn) Taintnode.maint/rdma-checktrue:NoExecute未回收RDMA Device Plugin DaemonSet容忍列表缺項(xiàng)Pod 被驅(qū)逐后無法回歸kubelet Device Manager斷開插件連接后自動移除擴(kuò)展資源調(diào)度器看到的節(jié)點(diǎn) Capacityrdma/rdma_shared_device消失CSI Node 插件 DaemonSet同樣容忍缺項(xiàng)Pod 不在故障節(jié)點(diǎn)CSI Controller 或存儲服務(wù)端健康無辜背鍋污點(diǎn)機(jī)制本身是個好東西它提供了一種優(yōu)雅的“節(jié)點(diǎn)排空”手段。但這次事故也暴露了一個很現(xiàn)實(shí)的問題污點(diǎn)是運(yùn)行時被外部運(yùn)維腳本動態(tài)加上的而容忍是靜態(tài)寫在 DaemonSet 模板里的。兩者一旦不同步平臺基礎(chǔ)組件就成了最先遭殃的受害者。4. 修復(fù)實(shí)操補(bǔ)齊容忍、恢復(fù)組件、驗(yàn)證掛載4.1 給 DaemonSet 補(bǔ)上容忍的正確寫法定位到根因之后修復(fù)其實(shí)不復(fù)雜給兩個受影響的 DaemonSet 都補(bǔ)上對node.maint/rdma-check的容忍。要注意這里不能只改一個Device Plugin 和 CSI Node 插件都必須覆蓋否則要么資源恢復(fù)了但掛載還是失敗要么反過來。我在實(shí)際修改時用的是kubectl -n platform edit ds/rdma-device-plugin在spec.template.spec.tolerations里追加tolerations: - key: node.maint/rdma-check operator: Exists effect: NoExecute細(xì)心的朋友可能會問為什么用operator: Exists而不是指定value: true兩個都行但Exists更穩(wěn)妥。它表示“只要這個 key 存在不管 value 是什么我都容忍”適合這類維護(hù)污點(diǎn)——因?yàn)槟悴恢老麓尉S護(hù)腳本會不會把 value 改成false或者maintenance。如果寫成具體的value: true以后污點(diǎn) value 一換又要改一輪 YAML沒必要。同樣的操作給storage-csi-node這個 DaemonSet 也來一遍然后觸發(fā)滾動更新kubectl -n platform rollout restart ds/rdma-device-plugin ds/storage-csi-node4.2 恢復(fù)順序很重要先組件后業(yè)務(wù)很多同行遇到這類故障手一快就去刪 Pending 的業(yè)務(wù) Pod想讓它重新調(diào)度。但在資源還沒恢復(fù)的時候刪調(diào)度器依然找不到帶 RDMA 的節(jié)點(diǎn)Pod 會繼續(xù) Pending沒有任何意義。正確的順序是“先讓平臺組件重新站起來再恢復(fù)業(yè)務(wù)”。我用kubectl -n platform get pod -o wide -l apprdma-device-plugin觀察新 Pod 的調(diào)度情況。因?yàn)槿萑桃呀?jīng)補(bǔ)齊Pod 很快就安排到了原本被污點(diǎn)擋在外面的存儲節(jié)點(diǎn)上狀態(tài)轉(zhuǎn)成Running。注意一個小細(xì)節(jié)Device Plugin Pod 起來之后kubelet 重新通過 Socket 完成設(shè)備注冊和上報這個過程中節(jié)點(diǎn) Capacity 不會立刻變化。我大概等了十幾秒再檢查kubectl get node cn-storage-07 -o jsonpath{.status.capacity.rdma\.io/rdma_shared_device}輸出從空變成了32說明資源重新回到了調(diào)度器的賬本里。CSI Node 插件也處于Running問題節(jié)點(diǎn)的CSINode對象重新被注冊kubelet 有辦法調(diào)用掛載操作了。4.3 驗(yàn)證清單Capacity 恢復(fù)與 CSI 掛載成功率業(yè)務(wù)恢復(fù)不是清一波 Pending 就完事我習(xí)慣按下面這套清單逐項(xiàng)驗(yàn)證驗(yàn)證項(xiàng)命令預(yù)期結(jié)果Device Plugin Pod 就緒kubectl -n platform get pod -l apprdma-device-plugin -o wide故障節(jié)點(diǎn)上有 Running 且 Ready 的 PodCSI Node 插件就緒kubectl -n platform get pod -l appstorage-csi-node -o wide故障節(jié)點(diǎn)上有 Running 且 Ready 的 Pod節(jié)點(diǎn)擴(kuò)展資源恢復(fù)kubectl get node cn-storage-07 -o jsonpath{.status.capacity.rdma\.io/rdma_shared_device}輸出等于物理網(wǎng)卡總量如32CSINode 對象恢復(fù)kubectl get csinode cn-storage-07 -o jsonpath{.spec.drivers}能看到存儲驅(qū)動名業(yè)務(wù) Pod 掛載成功kubectl get pod business-podRunning 狀態(tài)事件里無 FailedMount對于已經(jīng) Pending 的 Pod等資源恢復(fù)后我會刪幾個代表性地觀察確認(rèn)能調(diào)度成功后再批量處理。實(shí)際場景里調(diào)度器會周期性地做調(diào)度重試一部分 Pending Pod 在資源恢復(fù)后會自動起來如果等了很久還沒動手動刪掉觸發(fā)重建是最直接的。5. 排障方法論沉淀別再被“存儲端錯誤”牽著走5.1 這類故障的共同套路上層錯誤信息最不可信這次事故給我最大的啟發(fā)是多層依賴系統(tǒng)里的故障最外層的錯誤提示往往最具誤導(dǎo)性。CSI 掛載失敗明明只是一個“結(jié)果”卻因?yàn)閳箦e內(nèi)容指向存儲服務(wù)端把大量人力拖進(jìn)了存儲排查的泥潭。類似的事件在 GPU 資源消失、FPGA 資源消失、SR-IOV VF 異常的場景里也會出現(xiàn)。共同套路是上層應(yīng)用報“設(shè)備不存在”或“資源不足”物理硬件實(shí)際是健康的問題出在中間層的“資源上報代理”身上。遇到這種“硬件沒壞但上層看不見”的矛盾第一時間應(yīng)該把目光放到資源上報鏈路上檢查對應(yīng) Device Plugin 的存活狀態(tài)而不是先懷疑硬件和存儲。5.2 資源消失類問題的十分鐘排查手冊這幾步驟是我踩過坑之后總結(jié)出來的分享給同樣維護(hù)平臺的朋友看業(yè)務(wù) Pod 事件是FailedScheduling調(diào)度器看不到資源還是FailedMount掛載執(zhí)行層出問題先分清楚看節(jié)點(diǎn) Capacity直接搜擴(kuò)展資源名確認(rèn)資源到底有沒有從節(jié)點(diǎn)上報中消失看 Device Plugin 分布kubectl -n platform get pod -o wide | grep device一眼就能發(fā)現(xiàn)故障節(jié)點(diǎn)上有沒有對應(yīng)的 Pod看 DaemonSet 的調(diào)度事件如果有untolerated taint問題基本就是污點(diǎn)容忍對比節(jié)點(diǎn) Taint 與 DaemonSet Toleration# 快速列出所有節(jié)點(diǎn)的污點(diǎn) 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}前后一對照缺哪條補(bǔ)哪條。5.3 平臺組件污點(diǎn)容忍治理的三個建議這次事故本質(zhì)上是變更管理疏漏而不是技術(shù)原理問題。維護(hù)腳本加了污點(diǎn)卻忘了回收這種操作上的坑單純靠人盯是盯不住的。我后來在團(tuán)隊(duì)里推了三件事第一統(tǒng)一維護(hù)污點(diǎn)的前綴和生命周期。所有運(yùn)維腳本的臨時污點(diǎn)統(tǒng)一使用某個前綴比如node.maint/并且強(qiáng)制腳本在結(jié)束前調(diào)用回收邏輯?;厥談幼饕龅絻绲饶呐轮貜?fù)執(zhí)行也不會出錯。第二給關(guān)鍵 DaemonSet 建立“容忍覆蓋清單”。每一個平臺基礎(chǔ)組件都要在發(fā)布的 YAML 里顯式聲明支持哪些污點(diǎn)。定期把節(jié)點(diǎn)上的實(shí)際污點(diǎn)集合與所有 DaemonSet 容忍集合做一次求差集任何新出現(xiàn)的污點(diǎn)如果沒有任何容忍項(xiàng)立刻報警。第三變更前做一次“污點(diǎn)常見面測試”。模擬在目標(biāo)節(jié)點(diǎn)上打一個代表未來維護(hù)場景的新污點(diǎn)觀察核心組件是否還能保持存活。這個操作成本很低但能提前暴露很多配置盲區(qū)。這次事故之后我養(yǎng)成了一個有點(diǎn)“反直覺”的習(xí)慣每次看到 CSI 掛載失敗第一件事不是查存儲而是先看節(jié)點(diǎn)上的平臺組件健不健康、節(jié)點(diǎn)資源有沒有變化。這個習(xí)慣后來幫我快速度過了好幾次類似事件。說實(shí)話底層平臺的穩(wěn)定性往往就藏在這些不起眼的污點(diǎn)、容忍和資源上報配置里。硬件壞了至少能修能換配置上的盲區(qū)如果沒人發(fā)現(xiàn)它會一直在那里等著某個維護(hù)操作把它引爆。