
從 kube-scheduler 的角度來看,它是通過一系列算法計算出最佳節(jié)點運行 Pod,當出現(xiàn)新的 Pod 進行調(diào)度時,調(diào)度程序會根據(jù)其當時對 Kubernetes 集群的資源描述做出最佳調(diào)度決定,但是 Kubernetes 集群是非常動態(tài)的,由于整個集群范圍內(nèi)的變化,比如一個節(jié)點為了維護,我們先執(zhí)行了驅(qū)逐操作,這個節(jié)點上的所有 Pod 會被驅(qū)逐到其他節(jié)點去,但是當我們維護完成后,之前的 Pod 并不會自動回到該節(jié)點上來,因為 Pod 一旦被綁定了節(jié)點是不會觸發(fā)重新調(diào)度的,由于這些變化,Kubernetes 集群在一段時間內(nèi)就可能會出現(xiàn)不均衡的狀態(tài),所以需要均衡器來重新平衡集群。
當然我們可以去手動做一些集群的平衡,比如手動去刪掉某些 Pod,觸發(fā)重新調(diào)度就可以了,但是顯然這是一個繁瑣的過程,也不是解決問題的方式。為了解決實際運行中集群資源無法充分利用或浪費的問題,可以使用 descheduler 組件對集群的 Pod 進行調(diào)度優(yōu)化,descheduler 可以根據(jù)一些規(guī)則和配置策略來幫助我們重新平衡集群狀態(tài),其核心原理是根據(jù)其策略配置找到可以被移除的 Pod 并驅(qū)逐它們,其本身并不會進行調(diào)度被驅(qū)逐的 Pod,而是依靠默認的調(diào)度器來實現(xiàn),目前支持的策略有:
- RemoveDuplicates
- LowNodeUtilization
- HighNodeUtilization
- RemovePodsViolatingInterPodAntiAffinity
- RemovePodsViolatingNodeAffinity
- RemovePodsViolatingNodeTaints
- RemovePodsViolatingTopologySpreadConstraint
- RemovePodsHavingTooManyRestarts
- PodLifeTime
- RemoveFailedPods
這些策略都是可以啟用或者禁用的,作為策略的一部分,也可以配置與策略相關(guān)的一些參數(shù),默認情況下,所有策略都是啟用的。另外,還有一些通用配置,如下:
- nodeSelector:限制要處理的節(jié)點
- evictLocalStoragePods: 驅(qū)逐使用 LocalStorage 的 Pods
- ignorePvcPods: 是否忽略配置 PVC 的 Pods,默認是 False
- maxNoOfPodsToEvictPerNode:節(jié)點允許的最大驅(qū)逐 Pods 數(shù)
我們可以通過如下所示的 DeschedulerPolicy 來配置:
apiVersion: "descheduler/v1alpha2"
kind: "DeschedulerPolicy"
nodeSelector: "node=node1" # 如果沒有設(shè)置,所有內(nèi)容都將被處理,無需進行此設(shè)置。
maxNoOfPodsToEvictPerNode: 5000 # 如果沒有設(shè)置,就不需要進行限制。
maxNoOfPodsToEvictPerNamespace: 5000
profiles:
- name: ProfileName
pluginConfig:
- name: "DefaultEvictor"
args:
evictSystemCriticalPods: true
evictFailedBarePods: true
evictLocalStoragePods: true
nodeFit: true
plugins:
evict:
enabled:
- "DefaultEvictor"
deschedule:
enabled:
- ...
balance:
enabled:
- ...
[...]
安裝
descheduler 可以以 CronJob 或者 Deployment 的形式運行在 k8s 集群內(nèi),同樣我們可以使用 Helm Chart 來安裝 descheduler:
? helm repo add descheduler https://kubernetes-sigs.github.io/descheduler/
通過 Helm Chart 我們可以配置 descheduler 以 CronJob 或者 Deployment 方式運行,默認情況下 descheduler 會以一個 critical pod 運行,以避免被自己或者 kubelet 驅(qū)逐了,需要確保集群中有 system-cluster-critical 這個 Priorityclass:
? kubectl get priorityclass system-cluster-critical
NAME VALUE GLOBAL-DEFAULT AGE
system-cluster-critical 2000000000 false 87d
使用 Helm Chart 安裝默認情況下會以 CronJob 的形式運行,執(zhí)行周期為 schedule: "*/2 * * * *",這樣每隔兩分鐘會執(zhí)行一次 descheduler 任務,默認的配置策略如下所示:
apiVersion: v1
kind: ConfigMap
metadata:
name: descheduler
data:
policy.yaml: |
apiVersion: "descheduler/v1alpha1"
kind: "DeschedulerPolicy"
strategies:
LowNodeUtilization:
enabled: true
params:
nodeResourceUtilizationThresholds:
targetThresholds:
cpu: 50
memory: 50
pods: 50
thresholds:
cpu: 20
memory: 20
pods: 20
RemoveDuplicates:
enabled: true
RemovePodsHavingTooManyRestarts:
enabled: true
params:
podsHavingTooManyRestarts:
includingInitContainers: true
podRestartThreshold: 100
RemovePodsViolatingInterPodAntiAffinity:
enabled: true
RemovePodsViolatingNodeAffinity:
enabled: true
params:
nodeAffinityType:
- requiredDuringSchedulingIgnoredDuringExecution
RemovePodsViolatingNodeTaints:
enabled: true
RemovePodsViolatingTopologySpreadConstraint:
enabled: true
params:
includeSoftConstraints: false
通過配置 DeschedulerPolicy 的 strategies,可以指定 descheduler 的執(zhí)行策略,這些策略都是可以啟用或禁用的,下面我們會詳細介紹,這里我們使用默認策略即可,使用如下命令直接安裝即可:
? helm upgrade --install descheduler descheduler/descheduler --set image.repository=cnych/descheduler -n kube-system
部署完成后會創(chuàng)建一個 CronJob 資源對象來平衡集群狀態(tài):
? kubectl get cronjob -n kube-system
NAME SCHEDULE SUSPEND ACTIVE LAST SCHEDULE AGE
descheduler */2 * * * * False 1 8s 117s
? kubectl get job -n kube-system
NAME COMPLETIONS DURATION AGE
descheduler-28032982 1/1 15s 17s
? kubectl get pods -n kube-system -l job-name=descheduler-28032982
NAME READY STATUS RESTARTS AGE
descheduler-28032982-vxn24 0/1 Completed 0 31s
正常情況下就會創(chuàng)建一個對應的 Job 來執(zhí)行 descheduler 任務,我們可以通過查看日志可以了解做了哪些平衡操作:
? kubectl logs -f descheduler-28032982-vxn24 -nkube-system
I0420 08:22:10.019936 1 named_certificates.go:53] "Loaded SNI cert" index=0 certName="self-signed loopback" certDetail="\"apiserver-loopback-client@1681978930\" [serving] validServingFor=[apiserver-loopback-client] issuer=\"apiserver-loopback-client-ca@1681978929\" (2023-04-20 07:22:09 +0000 UTC to 2024-04-19 07:22:09 +0000 UTC (now=2023-04-20 08:22:10.019885292 +0000 UTC))"
I0420 08:22:10.020138 1 secure_serving.go:210] Serving securely on [::]:10258
I0420 08:22:10.020301 1 tlsconfig.go:240] "Starting DynamicServingCertificateController"
I0420 08:22:10.021237 1 policyconfig.go:211] converting Deschedule plugin: %sRemovePodsViolatingInterPodAntiAffinity
I0420 08:22:10.021255 1 policyconfig.go:211] converting Deschedule plugin: %sRemovePodsViolatingNodeAffinity
I0420 08:22:10.021262 1 policyconfig.go:211] converting Deschedule plugin: %sRemovePodsViolatingNodeTaints
I0420 08:22:10.021269 1 policyconfig.go:202] converting Balance plugin: %sRemovePodsViolatingTopologySpreadConstraint
I0420 08:22:10.021280 1 policyconfig.go:202] converting Balance plugin: %sLowNodeUtilization
I0420 08:22:10.021296 1 policyconfig.go:202] converting Balance plugin: %sRemoveDuplicates
I0420 08:22:10.021312 1 policyconfig.go:211] converting Deschedule plugin: %sRemovePodsHavingTooManyRestarts
# ......
I0420 08:22:11.630980 1 removeduplicates.go:162] "Duplicate found" pod="kruise-system/kruise-controller-manager-7d78fc5c97-pxsqx"
I0420 08:22:11.630997 1 removeduplicates.go:103] "Processing node" node="node2"
I0420 08:22:11.631052 1 removeduplicates.go:103] "Processing node" node="node3"
I0420 08:22:11.631113 1 removeduplicates.go:103] "Processing node" node="master1"
I0420 08:22:11.631184 1 removeduplicates.go:194] "Adjusting feasible nodes" owner={namespace:kruise-system kind:ReplicaSet name:kruise-controller-manager-7d78fc5c97 imagesHash:openkruise/kruise-manager:v1.3.0} from=4 to=3
I0420 08:22:11.631200 1 removeduplicates.go:203] "Average occurrence per node" node="node1" ownerKey={namespace:kruise-system kind:ReplicaSet name:kruise-controller-manager-7d78fc5c97 imagesHash:openkruise/kruise-manager:v1.3.0} avg=1
I0420 08:22:11.647438 1 evictions.go:162] "Evicted pod" pod="kruise-system/kruise-controller-manager-7d78fc5c97-pxsqx" reasnotallow="" strategy="RemoveDuplicates" node="node1"
I0420 08:22:11.647494 1 descheduler.go:408] "Number of evicted pods" totalEvicted=1
I0420 08:22:11.647583 1 reflector.go:227] Stopping reflector *v1.Namespace (0s) from k8s.io/client-go/informers/factory.go:150
I0420 08:22:11.647702 1 reflector.go:227] Stopping reflector *v1.PriorityClass (0s) from k8s.io/client-go/informers/factory.go:150
I0420 08:22:11.647761 1 tlsconfig.go:255] "Shutting down DynamicServingCertificateController"
I0420 08:22:11.647764 1 reflector.go:227] Stopping reflector *v1.Node (0s) from k8s.io/client-go/informers/factory.go:150
I0420 08:22:11.647811 1 secure_serving.go:255] Stopped listening on [::]:10258
......
從日志中我們就可以清晰的知道因為什么策略驅(qū)逐了哪些 Pods。
PDB
由于使用 descheduler 會將 Pod 驅(qū)逐進行重調(diào)度,但是如果一個服務的所有副本都被驅(qū)逐的話,則可能導致該服務不可用。如果服務本身存在單點故障,驅(qū)逐的時候肯定就會造成服務不可用了,這種情況我們強烈建議使用反親和性和多副本來避免單點故障,但是如果服務本身就被打散在多個節(jié)點上,這些 Pod 都被驅(qū)逐的話,這個時候也會造成服務不可用了,這種情況下我們可以通過配置 PDB(PodDisruptionBudget) 對象來避免所有副本同時被刪除,比如我們可以設(shè)置在驅(qū)逐的時候某應用最多只有一個副本不可用,則創(chuàng)建如下所示的資源清單即可:
# pdb-demo.yaml
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: pdb-demo
spec:
maxUnavailable: 1 # 設(shè)置最多不可用的副本數(shù)量,或者使用 minAvailable,可以使用整數(shù)或百分比
selector:
matchLabels: # 匹配Pod標簽
app: demo
關(guān)于 PDB 的更多詳細信息可以查看官方文檔:https://kubernetes.io/docs/tasks/run-application/configure-pdb/。
所以如果我們使用 descheduler 來重新平衡集群狀態(tài),那么我們強烈建議給應用創(chuàng)建一個對應的 PodDisruptionBudget 對象進行保護。
策略
PodLifeTime:驅(qū)逐超過指定時間限制的 pod
該策略用于驅(qū)逐比 maxPodLifeTimeSeconds 更舊的 Pods,可以通過 podStatusPhases 來配置哪類狀態(tài)的 Pods 會被驅(qū)逐,建議為每個應用程序創(chuàng)建一個 PDB,以確保應用程序的可用性,比如我們可以配置如下所示的策略來驅(qū)逐運行超過 7 天的 Pod:
apiVersion: "descheduler/v1alpha1"
kind: "DeschedulerPolicy"
strategies:
"PodLifeTime":
enabled: true
params:
maxPodLifeTimeSeconds: 604800 # Pods 運行最多7天
RemoveDuplicates
該策略確保只有一個和 Pod 關(guān)聯(lián)的 RS、Deployment 或者 Job 資源對象運行在同一節(jié)點上。如果還有更多的 Pod 則將這些重復的 Pod 進行驅(qū)逐,以便更好地在集群中分散 Pod。如果某些節(jié)點由于某些原因崩潰了,這些節(jié)點上的 Pod 漂移到了其他節(jié)點,導致多個與 RS 關(guān)聯(lián)的 Pod 在同一個節(jié)點上運行,就有可能發(fā)生這種情況,一旦出現(xiàn)故障的節(jié)點再次準備就緒,就可以啟用該策略來驅(qū)逐這些重復的 Pod。

