回到所有文章

Pod 建立之後,要跑在哪裡?理解 Scheduler 的決策與交接

新副本一直 Pending,CPU 卻很閒。拆開 Scheduler 的篩選、評分與指派,找出它被什麼擋下。

尖峰前 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
Allocatable4
已排上的 Pod requests 合計3.5
剩餘額度0.5
新 Pod 的 request1

新 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 觀察來源

Scheduler、api-server 中的 Pod 與 kubelet 的交接,以及排不上時的重試
開啟完整圖表

圖表達各自的責任,不是完整呼叫時序。選好節點之後仍可能有其他檢查,完整流程留在延伸閱讀。

不過,不是每次都找得到位置。如果 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,現在的狀態是:

NodeLabelTaint剩餘 CPU 額度
node-aworkload=web無0.5
node-bworkload=webdedicated=batch:NoSchedule2
node-cworkload=web無0.5
node-dworkload=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 UnschedulableScheduler 嘗試過,沒有可行的 Nodecondition 的 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 題的評價是根據明列條件做的工程取捨;官方來源支持的是底層機制,不代表官方推薦唯一的事故處理順序。

繼續閱讀其他筆記