解決k8s調度不均衡問題

語言: CN / TW / HK

前言

在近期的工作中,我們發現 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 | 檢查節點是否可調度; | | GeneralPred | 是 | 2 | 是一組聯合檢查,包含了:HostNamePred、PodFitsResourcesPred、PodFitsHostPortsPred、MatchNodeSelectorPred 4個檢查 | | HostNamePred | 否 | 3 | 檢查 Pod 指定的 Node 名稱是否和 Node 名稱相同; | | PodFitsHostPortsPred | 否 | 4 | 檢查 Pod 請求的端口(網絡協議類型)在節點上是否可用; | | MatchNodeSelectorPred | 否 | 5 | 檢查是否匹配 NodeSelector 節點選擇器的設置; | | PodFitsResourcesPred | 否 | 6 | 檢查節點的空閒資源(例如,CPU 和內存)是否滿足 Pod 的要求; | | NoDiskConflictPred | 是 | 7 | 根據 Pod 請求的卷是否在節點上已經掛載,評估 Pod 和節點是否匹配; | | PodToleratesNodeTaintsPred | 強制 | 8 | 檢查 Pod 的容忍是否能容忍節點的污點; | | CheckNodeLabelPresencePred | 否 | 9 | 檢測 NodeLabel 是否存在; | | CheckServiceAffinityPred | 否 | 10 | 檢測服務的親和; | | MaxEBSVolumeCountPred | 是 | 11 | 已廢棄,檢測 Volume 數量是否超過雲服務商 AWS 的存儲服務的配置限制; | | MaxGCEPDVolumeCountPred | 是 | 12 | 已廢棄,檢測 Volume 數量是否超過雲服務商 Google Cloud 的存儲服務的配置限制; | | MaxCSIVolumeCountPred | 是 | 13 | Pod 附加 CSI 卷的數量,判斷是否超過配置的限制; | | MaxAzureDiskVolumeCountPred | 是 | 14 | 已廢棄,檢測 Volume 數量是否超過雲服務商 Azure 的存儲服務的配置限制; | | MaxCinderVolumeCountPred | 否 | 15 | 已廢棄,檢測 Volume 數量是否超過雲服務商 OpenStack 的存儲服務的配置限制; | | CheckVolumeBindingPred | 是 | 16 | 基於 Pod 的卷請求,評估 Pod 是否適合節點,這裏的捲包括綁定的和未綁定的 PVC 都適用; | | NoVolumeZoneConflictPred | 是 | 17 | 給定該存儲的故障區域限制, 評估 Pod 請求的卷在節點上是否可用; | | EvenPodsSpreadPred | 是 | 18 | 檢測 Node 是否滿足拓撲傳播限制; | | MatchInterPodAffinityPred | 是 | 19 | 檢測是否匹配 Pod 的親和與反親和的設置; |

可以看出,Kubernetes 正在逐步移除某個具體雲服務商的服務的相關代碼,而使用接口(Interface)來擴展功能。

打分(Priority)

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

| 算法名稱 | 默認 | 權重 | 詳細説明 | | --- | --- | --- | --- | | EqualPriority | 否 | - | 給予所有節點相等的權重; | | MostRequestedPriority | 否 | - | 支持最多請求資源的節點。 該策略將 Pod 調度到整體工作負載所需的最少的一組節點上; | | RequestedToCapacityRatioPriority | 否 | - | 使用默認的打分方法模型,創建基於 ResourceAllocationPriority 的 requestedToCapacity; | | SelectorSpreadPriority | 是 | 1 | 屬於同一 Service、 StatefulSet 或 ReplicaSet 的 Pod,儘可能地跨 Node 部署(雞蛋不要只放在一個籃子裏,分散風險,提高可用性); | | ServiceSpreadingPriority | 否 | - | 對於給定的 Service,此策略旨在確保該 Service 關聯的 Pod 在不同的節點上運行。 它偏向把 Pod 調度到沒有該服務的節點。 整體來看,Service 對於單個節點故障變得更具彈性; | | InterPodAffinityPriority | 是 | 1 | 實現了 Pod 間親和性與反親和性的優先級; | | LeastRequestedPriority | 是 | 1 | 偏向最少請求資源的節點。 換句話説,節點上的 Pod 越多,使用的資源就越多,此策略給出的排名就越低; | | BalancedResourceAllocation | 是 | 1 | CPU和內存使用率越接近的節點權重越高,該策略不能單獨使用,必須和 LeastRequestedPriority 組合使用,儘量選擇在部署Pod後各項資源更均衡的機器。 | | NodePreferAvoidPodsPriority | 是 | 10000 | 根據節點的註解 scheduler.alpha.kubernetes.io/preferAvoidPods 對節點進行優先級排序。 你可以使用它來暗示兩個不同的 Pod 不應在同一節點上運行; | | NodeAffinityPriority | 是 | 1 | 根據節點親和中 PreferredDuringSchedulingIgnoredDuringExecution 字段對節點進行優先級排序; | | TaintTolerationPriority | 是 | 1 | 根據節點上無法忍受的污點數量,給所有節點進行優先級排序。 此策略會根據排序結果調整節點的等級; | | ImageLocalityPriority | 是 | 1 | 如果Node上存在Pod容器部分所需鏡像,則根據這些鏡像的大小來決定分值,鏡像越大,分值就越高; | | EvenPodsSpreadPriority | 是 | 2 | 實現了 Pod 拓撲擴展約束的優先級排序; |

我自己遇到的是“多節點調度資源不均衡問題”,所以跟節點資源相關的打分算法是我關注的重點。 1、BalancedResourceAllocation(默認開啟),它的計算公式如下所示: go score = 10 - variance(cpuFraction,memoryFraction,volumeFraction)*10 其中,每種資源的 Fraction 的定義是 :Pod 的 request 資源 / 節點上的可用資源。而 variance 算法的作用,則是計算每兩種資源 Fraction 之間的“距離”。而最後選擇的,則是資源 Fraction 差距最小的節點。 所以説,BalancedResourceAllocation 選擇的,其實是調度完成後,所有節點裏各種資源分配最均衡的那個節點,從而避免一個節點上 CPU 被大量分配、而 Memory 大量剩餘的情況。 2、LeastRequestedPriority(默認開啟),它的計算公式如下所示: go score = (cpu((capacity-sum(requested))10/capacity) + memory((capacity-sum(requested))10/capacity))/2 可以看到,這個算法實際上是根據 request 來計算出空閒資源(CPU 和 Memory)最多的宿主機。 3、MostRequestedPriority(默認不開啟),它的計算公式如下所示: go 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多平台發佈