解決k8s排程不均衡問題

劼哥stone發表於2022-06-20

前言

在近期的工作中,我們發現 k8s 叢集中有些節點資源使用率很高,有些節點資源使用率很低,我們嘗試重新部署應用和驅逐 Pod,發現並不能有效解決負載不均衡問題。在學習了 Kubernetes 排程原理之後,重新調整了 Request 配置,引入了排程外掛,才最終解決問題。這篇就來跟大家分享 Kubernetes 資源和排程相關知識,以及如何解決k8s排程不均衡問題。

Kubernetes 的資源模型

在 Kubernetes 裡,Pod 是最小的原子排程單位。這也就意味著,所有跟排程和資源管理相關的屬性都應該是屬於 Pod 物件的欄位。而這其中最重要的部分,就是 Pod 的 CPU 和記憶體配置。
像 CPU 這樣的資源被稱作“可壓縮資源”(compressible resources)。它的典型特點是,當可壓縮資源不足時,Pod 只會“飢餓”,但不會退出。
而像記憶體這樣的資源,則被稱作“不可壓縮資源(incompressible resources)。當不可壓縮資源不足時,Pod 就會因為 OOM(Out-Of-Memory)被核心殺掉。
Pod 可以由多個 Container 組成,所以 CPU 和記憶體資源的限額,是要配置在每個 Container 的定義上的。這樣,Pod 整體的資源配置,就由這些 Container 的配置值累加得到。
Kubernetes 裡 Pod 的 CPU 和記憶體資源,實際上還要分為 limits 和 requests 兩種情況:

spec.containers[].resources.limits.cpu
spec.containers[].resources.limits.memory
spec.containers[].resources.requests.cpu
spec.containers[].resources.requests.memory

這兩者的區別其實非常簡單:在排程的時候,kube-scheduler 只會按照 requests 的值進行排程。而在真正設定 Cgroups 限制的時候,kubelet 則會按照 limits 的值來進行設定。
這是因為在實際場景中,大多數作業使用到的資源其實遠小於它所請求的資源限額,這種策略能有效的提高整體資源的利用率。

Kubernetes 的服務質量

服務質量 QoS 的英文全稱為 Quality of Service。在 Kubernetes 中,每個 Pod 都有個 QoS 標記,通過這個 Qos 標記來對 Pod 進行服務質量管理,它確定 Pod 的排程和驅逐優先順序。在 Kubernetes 中,Pod 的 QoS 服務質量一共有三個級別:

  • Guaranteed:當 Pod 裡的每一個 Container 都同時設定了 requests 和 limits,並且 requests 和 limits 值相等的時候,這個 Pod 就屬於 Guaranteed 類別 。
  • Burstable:而當 Pod 不滿足 Guaranteed 的條件,但至少有一個 Container 設定了 requests。那麼這個 Pod 就會被劃分到 Burstable 類別。
  • BestEffort:而如果一個 Pod 既沒有設定 requests,也沒有設定 limits,那麼它的 QoS 類別就是 BestEffort。

具體地說,當 Kubernetes 所管理的宿主機上不可壓縮資源短缺時,就有可能觸發 Eviction 驅逐。目前,Kubernetes 為你設定的 Eviction 的預設閾值如下所示:

memory.available<100Mi
nodefs.available<10%
nodefs.inodesFree<5%
imagefs.available<15%

當宿主機的 Eviction 閾值達到後,就會進入 MemoryPressure 或者 DiskPressure 狀態,從而避免新的 Pod 被排程到這臺宿主機上,然後 kubelet 會根據 QoS 的級別來挑選 Pod 進行驅逐,具體驅逐優先順序是:BestEffort -> Burstable -> Guaranteed。
QoS 的級別是通過 Linux 核心 OOM 分數值來實現的,OOM 分數值取值範圍在-1000 ~1000之間。在 Kubernetes 中,常用服務的 OOM 的分值如下:

-1000  => sshd等程式    
-999   => Kubernetes 管理程式
-998   => Guaranteed Pod
0      => 其他程式    0
2~999  => Burstable Pod     
1000   => BestEffort Pod     

OOM 分數越高,就代表這個 Pod 的優先順序越低,在出現資源競爭的時候,就越早被殺掉,分數為-999和-1000的程式永遠不會因為 OOM 而被殺掉。