配置策略的時候,可以指定參數(shù) excludeOwnerKinds 用于排除類型,這些類型下的 Pod 不會被驅(qū)逐:
apiVersion: "descheduler/v1alpha1"
kind: "DeschedulerPolicy"
strategies:
"RemoveDuplicates":
enabled: true
params:
removeDuplicates:
excludeOwnerKinds:
- "ReplicaSet"
LowNodeUtilization
該策略主要用于查找未充分利用的節(jié)點,并從其他節(jié)點驅(qū)逐 Pod,以便 kube-scheduler 重新將它們調(diào)度到未充分利用的節(jié)點上。該策略的參數(shù)可以通過字段 nodeResourceUtilizationThresholds 進行配置。
節(jié)點的利用率不足可以通過配置 thresholds 閾值參數(shù)來確定,可以通過 CPU、內(nèi)存和 Pods 數(shù)量的百分比進行配置。如果節(jié)點的使用率均低于所有閾值,則認為該節(jié)點未充分利用。

此外,還有一個可配置的閾值 targetThresholds,用于計算可能驅(qū)逐 Pods 的潛在節(jié)點,該參數(shù)也可以配置 CPU、內(nèi)存以及 Pods 數(shù)量的百分比進行配置。thresholds 和 targetThresholds 可以根據(jù)你的集群需求進行動態(tài)調(diào)整,如下所示示例:
apiVersion: "descheduler/v1alpha1"
kind: "DeschedulerPolicy"
strategies:
"LowNodeUtilization":
enabled: true
params:
nodeResourceUtilizationThresholds:
thresholds:
"cpu": 20
"memory": 20
"pods": 20
targetThresholds:
"cpu": 50
"memory": 50
"pods": 50
需要注意的是:
- 僅支持以下三種資源類型:cpu、memory、pods
- thresholds 和 targetThresholds 必須配置相同的類型
- 參數(shù)值的訪問是 0-100(百分制)
- 相同的資源類型,thresholds 的配置不能高于 targetThresholds 的配置
如果未指定任何資源類型,則默認是 100%,以避免節(jié)點從未充分利用變?yōu)檫^度利用。和 LowNodeUtilization 策略關(guān)聯(lián)的另一個參數(shù)是 numberOfNodes,只有當未充分利用的節(jié)點數(shù)大于該配置值的時候,才可以配置該參數(shù)來激活該策略,該參數(shù)對于大型集群非常有用,其中有一些節(jié)點可能會頻繁使用或短期使用不足,默認情況下,numberOfNodes 為 0。
RemovePodsViolatingInterPodAntiAffinity
該策略可以確保從節(jié)點中刪除違反 Pod 反親和性的 Pod,比如某個節(jié)點上有 podA 這個 Pod,并且 podB 和 podC(在同一個節(jié)點上運行)具有禁止它們在同一個節(jié)點上運行的反親和性規(guī)則,則 podA 將被從該節(jié)點上驅(qū)逐,以便 podB 和 podC 運行正常運行。當 podB 和 podC 已經(jīng)運行在節(jié)點上后,反親和性規(guī)則被創(chuàng)建就會發(fā)送這樣的問題。

