K8s 排程:Scheduler、PriorityClass、Affinity、Taint 與 Toleration
K8s 排程:Scheduler、PriorityClass、Affinity、Taint 與 Toleration的重點整理。
K8s 排程:Scheduler、PriorityClass、Affinity、Taint 與 Toleration
排程題先判斷四件事 誰來排 → 哪些 Node 能放 → 合格 Node 中選哪台 → 放不下時是否能搶占。 PriorityClass 不會讓 Pod 跳過 Taint、Affinity 或資源限制;Toleration 也不會指定目的地。
| 問題 | 看哪個設定/機制 |
|---|---|
| 由誰處理這個 Pod? | schedulerName、Scheduler profile |
| 哪個 Pod 優先被處理? | PriorityClass、排程佇列 |
| 哪些 Node 能放? | requests、nodeSelector、Required Affinity、Taint/Toleration 等 |
| 合格 Node 中偏好哪台? | Scheduler Score、Preferred Affinity 等 |
| 移除低優先權 Pod 能否騰出合法位置? | Preemption;不能排除節點本身的硬限制 |
閱讀順序:Scheduler 流程 → 節點選擇與 Taint/Toleration → PriorityClass 與搶占、操作及排錯 → 多排程器。
以下指令使用 k 代表 kubectl,先設定 alias k=kubectl。Pod 建立、Admission 與 Node 執行的全貌見 K8s kubelet 與 Pod 執行流程。
Scheduler:選 Pod、篩 Node、評分與綁定
Scheduler 的核心是 先選 Pod → 篩 Node → 評分選 Node → 綁定。Plugin 是實作具體判斷的模組,掛在 Framework 提供的擴充點;同一個 Plugin 可以參與多個階段,也不必實作所有擴充點。
| 擴充點 | 在做什麼 | 例子/重點 |
|---|---|---|
| QueueSort | 決定等待中的 Pod 處理順序 | PrioritySort 依 Priority 等條件排序 |
| PreFilter | 整理篩選需要的共用資料,或先做整體檢查 | 計算 Pod 的資源需求 |
| Filter | 逐台檢查 Node 能不能放 | NodeResourcesFit 檢查 requests;NodeAffinity、TaintToleration 檢查節點條件 |
| PostFilter | 找不到可行 Node 時嘗試補救 | DefaultPreemption 評估搶占;不是每次都會搶占成功 |
| PreScore | 整理評分需要的共用資料 | 為候選 Node 的評分準備資訊 |
| Score | 替合格 Node 評分 | NodeResourcesFit 依資源策略評分;ImageLocality 偏好已有需要映像的 Node |
| Reserve | 在 Scheduler/Plugin 內暫記資源安排 | 尚未正式綁定,不等於在 Node 啟動容器 |
| Permit | 允許、拒絕或等待 | 可等待相關工作準備完成後再繼續 |
| PreBind | 做綁定前必要的準備 | 例如協調所需儲存資源 |
| Bind | 透過 API Server 完成 Pod 與 Node 的綁定 | DefaultBinder |
| PostBind | 綁定成功後通知與收尾 | 不再重新決定 Node |
Reserve 之後的流程失敗時,Unreserve 負責撤銷先前暫記的安排。Filter 和 Score 是「能不能放」與「放哪台比較好」的差別;PostFilter 則是沒有可行 Node 時的分支,不是正常篩選成功後一定接著執行的步驟。完整擴充點見 Scheduling Framework,內建 Plugin 見 Scheduler Configuration。
requests/limits 與 QoS 見 K8s 中的 Pod 是什麼。
先分清楚:Pod 想去哪裡、Node 允許誰進來
| 機制 | 設定位置 | 用途 |
|---|---|---|
| Node Label | Node 的 metadata.labels |
節點的特徵,例如 disk=ssd |
nodeSelector |
Pod 的 spec.nodeSelector |
選擇具有指定 labels 的 Node,多個鍵值全部要符合 |
| Node Affinity | Pod 的 spec.affinity.nodeAffinity |
用更有彈性的條件選 Node,區分必要條件與偏好 |
| Taint | Node 的 spec.taints |
排斥不符合容忍條件的 Pod |
| Toleration | Pod 的 spec.tolerations |
容忍匹配的 Taint,解除該 Taint 帶來的限制 |
Toleration 是通行資格,不是目的地 容忍某個 Node 的 Taint,不代表 Pod 一定會排到那台 Node,也不會略過 nodeSelector、Required Node Affinity、資源容量等其他限制。Node Label 和 Taint 是不同欄位;設定其中一項不會自動產生另一項。
Deployment 的 spec.selector.matchLabels 用來選 Pod;這裡的 nodeSelector 與 nodeAffinity 用來選 Node。若寫在 Deployment 中,Pod 的排程設定放在 spec.template.spec 下。
第一層:外層排程限制通常是 AND
對一般由 Kubernetes Scheduler 排程的新 Pod,可以把候選 Node 理解成:
未設定的限制不會額外篩掉 Node。「其他限制」包含可用資源、Pod affinity/anti-affinity、儲存與拓撲等要求。
Required Node Affinity 是硬條件;Preferred Node Affinity 是節點評分的一部分,不保證選中偏好的節點。PreferNoSchedule 也是軟性偏好,不屬於上面的阻擋性 Taint。
第二層:Required Node Affinity 內部,Terms 是 OR
- 多個
nodeSelectorTerms:任一 Term 符合即可,彼此是 OR。 - 同一 Term 內多個
matchExpressions:全部要符合,彼此是 AND。 - 單一
In條件的values:命中其中一個值即可。
例如:
對應 Pod 的 spec 片段:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: disk
operator: In
values: [ssd]
- key: zone
operator: In
values: [east]
- matchExpressions:
- key: disk
operator: In
values: [nvme]
- key: zone
operator: In
values: [west]
若同時設定 nodeSelector: {env: prod},完整條件是:
env=prod AND ((disk=ssd AND zone=east) OR (disk=nvme AND zone=west))
這只是 label/affinity 條件;還要通過 Taint 與其他排程限制。
第三層:Taint 與 Toleration 獨立檢查
一個 Node 可以有多個 Taints。先排除 Pod 已容忍的項目,再看剩下的 effect;只容忍其中一個,不代表通過其他 Taints。
| 未被容忍的 Taint effect | 新 Pod | 已經在 Node 上的 Pod |
|---|---|---|
NoSchedule |
不排到這個 Node | 不單純因此被驅逐 |
PreferNoSchedule |
盡量避免,但仍可能排入 | 不單純因此被驅逐 |
NoExecute |
不排到這個 Node | 觸發驅逐 |
常見匹配方式:Equal 比對 key、value 與 effect;Exists 比對 key 與 effect,不填 value。effect 留空可以匹配該 key 的所有 effects,因此不要把「同 key」直接當作「一定匹配」。
例如 Node 上有 maintenance=true:NoExecute,以下是 Pod 的 spec 片段:
tolerations:
- key: maintenance
operator: Equal
value: "true"
effect: NoExecute
tolerationSeconds: 120
若這個 Taint 在 Pod 已執行後加入,Pod 可容忍 120 秒;Taint 持續存在就會在期限到後被驅逐,期限前移除則不會因這個 Taint 被驅逐。不設定 tolerationSeconds 表示無期限容忍這個匹配的 NoExecute Taint;它不保證 Pod 不會因其他原因停止。
IgnoredDuringExecution 和 NoExecute 為什麼不衝突?
requiredDuringSchedulingIgnoredDuringExecution 的意思是:排程時必須符合;排程完成後,Node Label 改變不會單純因為這條 Affinity 不再符合而驅逐 Pod。
這個「忽略」只針對該 Affinity 條件在執行期間的變化,不會停用獨立的 Taint 驅逐機制。
假設 Pod 已在 node01 執行,以下各列獨立判斷,並假設沒有其他未容忍的 Taint:
| 事件 | 結果 |
|---|---|
| Node Label 改變,不再符合 Required Node Affinity | 不會單純因此被驅逐 |
Node 新增 NoSchedule Taint |
既有 Pod 不因此被驅逐 |
Node 新增 NoExecute,Pod 沒有匹配 Toleration |
Pod 會被驅逐 |
Node 新增 NoExecute,Pod 有匹配 Toleration,沒有時間限制 |
Pod 不會因這個 Taint 被驅逐 |
Node 新增 NoExecute,Pod 有 120 秒 Toleration |
若 Taint 持續存在,期限到後會被驅逐 |
「會被驅逐」描述控制平面的處置,不代表容器必定在同一瞬間停止。判斷時也要看實際 Pod 上的 Tolerations,不能只看手寫 YAML;例如 Kubernetes 通常會自動加入 not-ready/unreachable 的 300 秒 Tolerations。
一個完整例子:通行資格加上目的地
假設 Node 同時具備 dedicated=gpu label 與 dedicated=gpu:NoSchedule Taint。以下是 Pod 的 spec 片段:
nodeSelector:
dedicated: gpu
tolerations:
- key: dedicated
operator: Equal
value: gpu
effect: NoSchedule
nodeSelector 限定候選節點必須有該 label;Toleration 容許 Pod 通過匹配的 Taint。兩個條件都通過後,仍要檢查資源等其他要求。
只有 Toleration 時,Pod 也可能排到其他沒有這個 Taint 的 Node;只有 nodeSelector 時,匹配 label 的 Node 仍可能被未容忍的 Taint 擋住。
當場查與操作
以下是操作範例,node01 要換成實際節點名稱;新增/移除 Taint 會影響排程,NoExecute 還可能觸發驅逐。這篇筆記未對叢集執行這些操作。
alias k=kubectl
# 看 labels、Taints 與 Pod 實際落點
k get nodes --show-labels
k describe node node01
k get po -o wide
k describe po nginx
# 查 YAML 欄位
k explain po.spec.nodeSelector
k explain po.spec.affinity.nodeAffinity
k explain po.spec.tolerations
# 設定 label,以及新增/移除同一個 Taint
k label node node01 dedicated=gpu
k taint node node01 dedicated=gpu:NoSchedule
k taint node node01 dedicated=gpu:NoSchedule-
記住三個判斷層級 外層限制一起滿足;Required Node Affinity 的 Terms 才是 OR;Toleration 獨立處理 Taint。 先判斷「新 Pod 能不能排進去」,再判斷「已執行 Pod 會不會被驅逐」,就不會把 IgnoredDuringExecution 和 NoExecute 混在一起。
PriorityClass:Pod 有多重要
PriorityClass 是叢集層級的資源,縮寫 pc,不屬於某個 Namespace。它把類別名稱對應到整數優先權:value 越大,優先權越高;數字不是 CPU、Memory、時間或速度倍率。
例如 low-priority = 1000、high-priority = 100000,只代表後者優先;不代表執行速度快 100 倍。
| 欄位 | 意義 |
|---|---|
PriorityClass 的 value |
使用該類別的 Pod 所取得的優先權數值 |
Pod 的 spec.priorityClassName |
指定要用哪個類別 |
Pod 的 spec.priority |
由 Priority admission controller 依類別解析後填入,建立時不手動填寫 |
globalDefault: true |
讓未指定類別的新 Pod 使用這個類別 |
globalDefault: false |
不當全域預設,需要明確指定類別才會使用 |
preemptionPolicy: PreemptLowerPriority |
允許為這個 Pod 評估搶占較低優先權 Pod,為預設值 |
preemptionPolicy: Never |
不主動搶占其他 Pod;仍有排程優先權,也仍可能被更高優先權 Pod 搶占 |
沒指定類別、叢集也沒設定 global default 時,Pod priority 為 0。新增 global default 不會回頭修改既有 Pod 的 priority。
Pod:priorityClassName: high-priority
→ Priority admission 查到該 PriorityClass 的 value: 100000
→ Pod 自動取得 priority: 100000
→ Scheduler 依 PrioritySort 等機制處理排程
內建類別不等於一般 Pod 的預設類別
| 內建 PriorityClass | value | 用途 |
|---|---|---|
| system-cluster-critical | 2000000000 | 叢集關鍵元件 |
| system-node-critical | 2000001000 | 節點關鍵元件,優先權更高 |
system-node-critical 的 description「must not be moved」是在說明用途,文字本身不會鎖定 Node,也不是永遠不被驅逐的保證。一般應用依重要程度建立自己的類別。
搶占:移除較低優先權 Pod 後,能不能合法排入?
Preemption 是高優先權的待排程 Pod 找不到位置時,評估是否能終止某些較低優先權 Pod 來騰出空間。不是一看到低優先權 Pod 就驅逐;能直接排入就不需要搶占,也不保證搶占一定成功。
新 Pod 等待排程
→ 先找能直接放入的 Node
→ 找不到,且新 Pod 允許 PreemptLowerPriority
→ 假設移除某台 Node 上部分較低優先權 Pod
→ 若能滿足排程條件,才可能選它們作為搶占對象
| 卡住的原因 | 搶占是否可能解決? |
|---|---|
| Node 的 CPU/Memory 排程容量被其他 Pod requests 占滿 | 可能,移除 Pod 可釋放這些容量 |
| Node 有未容忍的阻擋性 Taint | 不能靠移除 Pod 解決,Taint 還在 |
| nodeSelector/Required Node Affinity 不符合 | 不能靠移除 Pod 解決,Node labels 沒改 |
| 新 Pod 單獨的需求就超過 Node 總可分配容量 | 不能,清空也放不下 |
例如唯一合格的 Node 有 4 CPU 可分配,低優先權 Pod A 的 request 占了 4 CPU;新 Pod B 的 request 是 2 CPU、priority 更高。若沒有其他條件阻擋,Scheduler 可能終止 A,等資源釋放後安排 B。A 不是被直接搬到另一台;若由 Deployment 管理,控制器會另外補建 Pod。
CPU/Memory 是否放得下主要看 requests 與 Node allocatable,不是直接拿 limits 或當下 CPU 使用率比較。Priority 也不會略過 Taint、Affinity 或其他硬限制;節點條件必須同時滿足前面的 Node Selector、Affinity 與 Taint/Toleration 規則。
Priority 和 QoS 是不同維度:Priority 表示重要程度;QoS 由 CPU/Memory requests/limits 等設定決定。搶占也不等同 kubelet 的節點壓力驅逐。
建立與檢查:用 pc
k create pc high-priority --value=100000
k create pc low-priority --value=1000
k get pc
k describe pc high-priority
k describe pc system-node-critical
--preemption-policy 預設為 PreemptLowerPriority,--global-default 預設為 false,所以題目要求允許搶占、不要設成 global default 時,上面兩條建立指令就足夠。不允許主動搶占時才加 --preemption-policy=Never。
Pod 指定 PriorityClass:先產生 YAML
k run -h 查指令參數;run 沒有直接設定 PriorityClass 的參數,所以先產生 YAML,再補欄位,不必記一長串 JSON overrides。
k run low-prio-pod --image=nginx -n default --dry-run=client -o yaml > pod.yaml
vi pod.yaml
在 spec 下加入 priorityClassName,與 containers 同層:
spec:
priorityClassName: low-priority
containers:
- name: low-prio-pod
image: nginx
k apply -f pod.yaml
k describe po low-prio-pod -n default
# 忘記欄位名稱時查 explain,不是猜 --priority-...
k explain po.spec | grep -i priority
replace –force 的 priority: 0 錯誤
把 critical-app 改成 high-priority 時,從既有物件匯出的 YAML 可能保留舊的 priority:
spec:
priority: 0
priorityClassName: high-priority
執行 k replace --force -f pod.yaml 後出現:
pod "critical-app" deleted from default namespace
Error from server (Forbidden): pods "critical-app" is forbidden: the integer value of priority (0) must not be provided in pod spec; priority admission controller computed 100000 from the given PriorityClass name
不是 replace 不支援,而是舊的 priority: 0 和 high-priority 算出的 100000 衝突。 Priority admission controller 會依類別填入 priority;建立時只指定 priorityClassName,刪掉 priority 整行,不要把它手改成 100000。
spec:
priorityClassName: high-priority
保留原本 image、resources、volumes 等所需設定。重建匯出的 Pod 時,也移除 status、metadata 中的 uid/resourceVersion/creationTimestamp/managedFields 等伺服器欄位;若希望重新排程,移除舊的 spec.nodeName,不要帶著先前的綁定直接重建。
若錯誤已顯示舊 Pod 刪除成功,修完檔案後直接 create。 以下以 kubectl 保留的暫存檔為例,使用時換成實際檔名:
vi /tmp/kubectl-edit-3323068860.yaml
k create -f /tmp/kubectl-edit-3323068860.yaml -n default
k get po critical-app -n default -w
看到 Running 後按 Ctrl-C,再查 k describe po critical-app -n default,確認 Priority 為 100000、Priority Class 為 high-priority。若未 Running,先看 Events,分辨 FailedScheduling、image pull 或其他啟動問題。
replace --force 是「刪掉舊物件,再建立新物件」,不會略過 Admission,也不是原子操作:建立失敗不會自動還原舊 Pod。若舊 Pod 還存在,先清理重建用的 YAML,再執行題目要求的重建:
k replace --force -f pod.yaml -n default
以上重建步驟適用本題直接管理的 Pod;Deployment 管理的 Pod 應修改其 spec.template,避免只改掉單一副本。
Multi-scheduler:多個程序,不一定是不同二進位檔
kube-scheduler 本身是可獨立執行的二進位程式;multi-scheduler 是部署方式,不是另一個叫 multi-scheduler 的執行檔。
可以使用相同 kube-scheduler 程式,啟動兩個獨立程序,分別讀取不同設定;也能把它們放在不同的 Pod/容器中。第二個 Scheduler 不一定要重新寫或編譯,若需要不同自訂 plugins 或自行實作,才可能改用另一個自訂 build/執行檔。
同一套 kube-scheduler 程式
├─ 程序 A:設定 default-scheduler → 接手對應名稱的 Pod
└─ 程序 B:設定 my-scheduler → 接手對應名稱的 Pod
第二個程序的啟動概念是:
kube-scheduler --config=/etc/kubernetes/my-scheduler/config.yaml
這是 Scheduler 程序的啟動方式,還需設定 API 連線、ServiceAccount/RBAC 與必要部署項目;不是執行這一行就完成所有安裝。官方的 Multiple Schedulers 範例 是將設定存入 ConfigMap,掛載給另一個 Scheduler Pod,再由容器中的 kube-scheduler 讀取。
單副本實驗設定可以寫成:
apiVersion: kubescheduler.config.k8s.io/v1
kind: KubeSchedulerConfiguration
profiles:
- schedulerName: my-scheduler
leaderElection:
leaderElect: false
Pod 的 spec.schedulerName: my-scheduler 要匹配設定中的 profile 名稱;它不是拿來對應 Scheduler Pod 的 metadata.name,也不是二進位檔名。沒填時預設為 default-scheduler;指定的 Scheduler 沒運作,Pod 會繼續等,不會自動改交給預設 Scheduler。不同 Scheduler 仍共用叢集的 Node 資源,不會因名稱不同就自動切出獨立資源池。
| 情境 | 程序與用途 |
|---|---|
| Multi-scheduler | 不同排程器程序處理不同 schedulerName 的 Pod,可使用不同策略 |
| Multi-profile | 同一個 kube-scheduler 程序載入多個 profiles,每個 profile 有自己的 schedulerName 與 plugin 設定 |
| Scheduler 高可用副本 | 同一個邏輯 Scheduler 的多個副本,使用同一組 leader election lock 選出 leader |
Multi-profile 的設定方式見 Scheduler Profiles。不同邏輯 Scheduler 各自做高可用時,要使用不同的 leader election lock,不能誤用同一個鎖,否則可能只剩其中一組在工作。
一般應用先用預設 Scheduler 的 nodeSelector、Affinity、Taint/Toleration 與 requests 解決需求;需要不同排程決策或特殊工作協調時,再評估多 Scheduler 或 profiles。
Pending 排程題:最短檢查順序
k get po -o wide
k describe po <Pod名稱>
k get nodes --show-labels
k describe node <候選Node名稱>
k get pc
- 先看 Events 是否為
FailedScheduling;若是映像或啟動錯誤,改查 Node 上的執行階段。 - 確認
schedulerName有對應且運作中的 Scheduler。 - 逐一核對 selector/required affinity、未容忍 Taint、requests 與 Node allocatable。
- 再看 Priority/Preemption;提高優先權不能解決 label 不符、未容忍 Taint 或單一 Pod 需求超過整台 Node 容量。