今日目的:三個看起來毫不相干的故障,共同點不是原因,是它們都在事後查不到。
先備知識:本篇銜接 Day 2〈CRC 資源配置〉,讀過該篇即可上手。
Day 2 結尾留了一句話:
磁碟壓力是一個事後去看就看不到的事件……證據只留在 kubelet 日誌與
oc get events裡,而 OpenShift 事件的預設保留期通常只有一小時左右,晚一步查就什麼都看不到。這個模式在後面幾天還會反覆出現:系統事後回報的狀態,跟事發當下的狀態是兩回事。
這篇就是再一次重演這個模式。本篇三個案例的故障類型完全不同:一個是 Windows 的行程管理,一個是叢集 DNS,一個是 kubelet 的重試排程。但踩的是同一個坑:Get-Process 或 oc get 回給你的那一格狀態,跟系統當下真正在做的事不是同一回事,而且不會報錯。三個案例剛好都對得上:
| 案例 | 畫面上看到的 | 系統實際在做的 | 為什麼事後查不到 |
|---|---|---|---|
| oc.exe 孤兒行程 | Get-Process 有 oc 在跑,看起來有正常作業 |
它在等上一代叢集裡一個早就不存在的 Pod | timeout 一到自己消失,什麼痕跡都不留 |
| CoreDNS | 2/2 Running,RESTARTS 6 |
計數器只記次數,不記原因 | 異常畫面(CrashLoopBackOff/0/2)只維持幾秒,events 一小時就過期 |
| 映像拉取退避 | Init:1/2,看起來只是還在啟動 |
kubelet 在等一個看不見的退避時鐘 | 刪掉重建幾十秒就轉 Running,讀數很漂亮——但那是本地映像快取造成的假象,這一輪根本沒有發生真正的拉取 |
也因為這樣,這篇沒有漂亮的故障截圖。三個案例裡只有第一個(oc.exe 孤兒行程)完整重現、而且推翻了自己原本的假設;另外兩個(CoreDNS、映像拉取)這一輪 crc stop/crc start 都乾淨過關,查不到,本來就是這三件事的共同特徵。後兩節給的是「知道自己在盲區時該怎麼做」的排查方法與預防性配置,不是這次事件的修復記錄。
下文腳本一律用 -o json 搭配 ConvertFrom-Json 解析 oc 輸出,直接讀物件屬性;PowerShell 5.1 對純文字輸出的型別陷阱(單行輸出會摺疊成純量字串而不是陣列)是另一個坑,留到 Day 4 細拆。
🪟 Windows/CRC 限定:本篇整篇處理的 oc.exe 孤兒行程殘留、CRC VM 重啟/網卡重新綁定造成的 CoreDNS 異常,都是 Windows 搭配 CRC 單機虛擬機這個組合特有的維運現象,在 Linux 原生跑 kubeadm 或雲端受管 K8s 叢集上不會遇到同樣的故障模式。
在 Windows PowerShell 執行長時間等待指令時(如 oc wait 或 oc rollout status):
oc wait --for=condition=Ready pod/xxx -n xxx --timeout=300s
oc rollout status daemonset/ovnkube-node -n openshift-ovn-kubernetes --timeout=120s
若中途中斷指令,Windows 環境可能持續在背景保留 oc.exe 子行程,Get-Process -Name "oc" 看得到它還在跑。
這兩個範例都帶了 --timeout,但「不帶 --timeout 會怎樣」這件事,oc wait 和 oc rollout status 的預設值完全不同,不能一概而論:
| 指令 | 不帶 --timeout 時的行為 |
|---|---|
oc wait |
預設 30s,時間到就自動放棄(0 是「只檢查一次」,負值則是等一整週) |
oc rollout status |
預設 0s,這裡的 0 是永不逾時,會一直掛著 |
oc wait 省略 --timeout 反而是全篇最不容易留下孤兒行程的情況;真正會無限期留著的,是 oc rollout status 不帶 --timeout,或 oc logs -f、oc get -w 這類本來就沒有終點的串流指令。但「中斷」實際上是什麼,比乍看之下複雜,〈實際的成因〉一節會拆開來看。
網路上(以及我自己原本的認知)常見的說法是:Ctrl+C 中斷 oc wait 之後,殘留的 oc.exe 會鎖住 CRC 的虛擬磁碟(crc.vhdx),導致 crc stop 卡死。
這個說法其實不必等實測才能反駁,機制上就站不住腳:oc.exe 是純粹的 API client,透過 HTTPS 呼叫 API server 就結束了工作,從頭到尾不會打開、讀寫宿主機上 crc.vhdx 這個虛擬磁碟檔——那個檔案是 Hyper-V 這層虛擬機管理程式自己在管的,oc.exe 這個 process 連它的檔案路徑都碰不到。下面的實測,是替這個原理性的結論多補一組數據,不是唯一的論證依據。
重現方式:先建立一個因資源需求不合理(要求 100 顆 CPU、200Gi 記憶體)而永遠停在 Pending 的測試 Pod,確保它不會提早變成 Ready,讓 oc wait 真的卡住而不是秒退;接著以背景行程啟動 oc wait --for=condition=Ready pod/repro-stuck-pod -n default --timeout=600s,不等它結束,模擬「Ctrl+C 沒清乾淨」後的殘留狀態。確認殘留行程存在:
Id ProcessName StartTime CPU
-- ----------- --------- ---
17668 oc 2026/8/1 下午 07:47:17 0.109375
在這個殘留行程仍在跑的情況下執行 crc stop,實際結果是直接回報成功,沒有卡住,也沒有任何磁碟鎖定相關的錯誤訊息:
level=info msg="Stopping the instance, this may take a few minutes..."
Stopped the instance
crc status 也確認 VM 確實進入 Stopped:
CRC VM: Stopped
OpenShift: Stopped (v4.21.14)
Disk Usage: 0B of 0B (Inside the CRC VM)
但更值得注意的是接下來這一步:VM 已經停止之後,那個殘留的 oc.exe 依然活著,這時候離它自己的 --timeout=600s 期限還沒到:
Id ProcessName StartTime CPU Responding
-- ----------- --------- --- ----------
17668 oc 2026/8/1 下午 07:47:17 0.125 True
磁碟鎖定這個環節,到這裡可以確定不成立。
殘留行程確實是個問題,只是問題不在「鎖住磁碟讓 crc stop 卡死」,而在於它變成一個孤兒行程:client 端完全不知道自己原本連的叢集已經沒了。只要 timeout 設得比接下來的操作視窗長(例如帶了 --timeout=600s,crc stop 到下一次 crc start 完成往往用不到 600 秒),或指令本身就沒有終點(oc rollout status、oc logs -f、oc get -w),實務上就會有三個影響:
crc start 完成、新叢集就緒時,若殘留行程的 timeout 還沒到期,它會對著新一代叢集的 API server 送請求,但它等待的其實是上一輪叢集裡的某個 Pod,狀態早就對不上。Get-Process -Name "oc" 判斷有沒有指令還在跑,會被這類殭屍行程騙到,誤以為有正常作業在進行——這條不受 timeout 長短影響,只要行程還活著就會誤判。oc rollout status、oc logs -f、oc get -w 這類沒有終點(或 timeout 設得很長)的指令,背景可能同時存在好幾個等著不同世代叢集資源的殘留行程,彼此狀態互不相干;oc wait 因為預設 30 秒就會自己收掉,撐不住這種累積。上一輪是用背景行程模擬殘留,嚴格說並不是真的按下 Ctrl+C,所以再補測一次:對共享 console 的 oc.exe 送出真正的 CTRL_C_EVENT。做法是用 -NoNewWindow 啟動 oc wait,讓它跟控制腳本共用同一個 console(比較接近使用者在終端機裡直接執行、再按下 Ctrl+C 的情境),確認行程存在後,透過 Win32 API GenerateConsoleCtrlEvent 送出 CTRL_C_EVENT。
結果 oc.exe 立即終止,沒有留下任何行程;而且這個信號連帶把發送信號的控制腳本自己也殺掉了——即使事先呼叫 SetConsoleCtrlHandler 要求忽略也沒有用。
這個「意外」與一個機制吻合:CTRL_C_EVENT 是廣播給整個 console process group 的,不是送給單一行程。所以在同一個 console 裡按下 Ctrl+C,前景的 oc.exe 是真的會收到並終止的。
那實務上看到的殘留是怎麼來的?上面的重現方式其實已經給出一個有實測支撐的候選答案:一開始就用背景行程(Start-Process,不帶 -NoNewWindow)啟動,這類行程會拿到自己獨立的新 console 與 process group,從頭到尾不在使用者操作的那個前景 console 裡——自然收不到操作者那個 console 送出的 CTRL_C_EVENT 或 CTRL_CLOSE_EVENT 廣播,Start-Job、CI runner 起的行程也是同樣的處境。至於直接關閉終端機視窗,Windows 通常還是會送出 CTRL_CLOSE_EVENT 給對應的 process group,多數情況下行程還是會被結束;只有當中斷方式本身沒有讓行程處在會收到訊號的 console process group 裡(例如某些 IDE 的中斷按鈕、job 物件式的行程管理),才會留下孤兒行程。背景啟動這條路已經實測驗證過;直接關閉終端機視窗是否也會產生同樣結果,這裡沒有逐一實測,仍標註為推論。
到這裡,原本那個說法的兩個環節都不成立:Ctrl+C 真的送達時不會留下行程,而留下來的行程也不會鎖住磁碟。真正該防的是另一件事——當中斷的方式讓信號沒能送達,會得到一個不知道叢集已經消失、直到自己 timeout 才會結束的孤兒行程。
這一段原本是照著常見說法寫的。為了補實際結果去重現,先發現殘留行程不鎖磁碟;再補測一次真正的 Ctrl+C,才發現連「Ctrl+C 會留下行程」這個前提也不成立。一個兩句話的說法,兩個環節都是錯的。
這個假說之所以能流傳這麼久,正是因為 Get-Process 只給你「行程還在」這一格資訊——它不告訴你這個行程握著哪些 handle、正在跟誰通訊、等的是哪一代叢集的哪個資源。看不到的那一段,人就自己補了一個因果:看到殘留的 oc.exe,看到 crc stop 偶爾比較慢,兩件事被接成「它鎖住了 vhdx」。上面這兩次實測,做的其實是同一件事——把那格狀態背後真正發生的事挖出來,然後發現接錯了。
而且這個窗口很窄。VM 停止之後那組結果只能證明「那一刻行程還在」,跟「timeout 未到期」無法區分——要證實孤兒行程撐過了 timeout 依然存在,這組對照沒補做,因為根本來不及:timeout 一到,行程就乾乾淨淨地自己消失,不留 log、不留 event,Get-Process 也查不到。上面那兩組畫面——VM 停止後行程還活著、CTRL_C_EVENT 送達後行程立刻消失——都是在它還活著的那幾分鐘裡才拍得到的,錯過就沒有第二次。
清理腳本值得放進維運前置流程:目的是避免孤兒行程干擾下一輪操作與自動化判斷,跟磁碟鎖定無關。在發起 crc stop 或關閉 PowerShell 視窗前,先做一次行程檢查與清理:
# 檢視目前背景殘留之 oc.exe 行程
Get-Process -Name "oc" -ErrorAction SilentlyContinue
# 強制終止所有殘留的 oc.exe 行程,避免孤兒行程干擾後續操作
Get-Process -Name "oc" -ErrorAction SilentlyContinue | Stop-Process -Force
範圍提醒:此指令按行程名稱比對,會終止所有名為 oc 的行程,不會區分哪一個才是真正該清理的殘留來源。若當下其他終端機視窗仍有正常運作中的 oc 指令(例如另一個視窗正在
oc logs -f監看日誌),會被一併強制中斷。執行前建議先確認目前沒有其他終端機正在依賴 oc 行程。
將此清理邏輯整合至維運前置流程:
function Clear-OcProcesses {
$ocProcesses = Get-Process -Name "oc" -ErrorAction SilentlyContinue
if ($ocProcesses) {
Write-Host "偵測到殘留 oc.exe 行程,進行強制清理..."
$ocProcesses | Stop-Process -Force
Start-Sleep -Seconds 2
} else {
Write-Host "無殘留 oc.exe 行程。"
}
}
Clear-OcProcesses
以下是這個叢集上 openshift-dns 命名空間目前的實際狀態:
NAME READY STATUS RESTARTS AGE
dns-default-gfbmv 2/2 Running 6 14d
node-resolver-m7vg8 1/1 Running 3 14d
兩個 Pod 都是 Running,看起來一切正常——RESTARTS 分別是 6 與 3。
CRC 虛擬機重啟或網路介面重新綁定後,節點層級的 DNS 設定可能沒有正確恢復;CoreDNS 轉發依賴的正是這份設定,外部網域或叢集內部路由(*.apps-crc.testing)因此解析失敗。
RESTARTS 6/3 比較站得住腳的解釋是:這 14 天內做過好幾次 crc stop / crc start(包含 Day 2 磁碟章節記錄的那一輪重啟),VM 重新開機本身就會讓 DaemonSet 的容器跟著重啟一輪——這個解釋不需要假設任何 DNS 異常,是目前唯一不需要額外假設的解釋。
CRC 重啟或網卡重新綁定會導致 DNS 解析失敗這條因果鏈,本身沒有這一輪的重現畫面:來源是這台機器過去幾次 crc stop/crc start 之後觀察到的解析短暫失效,當時沒有留存結果,也不是查自官方文件或外部 issue——這裡誠實標成「過去的操作經驗」,不是這一輪查證出來的結果。
想確認這幾次重啟裡有沒有真的因為介面重新綁定或 DNS 異常而重啟過,得另外查 oc describe pod 的 Last State(reason: Error 還是 Completed)或 oc logs --previous,這裡沒有做這一步查證,所以不把 RESTARTS 歸因到 DNS 異常上。CoreDNS 真正異常時的樣子通常是短暫的 CrashLoopBackOff 或 READY 欄位掉到 0/2——這個畫面只維持幾秒,而 OpenShift 事件的預設保留期通常只有一小時左右,晚一步查就什麼都看不到。這次為了撰寫本篇特地做的 crc stop / crc start,CoreDNS 跟 OLM 都乾淨過關,沒有觸發那個畫面。
動手刪 Pod 之前,值得先做幾步排查,確認問題真的出在 CoreDNS,而不是別的地方:
dns.operator.openshift.io/daemonset-dns=default這個 label 是從 cluster-dns-operator 的慣例推出來的,這裡沒有機器可以實際核對——套用前先跑一次oc get pods -n openshift-dns --show-labels確認這個叢集上的dns-defaultPod 真的帶這個 label,再定案。
以下三步只讀取資訊、不做任何變更,可以放心全部跑完再決定要不要往下刪 Pod:
$OC = (Get-ChildItem "$env:USERPROFILE\.crc\cache" -Recurse -Filter "oc.exe" | Sort-Object LastWriteTime -Descending | Select-Object -First 1).FullName
$kubeconfig = "$env:USERPROFILE\.crc\machines\crc\kubeconfig"
# 用 label selector 鎖定 dns-default DaemonSet 的 Pod,直接以物件屬性取得 metadata.name
$dnsPod = (& $OC --kubeconfig $kubeconfig get pods -n openshift-dns -l dns.operator.openshift.io/daemonset-dns=default -o json | ConvertFrom-Json).items |
Select-Object -First 1 |
Select-Object -ExpandProperty metadata |
Select-Object -ExpandProperty name
if (-not $dnsPod) {
Write-Host "未找到 dns-default Pod,請檢查 openshift-dns 狀態。"
} else {
# 1. 看 CoreDNS 容器本身的日誌有沒有轉發失敗、上游逾時之類的錯誤
& $OC --kubeconfig $kubeconfig logs -n openshift-dns $dnsPod -c dns --tail=100
# 2. 確認節點拿到的主機網卡 DNS 設定是否正常
& $OC --kubeconfig $kubeconfig debug node/crc -- chroot /host cat /etc/resolv.conf
# 3. 直接從節點角度測一次解析,排除「只是 Pod 內部異常」的可能性
& $OC --kubeconfig $kubeconfig debug node/crc -- chroot /host nslookup kubernetes.default.svc.cluster.local
}
oc debug node會實際啟動一個 Pod、佔用節點磁碟——Day 2 那次 DiskPressure 就是連開幾個 debug Pod 觸發的。這台機器背的還是那顆 60GB 的磁碟,跑完第 2、3 步記得確認 debug Pod 已經清掉;如果磁碟已經逼近門檻,先用crc status看一眼再決定要不要開。
這三步分別對應三個可能的斷點:CoreDNS 容器自己記錄了什麼、主機網卡層級的設定對不對、以及拋開 Pod 直接從節點角度測是否也解析失敗——第 2 步特別重要,它查的是節點層級的 DNS 設定,不只影響 CoreDNS,下一節的映像拉取退避也是同一個根因。排查完確認問題出在 CoreDNS Pod 本身(而不是節點網路設定),修復方式才是重啟這個 Pod(沿用上面已經取得的 $dnsPod):
Write-Host "發現 CoreDNS Pod: $dnsPod,發起重啟..."
& $OC --kubeconfig $kubeconfig delete pod $dnsPod -n openshift-dns
Pod 轉成 Running 只代表容器重新啟動完成,不代表解析真的修好了。用一次實際查詢收尾,對應收工檢查清單裡「已確認可正常解析外部網域與內部路由」這一項:
# 內部路由:叢集內部 Service DNS 能不能解析(沿用上面的 $OC 與 $kubeconfig)
& $OC --kubeconfig $kubeconfig run dns-verify --image=busybox:1.36 --restart=Never --rm -i -n default -- nslookup kubernetes.default
# 外部網域:出站解析能不能通
& $OC --kubeconfig $kubeconfig run dns-verify-ext --image=busybox:1.36 --restart=Never --rm -i -n default -- nslookup www.redhat.com
要注意這兩條指令本身也得先拉 busybox 這個映像。拉 busybox 用的是節點層級的 DNS,跟你剛重啟的 CoreDNS 不是同一條路徑——所以這兩條指令其實是兩段驗證疊在一起:拉得下來,代表節點層級的解析通了;nslookup 回得來位址,才代表 CoreDNS 這一支也好了。卡在 ImagePullBackOff,指向的是節點那一層,回頭跑前面第 2 步排查(Docker Hub 匿名拉取的額度限制是另一個可能原因,但機率較低)。
兩條指令都回得來位址才算數;任何一條逾時或回 NXDOMAIN,代表問題還沒解決。
Marketplace Pods 若在 DNS 尚未就緒時被建立,畫面上會依序經過三種樣子:拉取進行中是 Init:1/2,拉取失敗後變成 Init:ErrImagePull,進入重試排程後才是 Init:ImagePullBackOff。麻煩的是第一種——Init:1/2 跟「一切正常、只是還在啟動」長得一模一樣,從這一格看不出它接下來會成功還是會失敗。
OpenShift 初始化運作時,OLM (Operator Lifecycle Manager) 會自動拉取 Red Hat Catalog Image(如 registry.redhat.io/redhat/*-index:v4.21),在一般連線速率下約需 10 至 15 分鐘(企業內網或離線閉環環境視內部 Registry 同步配置而定,本系列後續 Day 21、Day 24 會處理這類環境的架構優化)。
上一節的因果鏈到「CoreDNS 解析失敗」為止,不能直接接到這一節——映像拉取的 DNS 查詢不經過 CoreDNS。這是標準的 OpenShift/CRI-O 架構行為,也對這個叢集實測驗證過:刪除 dns-default Pod,趁它還沒重建完成的空窗(舊 Pod 還在 Terminating、新 Pod 剛 ContainerCreating)建立一個用到 busybox:1.36(這個節點上事先確認過沒有快取)的測試 Pod,kubelet 事件記錄顯示映像在 5.568 秒內拉取完成、容器隨即啟動——全程 CoreDNS 都不是 Ready 狀態。拉映像是 CRI-O 在節點層級做的,用的是節點自己的 /etc/resolv.conf;CoreDNS(dns-default)服務的是 Pod 內部的解析,兩者走不同路徑。正確的關係是並列,不是串聯:
CRC VM 重啟/網卡重新綁定
→ 節點層級 DNS 異常(/etc/resolv.conf、上游不通)
├─ CoreDNS 轉發失敗 → Pod 內部解析不到
└─ CRI-O 拉映像時解析不到 registry → 進入指數退避 → OLM 停擺
這也順帶解釋了 openshift-dns 底下為什麼有兩個 DaemonSet:dns-default 負責 Pod 內部解析;node-resolver 則是節點層級的另一塊拼圖,維護每個節點 /etc/hosts 裡給內部映像倉庫用的靜態記錄。兩者都算在「節點層級 vs Pod 層級」這條分界裡,屬於節點那一側的基礎設施。
也就是說,重啟 dns-default Pod(上一節的修法)只處理左邊那一支;如果根因是節點層級的 DNS 設定本身沒恢復,右邊這一支不會因此變好——上一節排查步驟第 2 步(節點網卡 DNS 設定)查的正是這個共同根因。
而且就算節點層級的 DNS 恢復了,右邊那一支也不會馬上自己好——kubelet 對映像拉取失敗採用指數退避(Exponential Backoff)重試排程:一旦初期因 DNS 解析失敗被記錄為 ImagePullBackOff,後續重試間隔會逐步拉長,即使 DNS 稍後恢復正常,Pod 也不會立即重新嘗試,而是等待排定的下一個退避週期,往往需要數分鐘甚至更久。這與「DNS 修復後應該馬上恢復正常」的直覺不符。
對這個叢集的 openshift-marketplace 命名空間執行了一次強制刪除指令,並在重建過程中連續輪詢,看真正卡住的 Init 狀態會不會再次出現。輪詢過程中抓到一張 Init 階段的畫面:
NAME READY STATUS RESTARTS AGE
redhat-operators-wtvb2 0/1 Init:1/2 0 6s
老實說,這畫面不構成證據——Init:1/2 只是「兩個 init container 完成了一個」,是每個 Pod 重建時必經的正常過渡狀態,不是故障訊號。真正的故障訊號要看到 Init:ImagePullBackOff 或 Init:ErrImagePull,這一輪沒有出現:所有 Pod 在數十秒內就從 Init 轉為 Running,沒有觸發 Back-off pulling image 事件。
這正是本篇主題最直接的例子:這一輪讀數很漂亮,但那是假象——刪掉再重建是在同一個節點上,映像仍留在節點的本地快取裡,這一輪其實沒有發生真正的拉取,數十秒只是容器啟動時間,不能拿來當離線鏡像倉庫效能的證據。這份快取算的是 Day 2 那本磁碟帳的同一顆磁碟、同一份預算——這台機器背的還是那顆 60GB 的磁碟,拉得越多、越靠近 Day 2 記錄的那個 85% 門檻。
真正會卡在退避循環的情境,是映像不在本地快取、而且 DNS 或 Registry 真的不可用的時候。手上沒有留存 Back-off pulling image 事件的畫面;要重現通常得搭配 DNS 暫時失效或清空節點映像快取,這裡不刻意製造那個條件去影響叢集上其他正在跑的工作負載。上一節提到的那一輪 crc stop / crc start 裡,VM 重啟後 OLM 首次重新拉取 Catalog Image 同樣乾淨過關,沒有任何拉取失敗的紀錄。
修復這條鏈時順序是固定的:先確認節點層級的 DNS 已經恢復(不只是 CoreDNS Pod 重啟完成),再主動清掉已經進入退避週期的 Pod,跳過任何一步、後面那步都會白做。節點層級的 DNS 解析修好之後,清掉卡住的 Pod,讓 kubelet 用最新的網路設定重新拉一次映像:
# 沿用前一節定義的 $OC 與 $kubeconfig
& $OC --kubeconfig $kubeconfig delete pods -n openshift-marketplace --all --force --grace-period=0
要留意的是 --all 會刪掉 openshift-marketplace 命名空間下所有 Pod,不只是卡在 Init 的那幾個,也包括 marketplace-operator 本身;--force --grace-period=0 在此情境下安全,原因是這整個命名空間裡沒有任何 Pod 掛載持久化儲存,強制刪除沒有資料一致性風險,只是跳過本來就不會執行到的優雅終止流程,而且 marketplace-operator 有 Deployment 控制器會自動拉回來。這個組合不是通用解法——若套用在已掛載 PVC 或正常運作中的工作負載上,跳過優雅終止可能導致連線中斷或資料寫入不完整,需視情境判斷。
# 重試上限對齊本節開頭提及的 Catalog Image 拉取時間估計(10 至 15 分鐘):
# 60 次 * 15 秒 = 900 秒(15 分鐘),抓在預期完成時間的上緣,避免拉取尚未完成就提前判定逾時
$maxRetries = 60
$delaySeconds = 15
$retryCount = 0
do {
Start-Sleep -Seconds $delaySeconds
$retryCount++
$podsJson = & $OC --kubeconfig $kubeconfig get pods -n openshift-marketplace -o json | ConvertFrom-Json
if (-not $podsJson.items -or $podsJson.items.Count -eq 0) {
# 強制刪除之後、新 Pod 還沒建立完成之前會有一小段空窗期,這裡不能當成「就緒」
Write-Host "命名空間目前沒有任何 Pod,尚在重建中... ($retryCount/$maxRetries)"
$notReady = $true
} else {
# status.phase 只反映 Pod 生命週期階段,Running 不等於容器已就緒——
# 必須進一步比對 containerStatuses[].ready,否則會把還在拉映像的 Pod 誤判為就緒
# 注意:Failed 狀態的 Pod 也會落在「不就緒」分支,導致迴圈跑滿 $maxRetries 才報錯,
# 這是刻意保留的簡化寫法——如果要 fail-fast,需另外針對 phase -eq "Failed" 加一個提早跳出的分支
$notReady = $podsJson.items | Where-Object {
$_.status.phase -notin @("Succeeded") -and
(-not $_.status.containerStatuses -or
($_.status.containerStatuses | Where-Object { -not $_.ready }))
}
Write-Host "等待 Marketplace Pods 轉為 Running/Succeeded... ($retryCount/$maxRetries)"
}
} while ($notReady -and $retryCount -lt $maxRetries)
if ($notReady) {
Write-Error "Marketplace 逾時未就緒。"
} else {
Write-Host "Marketplace 就緒,CRC 可以開始部署。"
}
計數器、快取讀數、行程列表——這三次踩的都是同一類東西。它們都是二手狀態,是系統整理過之後才給你看的摘要,各自省略了不同的東西:計數器省略原因,快取省略「是否真的發生」,行程列表省略「它在做什麼」。維運時真正該問的不是「現在顯示什麼」,而是「這個顯示是誰算出來的、它省略了什麼」。
省略的那段不會空著,人會自己補一個因果,填錯了也不會有人發現——第一節那個流傳多年的 vhdx 說法就是這樣來的。
Day 4 要處理的是另一種摘要失真:PowerShell 把 CLI 輸出重新包裝成物件時,同樣會在你沒注意的地方悄悄改變型別或省略內容。
--timeout(oc rollout status 尤其要帶,它預設 0s 不逾時,會一直掛著);在前景 console 執行,不要丟到背景;要中斷就在同一個終端機視窗裡直接按 Ctrl+C(會確實終止),而不是直接關視窗、斷線,或用某些 IDE 的中斷按鈕——這些路徑可能無法把信號完整送達整個 console process group,才是留下孤兒行程的真正成因。