要禁用該策略,直接配置成 false 即可:
apiVersion: "descheduler/v1alpha1"
kind: "DeschedulerPolicy"
strategies:
"RemovePodsViolatingInterPodAntiAffinity":
enabled: false
RemovePodsViolatingNodeTaints
該策略可以確保從節(jié)點中刪除違反 NoSchedule 污點的 Pod,比如有一個名為 podA 的 Pod,通過配置容忍 key=value:NoSchedule 允許被調(diào)度到有該污點配置的節(jié)點上,如果節(jié)點的污點隨后被更新或者刪除了,則污點將不再被 Pods 的容忍滿足,然后將被驅(qū)逐:
apiVersion: "descheduler/v1alpha1"
kind: "DeschedulerPolicy"
strategies:
"RemovePodsViolatingNodeTaints":
enabled: true
RemovePodsViolatingNodeAffinity
該策略確保從節(jié)點中刪除違反節(jié)點親和性的 Pod。比如名為 podA 的 Pod 被調(diào)度到了節(jié)點 nodeA,podA 在調(diào)度的時候滿足了節(jié)點親和性規(guī)則 requiredDuringSchedulingIgnoredDuringExecution,但是隨著時間的推移,節(jié)點 nodeA 不再滿足該規(guī)則了,那么如果另一個滿足節(jié)點親和性規(guī)則的節(jié)點 nodeB 可用,則 podA 將被從節(jié)點 nodeA 驅(qū)逐,如下所示的策略配置示例:
apiVersion: "descheduler/v1alpha1"
kind: "DeschedulerPolicy"
strategies:
"RemovePodsViolatingNodeAffinity":
enabled: true
params:
nodeAffinityType:
- "requiredDuringSchedulingIgnoredDuringExecution"
RemovePodsViolatingTopologySpreadConstraint
該策略確保從節(jié)點驅(qū)逐違反拓撲分布約束的 Pods,具體來說,它試圖驅(qū)逐將拓撲域平衡到每個約束的 ??maxSkew?
? 內(nèi)所需的最小 Pod 數(shù),不過該策略需要 k8s 版本高于 1.18 才能使用。
默認情況下,此策略僅處理硬約束,如果將參數(shù) ??includeSoftConstraints?
? 設(shè)置為 True,也將支持軟約束。
apiVersion: "descheduler/v1alpha1"
kind: "DeschedulerPolicy"
strategies:
"RemovePodsViolatingTopologySpreadConstraint":
enabled: true
params:
includeSoftConstraints: false
RemovePodsHavingTooManyRestarts
該策略確保從節(jié)點中刪除重啟次數(shù)過多的 Pods,它的參數(shù)包括 podRestartThreshold(這是應將 Pod 逐出的重新啟動次數(shù)),以及包括InitContainers,它確定在計算中是否應考慮初始化容器的重新啟動,策略配置如下所示:
apiVersion: "descheduler/v1alpha1"
kind: "DeschedulerPolicy"
strategies:
"RemovePodsHavingTooManyRestarts":
enabled: true
params:
podsHavingTooManyRestarts:
podRestartThreshold: 100
includingInitContainers: true
Filter Pods
在驅(qū)逐 Pods 的時候,有時并不需要所有 Pods 都被驅(qū)逐,descheduler 提供了兩種主要的方式進行過濾:命名空間過濾和優(yōu)先級過濾。
命名空間過濾
該策略可以配置是包含還是排除某些名稱空間。可以使用該策略的有:
- PodLifeTime
- RemovePodsHavingTooManyRestarts
- RemovePodsViolatingNodeTaints
- RemovePodsViolatingNodeAffinity
- RemovePodsViolatingInterPodAntiAffinity
- RemoveDuplicates
- RemovePodsViolatingTopologySpreadConstraint
比如只驅(qū)逐某些命令空間下的 Pods,則可以使用 include 參數(shù)進行配置,如下所示:
apiVersion: "descheduler/v1alpha1"
kind: "DeschedulerPolicy"
strategies:
"PodLifeTime":
enabled: true
params:
podLifeTime:
maxPodLifeTimeSeconds: 86400
namespaces:
include:
- "namespace1"
- "namespace2"
又或者要排除掉某些命令空間下的 Pods,則可以使用 exclude 參數(shù)配置,如下所示:
apiVersion: "descheduler/v1alpha1"
kind: "DeschedulerPolicy"
strategies:
"PodLifeTime":
enabled: true
params:
podLifeTime:
maxPodLifeTimeSeconds: 86400
namespaces:
exclude:
- "namespace1"
- "namespace2"
優(yōu)先級過濾
所有策略都可以配置優(yōu)先級閾值,只有在該閾值以下的 Pod 才會被驅(qū)逐,我們可以通過設(shè)置 thresholdPriorityClassName(將閾值設(shè)置為指定優(yōu)先級類別的值)或 thresholdPriority(直接設(shè)置閾值)參數(shù)來指定該閾值。默認情況下,該閾值設(shè)置為 system-cluster-critical 這個 PriorityClass 類的值。
比如使用 thresholdPriority:
apiVersion: "descheduler/v1alpha1"
kind: "DeschedulerPolicy"
strategies:
"PodLifeTime":
enabled: true
params:
podLifeTime:
maxPodLifeTimeSeconds: 86400
thresholdPriority: 10000
或者使用 thresholdPriorityClassName 進行過濾:
apiVersion: "descheduler/v1alpha1"
kind: "DeschedulerPolicy"
strategies:
"PodLifeTime":
enabled: true
params:
podLifeTime:
maxPodLifeTimeSeconds: 86400
thresholdPriorityClassName: "priorityclass1"
不過需要注意不能同時配置 thresholdPriority 和 thresholdPriorityClassName,如果指定的優(yōu)先級類不存在,則 descheduler 不會創(chuàng)建它,并且會引發(fā)錯誤。
注意事項
當使用 descheduler 驅(qū)除 Pods 的時候,需要注意以下幾點:
- 關(guān)鍵性 Pod 不會被驅(qū)逐,比如 priorityClassName 設(shè)置為 system-cluster-critical 或 system-node-critical 的 Pod。
- 不屬于 RS、Deployment 或 Job 管理的 Pods 不會被驅(qū)逐。
- DaemonSet 創(chuàng)建的 Pods 不會被驅(qū)逐。
- 使用 LocalStorage 的 Pod 不會被驅(qū)逐,除非設(shè)置 evictLocalStoragePods: true。
- 具有 PVC 的 Pods 不會被驅(qū)逐,除非設(shè)置 ignorePvcPods: true。
- 在 LowNodeUtilization 和 RemovePodsViolatingInterPodAntiAffinity 策略下,Pods 按優(yōu)先級從低到高進行驅(qū)逐,如果優(yōu)先級相同,Besteffort 類型的 Pod 要先于 Burstable 和 Guaranteed 類型被驅(qū)逐。
- annotations 中帶有 descheduler.alpha.kubernetes.io/evict 字段的 Pod 都可以被驅(qū)逐,該注釋用于覆蓋阻止驅(qū)逐的檢查,用戶可以選擇驅(qū)逐哪個 Pods。
- 如果 Pods 驅(qū)逐失敗,可以設(shè)置 --v=4 從 descheduler 日志中查找原因,如果驅(qū)逐違反 PDB 約束,則不會驅(qū)逐這類 Pods。