劃重點:如果期望 Pod 儘可能的不被驅逐,就應當把 Pod 裡的每一個 Container 的 requests 和 limits 都設定齊全,並且 requests 和 limits 值要相等。

Kubernetes 的排程策略

kube-scheduler 是 Kubernetes 叢集的預設排程器,它的主要職責是為一個新建立出來的 Pod,尋找一個最合適的 Node。kube-scheduler 給一個 Pod 做排程選擇包含三個步驟:

過濾(Predicate)

過濾階段,首先遍歷全部節點,過濾掉不滿足條件的節點,屬於強制性規則,這一階段輸出的所有滿足要求的 Node 將被記錄並作為第二階段的輸入,如果所有的節點都不滿足條件,那麼 Pod 將會一直處於 Pending 狀態,直到有節點滿足條件,在這期間排程器會不斷的重試。
排程器會根據限制條件和複雜性依次進行以下過濾檢查,檢查順序儲存在一個名為 PredicateOrdering() 的函式中,具體如下表格:

演算法名稱預設順序詳細說明
CheckNodeUnschedulablePred強制1檢查節點是否可排程;
GeneralPred2是一組聯合檢查,包含了:HostNamePred、PodFitsResourcesPred、PodFitsHostPortsPred、MatchNodeSelectorPred 4個檢查
HostNamePred3檢查 Pod 指定的 Node 名稱是否和 Node 名稱相同;
PodFitsHostPortsPred4檢查 Pod 請求的埠(網路協議型別)在節點上是否可用;
MatchNodeSelectorPred5檢查是否匹配 NodeSelector 節點選擇器的設定;
PodFitsResourcesPred6檢查節點的空閒資源(例如,CPU 和記憶體)是否滿足 Pod 的要求;
NoDiskConflictPred7根據 Pod 請求的卷是否在節點上已經掛載,評估 Pod 和節點是否匹配;
PodToleratesNodeTaintsPred強制8檢查 Pod 的容忍是否能容忍節點的汙點;
CheckNodeLabelPresencePred9檢測 NodeLabel 是否存在;
CheckServiceAffinityPred10檢測服務的親和;
MaxEBSVolumeCountPred11已廢棄,檢測 Volume 數量是否超過雲服務商 AWS 的儲存服務的配置限制;
MaxGCEPDVolumeCountPred12已廢棄,檢測 Volume 數量是否超過雲服務商 Google Cloud 的儲存服務的配置限制;
MaxCSIVolumeCountPred13Pod 附加 CSI 卷的數量,判斷是否超過配置的限制;
MaxAzureDiskVolumeCountPred14已廢棄,檢測 Volume 數量是否超過雲服務商 Azure 的儲存服務的配置限制;
MaxCinderVolumeCountPred15已廢棄,檢測 Volume 數量是否超過雲服務商 OpenStack 的儲存服務的配置限制;
CheckVolumeBindingPred16基於 Pod 的卷請求,評估 Pod 是否適合節點,這裡的捲包括繫結的和未繫結的 PVC 都適用;
NoVolumeZoneConflictPred17給定該儲存的故障區域限制, 評估 Pod 請求的卷在節點上是否可用;
EvenPodsSpreadPred18檢測 Node 是否滿足拓撲傳播限制;
MatchInterPodAffinityPred19檢測是否匹配 Pod 的親和與反親和的設定;

可以看出,Kubernetes 正在逐步移除某個具體雲服務商的服務的相關程式碼,而使用介面(Interface)來擴充套件功能。

打分(Priority)

打分階段,通過 Priority 策略對可用節點進行評分,最終選出最優節點。具體是用一組打分函式處理每一個可用節點,每一個打分函式會返回一個 0~100 的分數,分數越高表示節點越優, 同時每一個函式也會對應一個權重值。將每個打分函式的計算得分乘以權重,然後再將所有打分函式的得分相加,從而得出節點的最終優先順序分值。權重可以讓管理員定義優選函式傾向性的能力,其計算優先順序的得分公式如下:

finalScoreNode = (weight1 * priorityFunc1) + (weight2 * priorityFunc2) + … + (weightn * priorityFuncn)

全部打分函式如下表格所示:

