一、Pod 的一生:從 API 到 cgroup 的“四幕劇”
要理解為何需要強制刪除,必須先回顧 Pod 的正常生命周期。
第一幕,API 層:用戶在控制平面創建對象,etcd 寫入記錄,調度器賦值得 nodeName。
第二幕,節點層:kubelet 通過 watch 感知綁定事件,調用容器運行時創建 sandbox,配置網絡插件,掛載存儲,啟動業務容器,寫入探針。
第三幕,運行期: readiness 決定 Service 是否把流量放進來,liveness 決定 kubelet 是否重啟容器;同時,控制器持續比較“期望副本數”與“實際 Pod 數”,隨時補位或縮容。
第四幕,刪除期:用戶提交刪除,API 給 Pod 打上 deletionTimestamp,若存在優雅期則等待,kubelet 收到“下線”通知,執行 preStop、發送 SIGTERM、等待 gracePeriod,最終調用運行時刪除容器,卸載卷,通知 API 移除 finalizer,對象從 etcd 消失。
任何一幕“卡帶”,都會導致 Pod 在控制平面“陰魂不散”。強制刪除就是強行把第四幕的幕布拉下,不管臺下是否還有演員沒卸妝。
二、優雅刪除的“暗礁”:為什么正常流程會擱淺
1. kubelet 失聯:節點網絡抖動、進程僵死、甚至主板故障,導致 API 無法收到 Pod 已卸載的確認。
2. finalizer 陷阱:自定義控制器、存儲驅動、網絡策略都在對象上注冊“清理鉤子”,一旦控制器自身 bug 或外部系統不可用,finalizer 永不清空。
3. 存儲卷未卸載:分布式存儲出現“掛載泄漏”,節點側持續返回“volume is still in use”,kubelet 不敢確認刪除完成。
4. 網絡端點延遲:Service 控制器與端點切片控制器版本不匹配,導致 IP 一直掛在 endpoints 列表,Pod 被誤認為仍在提供服務。
5. 內核級死鎖:容器運行時與 low-level 運行時(如 runc)在銷毀 cgroup 命名空間時互相等待,SIGKILL 發不下去。
上述場景里,API 層已經“死心”,節點層卻“死撐”,于是對象永遠停在 terminating。強制刪除的底層邏輯,就是越過節點確認,直接讓 API 層“死心也死身”。
三、強制刪除的“手術刀”到底切在哪里
命令行里那條看似普通的指令,實際做了三件非常“暴力”的事:
1. 立即清空 finalizers 數組——不管上面掛了多少鉤子,一律視為完成;
2. 將 gracePeriod 覆蓋為 0——告訴 API 不需要再等待 kubelet 的“尸體確認”;
3. 向 etcd 發起一個無條件刪除事務——繞過 kubelet 的異步匯報。
結果:對象瞬間從 etcd 消失,調度器、Service、控制器再也看不到它。但“靈魂”若未走遠,就可能留下“幽靈進程”——容器仍在節點上跑,IP 仍被占用,卷仍被鎖定。這也是強制刪除后必須二次巡檢的原因。
四、節點側“幽靈”:容器運行時為何收不到死訊
API 記錄被抹除,kubelet 卻可能因 watch 斷開或本地緩存延遲,未感知到“強制”事件。于是節點側繼續按舊緩存管理容器。更糟的是,重啟 kubelet 后會重新同步 Pod 列表,發現“本機有容器但 API 無記錄”,把它當成“孤立容器”殺掉——但如果節點此后不再重啟,容器就可能永遠活著。
解決方案:
- 登錄節點,手動調用運行時刪除 sandbox;
- 用系統工具卸載掛載點、清理網絡插件的 veth pair;
- 若容器處于未知狀態,直接通過進程命名空間發送不可屏蔽信號。
只有完成這三步,才能算“物理死亡”。
五、存儲卷“孤兒”:PV 為何遲遲無法 Released
強制刪除常把 Pod 從 API 層抹掉,但并未觸發 kubelet 的 unmount 序列,導致節點側繼續持有掛載句柄。部分存儲驅動依靠 kubelet 的 VolumeManager 狀態來調用 NodeUnpublish / NodeUnstage,一旦 kubelet 收不到 Pod 對象,就永遠不會執行卸載,PV 停留在 Released 之外的“Unknown”狀態。
排障路徑:
1. 查看節點 `/proc/mounts`,確認掛載點是否仍在;
2. 手動卸載,并觸發驅動提供的 force-detach 接口;
3. 若使用本地 pv,還需清理 `local-volume-provisioner` 的符號鏈接;
4. 最后編輯 PV,移除 `kubernetes.io/pv-protection` finalizer,讓回收流程繼續。
任何一步偷懶,都會讓下一次調度到新節點的 Pod 無法重新掛載,呈現“多節點同時讀同一塊盤”的驚險畫面。
六、網絡“僵尸”:IP 泄漏與 Service 端點堆積
即使容器已死,IP 地址仍可能掛在網絡插件的分布式存儲里。部分舊版本插件依靠 Pod delete 事件觸發 IP 回收,強制刪除繞過了正常事件流,導致 IP 被標為“已分配”,卻找不到對應 Pod,新 Pod 再來時只能拿到不同段地址,集群碎片化悄然發生。
巡檢命令:
- 查看網絡插件的 IPAM 記錄,對比 API 層 Pod IP 列表;
- 對泄漏項手動調用釋放接口;
- 若集群規模龐大,可寫腳本批量比對,定期收斂。
Service 端點側同理,若端點控制器延遲,需手動刪除對應的端點對象,讓新 Pod 正常注冊。
七、二次傷害:濫用強制刪除的“連鎖雪崩”
1. 狀態ful 應用: Operator 依賴 Pod 名做分布式選主,強制刪除后新 Pod 立即復用同名,舊主進程仍在節點上寫數據,出現“雙主”腦裂。
2. 批處理任務: CronJob 根據 Pod 完成狀態決定是否發起下一次任務,強制刪除讓狀態丟失,可能導致重復跑或永不跑。
3. 集群級垃圾回收: 控制器以為副本已下降,繼續擴容,結果節點側幽靈進程占用端口,新 Pod 起不來,呈現“越擴容越不可用”的詭異曲線。
箴言:強制刪除不是“一鍵重啟”,而是“截肢手術”。術前必須確認止血點,術后必須清創,否則感染會迅速蔓延。
八、止血與清創:強制刪除后的標準化巡檢清單
1. 確認對象已從 etcd 消失:`get pod` 返回 404。
2. 登錄原節點,檢查容器列表是否仍存在該 Pod sandbox,若存在則手動刪除。
3. 查看 `/proc/mounts`,確認無殘留掛載,若有則手動卸載。
4. 調用網絡插件診斷工具,確認 IP 已回收。
5. 檢查 PV 狀態,若處于 “Terminating” 或 “Failed”,按存儲驅動文檔強制解綁。
6. 觀察控制器日志,確認無“重復創建”或“副本數不一致”錯誤。
7. 若涉及有狀態服務,人工介入選主流程,確保新 Pod 真正拿到領導鎖。
把以上步驟寫成腳本或 Ansible Playbook,強制刪除后一鍵執行,才能把“截肢”變成“縫合”。
九、治本:如何減少下一次強制刪除的沖動
1. 為所有自定義控制器實現“可觀測的 finalizer”:在 CRD 狀態里記錄清理進度,方便運維一眼看出卡在哪一步。
2. 給節點加“優雅下線”鉤子:系統關機前自動驅逐 Pod,并確保容器運行時完成卸載。
3. 存儲驅動開啟“掛載懸浮檢測”:定期掃描 `/proc/mounts` 與內部記錄差異,主動回收孤兒卷。
4. 網絡插件升級到事件驅動型:不再單純依賴 Pod delete 事件,而是對比運行態與 API 態,自動收斂。
5. 在 CI 階段注入“節點失聯”混沌測試:讓控制器習慣“節點突然蒸發”,提前暴露 finalizer 死鎖問題。
治本的核心是“讓正常流程足夠健壯”,而不是“讓強制刪除更方便”。當優雅路徑始終暢通,就沒有人愿意再走鋼絲。
十、寫在最后:敬畏“終態”背后的分布式契約
強制刪除是一張單程票,一旦撕下,就再也回不到“優雅”的起點。它讓你暫時擺脫terminating的煎熬,卻把風險分散到節點、存儲、網絡、控制器甚至業務邏輯的每一個角落。真正的專業素養,不在于熟練使用這條命令,而在于讓這條命令失去用武之地:通過可觀測性提前發現 finalizer 堆積,通過混沌工程提前暴露節點失聯,通過自動化腳本把術后清創做成例行公事后,你會發現——“強制刪除”不再是救火利器,而是歷史課本里一段“集群幼年期的黑暗傳說”。愿你在下一次手指懸在回車鍵上方時,想起的不只是眼前卡住的 Pod,還有那條被一刀切斷卻仍在節點側呼吸的幽靈進程,以及它可能引發的雪崩。分布式系統沒有“萬能重啟”,只有“可控自愈”。讓時間回到業務,讓平衡歸于終態,才是我們對集群最大的溫柔。