尖峰前 30 分鐘,hello-web 需要再多一個副本。新 Pod 建立了,卻一直停在 Pending;看了一眼監控,Node 的 CPU 使用率明明很低。 這時候,你會先改哪裡?
要回答這個問題,得先知道 Scheduler 替一個 Pod 選 Node 時,到底看了哪些東西。這篇先把 Scheduler 的判斷拆開來看,再回到這個問題。
1. Pod 建立之後,Scheduler 怎麼接手?
Deployment 需要一個新副本時,api-server 裡會先多出一個 Pod 物件(它怎麼被建立,可以參考〈Deployment 如何變成 Pod?〉)。
Scheduler 會持續觀察 api-server 上還沒有 Node 的 Pod,逐一替它們選位置。Scheduler 觀察 Pod 的實作 輪到某個 Pod 時,它要回答的問題只有一個:這個 Pod 該放在哪台 Node?
2. 先找能放的,再選適合的
Scheduler 替一個 Pod 選 Node,分成兩步。先問「哪些 Node 根本不能放?」,把不符合必要條件的排除掉,這一步叫 Filtering;如果還剩好幾台,再問「哪一台比較適合?」,替這些候選打分數,這一步叫 Scoring。Scheduler 的選擇流程
這個順序有一個直接的推論:Pod 排不上,一定是 Filtering 的問題。 Scoring 只在有候選時才會發生,分數再高,也救不回一台已經被排除的 Node。所以開頭那個副本卡住,要找的是「它被哪些必要條件擋下」。
必要條件:三種常見的門檻
Filtering 會檢查很多種條件,實務上最常擋住 Pod 的是這三種:
- 資源額度:Pod 宣告需要 1 CPU,Node 剩下的額度只有 0.5,就放不下。「剩下多少」怎麼算,是下一節的重點,也是開頭「CPU 明明很閒」的答案。
- Taint 與 toleration:Node 可以替自己設下門檻。例如有 GPU 的 Node 貼上
dedicated=gpu:NoSchedule,意思是「這台留給需要 GPU 的工作」,沒有對應 toleration 的 Pod 會被排除。帶了 toleration 只是解除這道阻擋,讓 Pod 可以進入候選,Scheduler 不一定會選那台。Taints 與 tolerations - 必要的 node affinity:Pod 也可以反過來挑 Node。訓練工作在
requiredDuringSchedulingIgnoredDuringExecution裡要求accelerator=gpu,就只有帶這個 label 的 Node 能進候選。Node affinity
後兩種門檻方向相反:taint 是 Node 擋掉不該來的 Pod,affinity 是 Pod 挑選自己要去的 Node。所以想讓一批 Node 專門跑某類工作,官方文件建議兩者一起用:只加 taint,帶 toleration 的工作仍可能被排到其他 Node;只加 affinity,其他 Pod 仍然可以佔用這批 Node。專用節點與特殊硬體的例子
一台 Node 只要有一道門檻過不了,就會被排除。所以能不能排上,不是看「叢集總共還剩多少 CPU」,而是看有沒有一台 Node 能同時通過所有門檻。
偏好:Scoring 怎麼選
通過 Filtering 的 Node 如果不只一台,Scheduler 會替每台打分數。分數來自好幾個評分項目,例如 preferred node affinity(Pod 表達「符合比較好」的偏好),以及資源分布等其他考量,最後依權重加總,選出總分最高的一台。Filter 與 Score
這裡要分清楚 required 和 preferred:required 決定「能不能進候選」,preferred 只在候選之間加分。偏好某台 Node,也不保證最後會選到它,因為總分還取決於其他項目。
三道門檻裡,taint 和 affinity 看設定就能判斷,最容易讓人困惑的是資源:監控上明明還很閒,為什麼會「放不下」?
3. CPU 明明很閒,為什麼還放不下?
Scheduler 判斷放不放得下,看的不是監控上的使用率,而是 requests 的帳。每台 Node 有一個可供 Pod 使用的 CPU 額度,叫 Allocatable;已經排上這台的 Pod,各自宣告了 request。兩者相減,就是剩餘額度。
| 某台 Node 的容量帳 | CPU |
|---|---|
| Allocatable | 4 |
| 已排上的 Pod requests 合計 | 3.5 |
| 剩餘額度 | 0.5 |
| 新 Pod 的 request | 1 |
新 Pod 要 1,這台只剩 0.5,於是被排除。即使監控顯示這台實際只用了 0.4 CPU,Scheduler 也不會把「已計入 3.5」改成 0.4。CPU 容量檢查實作
既然監控上就有即時使用量,為什麼不照 Node 當下有多忙來調度?主要有三個原因:
- 使用量會變。 這一刻很閒,可能只是原有工作負載還沒進入高峰,五分鐘後就不一定了。
- request 是承諾。 CPU 不夠分時,容器依 request 的比例取得 CPU;排程要確保這些承諾加起來不超過 Node 的額度,承諾才有意義。requests 如何套用在執行時
- 排程是一次性的決定。 Pod 排上之後,不會因為使用量變化而被搬走,所以這個決定必須在一段時間內都站得住。
因此 Kubernetes 預設以 requests 作為容量判斷的基準,瞬間的空閒不會變成更多可排程額度。資源需求與排程 依實際負載調度的做法也存在,例如 scheduler-plugins 的 Trimaran,但那是額外安裝的外掛,不是預設行為。
所以「CPU 很閒」和「requests 已經計滿」可以同時成立,這就是開頭那個問題的一半答案。
request 本身可能高估,也可能低估,需要用工作負載資料檢討;但「這次容量檢查怎麼算」和「這個 request 設得好不好」,是兩個問題。這份帳也不是替容器鎖住專用 CPU 核心,執行時 CPU 怎麼分配,見〈CPU 才用三成,為什麼還被 throttle?〉。本例只計算單一一般容器,其他資源計算邊界放在延伸閱讀。
通過 Filtering、選出分數最高的 Node 之後,Scheduler 要把這個決定交出去。
4. 指派成功,或這次還排不上
把節點指派提交給 api-server 的動作,叫做 binding。預設的處理方式會送出包含 Pod 身分與目標 Node 的 Binding 請求。成功後,api-server 裡的 Pod 會以 .spec.nodeName 記錄指派。DefaultBinder
目標 Node 上的 kubelet 透過 api-server 觀察分配給自己的 Pod,再處理啟動。Scheduler 不需要直接呼叫 kubelet 說「幫我啟動這個 Pod」。kubelet 的 Pod 觀察來源
圖表達各自的責任,不是完整呼叫時序。選好節點之後仍可能有其他檢查,完整流程留在延伸閱讀。
不過,不是每次都找得到位置。如果 Filtering 之後一台 Node 都不剩,Scheduler 就沒有位置可以提交。在一般找不到可行節點的失敗路徑,會留下 FailedScheduling Event,並更新 PodScheduled condition,也就是 Pod 上記錄排程是否完成與原因的狀態,讓我們知道這次遇到什麼限制。排程失敗處理
但這個 Pod 不會因此永遠失去機會。Node 新增、資源釋出,或相關條件改變,都可能讓它重新回到排程佇列,等待下一次嘗試。重試也會搭配退避,也就是失敗後先等一段時間,避免一直做相同的無效工作。哪些變化能觸發重試,取決於佇列與對應規則,不能假設所有更新都會立即重試。重新入列與 QueueingHint
實驗篇裡可以看到這件事:一個排不上的 Pod,在節點釋出容量後,沒有被重建,同一個 UID 就自己排上了。
到這裡,Scheduler 的決策與交接已經走完:篩選、評分、計入容量,再把指派交給 api-server 與 kubelet。現在可以回到開頭那個排不上的副本了。
5. 回到開頭:你會先改哪裡?
把前面的機制放回開頭的值班情境。以下是根據問答素材延伸的模擬題,不是實際事故紀錄。
hello-web 的 Pod 要求 1 CPU,必須跑在 workload=web 的 Node 上,沒有任何 toleration。叢集裡有四台 worker,現在的狀態是:
| Node | Label | Taint | 剩餘 CPU 額度 |
|---|---|---|---|
| node-a | workload=web | 無 | 0.5 |
| node-b | workload=web | dedicated=batch:NoSchedule | 2 |
| node-c | workload=web | 無 | 0.5 |
| node-d | workload=batch | 無 | 2 |
用第 2 節的三道門檻看一次:node-a、node-c 是 web 節點,但剩餘額度放不下 1 CPU;node-b 額度夠,卻有新副本沒有容忍的 taint;node-d 額度也夠,label 卻不符合必要的 affinity。四台各被一道門檻擋下,新副本一個候選都沒有。監控上 CPU 很閒,正是第 3 節說的情況:node-a、node-c 的 requests 已經計滿,使用量卻還沒上來。
kubectl describe pod 會看到這樣的 Event。以下是在 kind 重現同一個狀態的實際輸出(步驟見實驗篇),scheduling-lab-worker 到 -worker4 依序對應 node-a 到 node-d,另外多一台 control-plane:
$ kubectl get pod hello-web-new -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
hello-web-new 0/1 Pending 0 1s <none> <none> <none> <none>
$ kubectl describe pod hello-web-new
...
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Warning FailedScheduling 1s default-scheduler 0/5 nodes are available:
1 node(s) didn't match Pod's node affinity/selector,
1 node(s) had untolerated taint {dedicated: batch},
1 node(s) had untolerated taint {node-role.kubernetes.io/control-plane: },
2 Insufficient cpu.
no new claims to deallocate,
preemption: 0/5 nodes are available:
2 No preemption victims found for incoming pod,
3 Preemption is not helpful for scheduling.
開頭的 0/5 nodes are available 表示五台 Node 一台都不可行,接著依原因分組計數:2 Insufficient cpu 是 node-a 與 node-c,taint 是 node-b 和 control-plane,affinity 不符是 node-d。每一組都對應一個要回頭比對的設定。
後段的 no new claims to deallocate 與 preemption:,是篩選失敗之後 Scheduler 嘗試的補救:釋放 Dynamic Resource Allocation 的資源,以及驅逐優先權較低的 Pod。在這個情境裡兩者都幫不上忙,逐段解讀放在實驗篇。
你已確認排程狀態與 Events 指向這些限制;既有副本正常,錯誤率與延遲尚未惡化,但 30 分鐘後預計進入流量尖峰。
再補幾個決策條件:
- 相符的 web 節點池可擴容,配額與預算已確認,過往約 10 分鐘可提供新 Node;這不是本次完成時間的保證。
- batch 節點仍有需要保留的專用需求;web 的必要部署條件也尚未被證明可以放寬。
- 你只有當下 CPU 很低的資料,沒有足夠的啟動、尖峰與負載測試資料證明 request 高估。
你會先選哪一項?它會改變哪個阻擋條件?你要看到什麼,才會認為這次處理成功?
| 選項 | 行動 |
|---|---|
| A | 把新 Pod 的 CPU request 從 1 降到 0.5 |
| B | 增加符合條件的 web 節點容量 |
| C | 為工作負載增加 node-b 對應的 toleration |
| D | 把 required node affinity 改為 preferred |
| E | 刪除 Pending Pod,讓 ReplicaSet Controller 重建 |
展開:我的選擇與各方案評價
在這些條件下,我會先選 B,並持續確認擴容是否跟得上時間。 它直接補足符合部署要求的容量;A、C、D 則都需要目前還沒有的證據。這是依題目條件做的工程判斷,不是 Kubernetes 官方規定的處理順序。
| 方案 | 我的評價 | 驗證與撤回要注意什麼? |
|---|---|---|
| A:降低 request | 可以讓 node-a、node-c 通過本題的 CPU 容量檢查,但低即時使用量不足以證明 0.5 合理。降低 request 也會改變執行時 CPU 爭用的待遇,不能只看排程成功。 | 有資料支持時才小範圍驗證,觀察尖峰延遲與資源爭用。撤回需恢復工作負載設定;恢復較大 request 後,還要有足夠容量承接。 |
| B:增加相符容量 | 本題優先。代價是成本與等待時間;增加不符合 label、taint 或其他條件的 Node,仍然解不了問題。 | 確認 Node Ready、有效剩餘容量與 Pod Ready,再看服務指標。臨時容量事後可縮回,但需先確認工作負載能安全遷移,不能直接刪 Node 當作回復。 |
| C:增加 toleration | 只解除對應 taint 的阻擋。需要先確認共用是否被允許,以及是否會干擾專用工作負載;本題缺乏這個依據。 | 同時觀察 web 與 batch 的指標。從 template 移除 toleration,不會自動搬走已跑在那裡的 Pod,需要另外安排安全替換。 |
| D:放寬 affinity | 可能讓 node-d 進入候選,但改變了原本的部署要求。若它其實只是偏好,可以重新設計;本題不能先假設如此。 | 驗證新位置是否滿足工作負載需求。恢復 template 的 required 規則,也不等於現有 Pod 立即搬回合規位置。 |
| E:重建 Pod | 在本題設定與容量不變的前提下,沒有消除任何阻擋條件,也不必靠刪除才得到重試機會。 | 會換掉 Pod 身分並打斷原有追蹤,沒有明確收益;優先保留線索、處理限制。 |
A 的資源語意見 Resource Management,C、D 的許可與執行後語意見 Taints and Tolerations 與 Node Affinity。
實際更改應作用在管理它的工作負載設定,例如 Deployment 的 Pod template;這些選項不代表所有欄位都能直接 patch 既有 Pod。若採 GitOps,也要讓宣告來源與變更一致,並評估因此觸發的 rollout。Deployment 的 Pod template 更新
成功條件是服務取得需要的可用容量。 nodeName 有值只能說明走過了指派這一關,還要確認 Pod Ready、延遲與錯誤率,以及其他工作負載有沒有受到影響。實驗篇裡,A、C、D 都讓 Pod 取得了 Node,落點卻正是上表擔心的位置。
再改一個條件:如果配額已滿,來不及擴容呢?
這時我不會自動改選 A。要先補的是各替代方案的可行依據:有沒有可暫停、且獲准讓出容量的低優先工作?affinity 是否只是歷史設定?request 有沒有可靠量測?也要準備尖峰前容量不足時的流量或功能降級方案。這些已是服務層面的決策,不能只靠「哪個改動最快讓 Pod 不再 Pending」排序。
從節點指派,接到真正啟動
Scheduler 的工作到指派為止:篩選、評分,再把決定交給 api-server。開頭的副本排不上時,Scheduler 其實正照著我們給的條件工作;要改的,是條件或容量,而不是 Scheduler。
本篇解釋到 Scheduler 完成指派為止,Pod 在 Node 上怎麼啟動還沒有展開。接下來的問題是:kubelet 看到這個 Pod 之後,怎麼把 api-server 裡的描述變成 Node 上的容器程序?
這就是下一段要追的交接。
延伸閱讀:排查時值得知道的幾個邊界
Pending 不一定是還沒選到 Node
Pod 的 phase 是概括狀態。Pending 也涵蓋部分容器準備過程,例如下載 image;而 kubectl get pods 的 STATUS 欄位可能顯示 SchedulingGated、ContainerCreating、ImagePullBackOff 等更具體的提示,不一定直接等於 phase。Pod lifecycle
要確認卡在哪裡,先看 .spec.nodeName:空的就追排程,有值就沿著那台 Node 的啟動訊息往下查。還沒有 Node 時,PodScheduled condition 可以再把原因分開:
PodScheduled | 代表什麼? | 接著看什麼 |
|---|---|---|
| 持續沒有這個 condition | 還沒有 Scheduler 處理過它 | .spec.schedulerName 是否有對應的 Scheduler 在執行 |
False,reason SchedulingGated | 被 scheduling gates 擋在排程之前 | .spec.schedulingGates 由誰負責移除 |
False,reason Unschedulable | Scheduler 嘗試過,沒有可行的 Node | condition 的 message 與 FailedScheduling Events |
True | 已經指派 Node | 容器狀態、Events 與該 Node 的狀態 |
新建立的 Pod 在 Scheduler 第一次處理前,也會短暫沒有這個 condition,所以第一列的重點是「持續」。前兩種情況根本沒走到選節點,因此也不會有 FailedScheduling:沒有失敗 Event,不代表一切正常。這四種情況都可以在實驗篇裡看到。
查詢時,換成實際的 Pod 名稱:
kubectl get pod <pod> -o wide
kubectl get pod <pod> \
-o jsonpath='{.status.phase}{"\n"}{.spec.nodeName}{"\n"}{range .status.conditions[?(@.type=="PodScheduled")]}{.status}{" "}{.reason}{" "}{.message}{"\n"}{end}'
kubectl describe pod <pod>
手動設定 nodeName 可以繞過 Scheduler,因此有值不一定能證明經過一般篩選。nodeName 的語意
Events 也需要對照時間。先前的失敗可能已經過時;一台 Node 被某項 Filter 排除後,後面的檢查也可能不再執行,所以訊息不保證列出它的所有問題。Filter 階段
選到節點之後,還有哪些步驟?
本文把主線收斂成篩選、評分與指派。實際流程中,選好 Node 之後還可能有保留資源(Reserve)、決定是否等待或放行(Permit),以及指派前的準備(PreBind)等步驟。這些步驟解釋了為什麼「已經選到 Node」仍不等於一定完成 binding。
選好 Node 到完成指派之間,還有一段時間差。Scheduler 判斷時讀的是自己在本地保存的一份叢集資料:有哪些 Node、各自的條件,以及已經分配了哪些 Pod。它選定 Node 時,會先在這份資料裡把 Pod 的需求記到那台 Node 上,這個動作叫 assume;binding 則非同步進行,等待期間可以繼續處理下一個 Pod,而下一個 Pod 的容量檢查已經把這筆需求算進去。之後觀察到 api-server 完成指派時不會重複扣帳,後續步驟失敗則會撤銷這筆記帳。AssumePod 與 ForgetPod · ScheduleOne
這份本地資料靠觀察 api-server 更新,可能慢一拍;assume 只負責 Scheduler 自己剛做的決定。所以還有最後一道關卡:kubelet 在啟動 Pod 前,會依自己看到的 Node 狀態再檢查一次資源,放不下就拒絕,Pod 會以 OutOfcpu 之類的原因失敗。kubelet 的准入檢查 Assume 不代表 api-server 已經接受指派,更不代表容器已啟動。
在 v1.34.0 實作裡,只剩一個可行節點時,還會略過評分。想追完整順序,可以對照 Scheduling Framework 與 schedule_one.go。
查容量時,要看 api-server 裡真正保存的設定
原始 YAML 沒填 request,不代表 Scheduler 看到零。如果只填 limit,且沒有其他 admission 預設 request,Kubernetes 會以 limit 作為 request。Admission 是 api-server 接受物件時套用預設值、驗證或修改的處理環節,所以排查時要看 api-server 中的 Pod。Requests 與 limits
有 init containers、Pod overhead 等設定時,也不能直接套用本文的單一容器算法。這些細節適合在真正遇到數字對不起來時,回到 官方資源管理文件 逐項核對。
儲存也可能影響「選哪台」
看到 PVC Pending,不能一律歸類成指派後的掛載問題。儲存拓撲可能參與排程;StorageClass 的 WaitForFirstConsumer 會延後 volume 的 binding 或供應,讓選擇能配合 Pod 的排程條件。Volume binding mode
本文的實作細節核對 Kubernetes v1.34.0;節點數字是概念推演,第 5 節的 Event 則是在本機 kind 叢集重現的實際輸出。Production 題的評價是根據明列條件做的工程取捨;官方來源支持的是底層機制,不代表官方推薦唯一的事故處理順序。