演算法名稱預設權重詳細說明
EqualPriority-給予所有節點相等的權重;
MostRequestedPriority-支援最多請求資源的節點。 該策略將 Pod 排程到整體工作負載所需的最少的一組節點上;
RequestedToCapacityRatioPriority-使用預設的打分方法模型,建立基於 ResourceAllocationPriority 的 requestedToCapacity;
SelectorSpreadPriority1屬於同一 Service、 StatefulSet 或 ReplicaSet 的 Pod,儘可能地跨 Node 部署(雞蛋不要只放在一個籃子裡,分散風險,提高可用性);
ServiceSpreadingPriority-對於給定的 Service,此策略旨在確保該 Service 關聯的 Pod 在不同的節點上執行。 它偏向把 Pod 排程到沒有該服務的節點。 整體來看,Service 對於單個節點故障變得更具彈性;
InterPodAffinityPriority1實現了 Pod 間親和性與反親和性的優先順序;
LeastRequestedPriority1偏向最少請求資源的節點。 換句話說,節點上的 Pod 越多,使用的資源就越多,此策略給出的排名就越低;
BalancedResourceAllocation1CPU和記憶體使用率越接近的節點權重越高,該策略不能單獨使用,必須和 LeastRequestedPriority 組合使用,儘量選擇在部署Pod後各項資源更均衡的機器。
NodePreferAvoidPodsPriority10000根據節點的註解 scheduler.alpha.kubernetes.io/preferAvoidPods 對節點進行優先順序排序。 你可以使用它來暗示兩個不同的 Pod 不應在同一節點上執行;
NodeAffinityPriority1根據節點親和中 PreferredDuringSchedulingIgnoredDuringExecution 欄位對節點進行優先順序排序;
TaintTolerationPriority1根據節點上無法忍受的汙點數量,給所有節點進行優先順序排序。 此策略會根據排序結果調整節點的等級;
ImageLocalityPriority1如果Node上存在Pod容器部分所需映象,則根據這些映象的大小來決定分值,映象越大,分值就越高;
EvenPodsSpreadPriority
2實現了 Pod 拓撲擴充套件約束的優先順序排序;

我自己遇到的是“多節點排程資源不均衡問題”,所以跟節點資源相關的打分演算法是我關注的重點。
1、BalancedResourceAllocation(預設開啟),它的計算公式如下所示:

score = 10 - variance(cpuFraction,memoryFraction,volumeFraction)*10

其中,每種資源的 Fraction 的定義是 :Pod 的 request 資源 / 節點上的可用資源。而 variance 演算法的作用,則是計算每兩種資源 Fraction 之間的“距離”。而最後選擇的,則是資源 Fraction 差距最小的節點。
所以說,BalancedResourceAllocation 選擇的,其實是排程完成後,所有節點裡各種資源分配最均衡的那個節點,從而避免一個節點上 CPU 被大量分配、而 Memory 大量剩餘的情況。
2、LeastRequestedPriority(預設開啟),它的計算公式如下所示:

score = (cpu((capacity-sum(requested))10/capacity) + memory((capacity-sum(requested))10/capacity))/2

可以看到,這個演算法實際上是根據 request 來計算出空閒資源(CPU 和 Memory)最多的宿主機。
3、MostRequestedPriority(預設不開啟),它的計算公式如下所示:

score = (cpu(10 sum(requested) / capacity) + memory(10 sum(requested) / capacity)) / 2

在 ClusterAutoscalerProvider 中替換 LeastRequestedPriority,給使用多資源的節點更高的優先順序。

你可以修改 /etc/kubernetes/manifests/kube-scheduler.yaml 配置,新增 v=10 引數來開啟排程打分日誌。

自定義配置

如果官方預設的過濾和打分策略,無法滿足實際業務,我們可以自定義配置:

  • 排程策略:允許你修改預設的過濾 斷言(Predicates) 和打分 優先順序(Priorities) 。
  • 排程配置:允許你實現不同排程階段的外掛, 包括:QueueSort, Filter, Score, Bind, Reserve, Permit 等等。 你也可以配置 kube-scheduler 執行不同的配置檔案。

解決k8s排程不均衡問題

一、按實際用量配置 Pod 的 requeste

從上面的排程策略可以得知,資源相關的打分演算法 LeastRequestedPriority 和 MostRequestedPriority 都是基於 request 來進行評分,而不是按 Node 當前資源水位進行排程(在沒有安裝 Prometheus 等資源監控相關元件之前,kube-scheduler 也無法實時統計 Node 當前的資源情況),所以可以動態採 Pod 過去一段時間的資源使用率,據此來設定 Pod 的Request,才能契合 kube-scheduler 預設打分演算法,讓 Pod 的排程更均衡。

二、為資源佔用較高的 Pod 設定反親和

對一些資源使用率較高的 Pod ,進行反親和,防止這些專案同時排程到同一個 Node,導致 Node 負載激增。

三、引入實時資源打分外掛 Trimaran

但在實際專案中,並不是所有情況都能較為準確的估算出 Pod 資源用量,所以依賴 request 配置來保障 Pod 排程的均衡性是不準確的。那有沒有一種通過 Node 當前實時資源進行打分排程的方案呢?Kubernetes 官方社群 SIG 小組提供的排程外掛 Trimaran 就具備這樣的能力。

Trimaran 官網地址:https://github.com/kubernetes-sigs/scheduler-plugins/tree/master/pkg/trimaran

Trimaran 是一個實時負載感知排程外掛,它利用 load-watcher 獲取程式資源利用率資料。目前,load-watcher支援三種度量工具:Metrics Server、Prometheus 和 SignalFx。

  • Kubernetes Metrics Server:是 kubernetes 監控體系中的核心元件之一,它負責從 kubelet 收集資源指標,然後對這些指標監控資料進行聚合(依賴kube-aggregator),並在 Kubernetes Apiserver 中通過 Metrics API( /apis/metrics.k8s.io/) 公開暴露它們;
  • Prometheus Server: 是一款基於時序資料庫的開源監控告警系統,非常適合 Kubernetes 叢集的監控。基本原理是通過 Http 協議週期性抓取被監控元件的狀態,任意元件只要提供對應的 Http 介面就可以接入監控。不需要任何 SDK 或者其他的整合過程。這樣做非常適合做虛擬化環境監控系統,比如 VM、Docker、Kubernetes 等。官網地址:https://prometheus.io/
  • SignalFx:是一家基礎設施及應用實時雲監控服務商,它採用了一個低延遲、可擴充套件的流式分析引擎,以監視微服務(鬆散耦合、獨立部署的應用元件集合)和協調的容器環境(如Kubernetes和Docker)。官網地址:https://www.signalfx.com/

Trimaran 的架構如下:
image.png
可以看到在 kube-scheduler 打分的過程中,Trimaran 會通過 load-watcher 獲取當前 node 的實時資源水位,然後據此打分從而干預排程結果。

Trimaran 打分原理:https://github.com/kubernetes-sigs/scheduler-plugins/tree/master/kep/61-Trimaran-real-load-aware-scheduling

四、引入重平衡工具 descheduler

從 kube-scheduler 的角度來看,排程程式會根據其當時對 Kubernetes 叢集的資源描述做出最佳排程決定,但排程是靜態的,Pod 一旦被繫結了節點是不會觸發重新排程的。雖然打分外掛可以有效的解決排程時的資源不均衡問題,但每個 Pod 在長期的執行中所佔用的資源也是會有變化的(通常記憶體會增加)。假如一個應用在啟動的時候只佔 2G 記憶體,但執行一段時間之後就會佔用 4G 記憶體,如果這樣的應用比較多的話,Kubernetes 叢集在執行一段時間後就可能會出現不均衡的狀態,所以需要重新平衡叢集。
除此之外,也還有一些其他的場景需要重平衡:

  • 叢集新增新節點,一些節點不足或過度使用;
  • 某些節點發生故障,其pod已移至其他節點;
  • 原始排程決策不再適用,因為在節點中新增或刪除了汙點或標籤,不再滿足 pod/node 親和性要求。

當然我們可以去手動做一些叢集的平衡,比如手動去刪掉某些 Pod,觸發重新排程就可以了,但是顯然這是一個繁瑣的過程,也不是解決問題的方式。為了解決實際執行中叢集資源無法充分利用或浪費的問題,可以使用 descheduler 元件對叢集的 Pod 進行排程優化,descheduler 可以根據一些規則和配置策略來幫助我們重新平衡叢集狀態,其核心原理是根據其策略配置找到可以被移除的 Pod 並驅逐它們,其本身並不會進行排程被驅逐的 Pod,而是依靠預設的排程器來實現,descheduler 重平衡原理可參見官網。

descheduler 官網地址:https://github.com/kubernetes-sigs/descheduler

參考資料

本文由mdnice多平臺釋出

相關文章