Kubernetes集羣調度器原理剖析及思考

導讀

雲環境或者計算倉庫級別(將整個數據中心當做單個計算池)的集羣管理系統通常會定義出工作負載的規範,並使用調度器將工作負載放置到集羣恰當的位置。好的調度器可以讓集羣的工作處理更高效,同時提高資源利用率,節省能源開銷。

通用調度器,如Kubernetes原生調度器Scheduler實現了根據特定的調度算法和策略將pod調度到指定的計算節點(Node)上。但實際上設計大規模共享集羣的調度器並不是一件容易的事情。調度器不僅要了解集羣資源的使用和分佈情況,還要兼顧任務分配速度和執行效率。過度設計的調度器屏蔽了太多的技術實現,以至於無法按照預期完成調度任務,或導致異常情況的發生,不恰當的調度器的選擇同樣會降低工作效率,或導致調度任務無法完成。

本文主要從設計原理、代碼實現兩個層面介紹Kubernetes的調度器以及社區對其的補充加強,同時對業界常用調度器的設計實現進行比較分析。通過本文,讀者可瞭解調度器的來龍去脈,從而爲選擇甚至設計實現適合實際場景的調度器打下基礎。

註明:本文中代碼基於v1.11版本Kubernetes進行分析,如有不當之處,歡迎指正!

調度器的基本知識

1.1 調度器的定義

通用調度的定義是指基於某種方法將某項任務分配到特定資源以完成相關工作,其中任務可以是虛擬計算元素,如線程、進程或數據流,特定資源一般是指處理器、網絡、磁盤等,調度器則是完成這些調度行爲的具體實現。使用調度器的目的是實現用戶共享系統資源的同時,降低等待時間,提高吞吐率以及資源利用率。

本文中我們討論的調度器是指大規模集羣下調度任務的實現,比較典型的有Mesos/Yarn(Apache)、Borg/Omega(Google)、Quincy(Microsoft)等。構建大規模集羣(如數據中心規模)的成本非常之高,因此精心設計調度器就顯得尤爲重要。

常見類型的調度器的對比分析如下表1所示:

Kubernetes集羣調度器原理剖析及思考

1.2 調度器的考量標準

我們首先思考一下調度器是根據哪些信息來進行調度工作的,以及哪些指標可以用來衡量調度工作質量。

調度器的主要工作是將資源需求與資源提供方做全局最優的匹配。所以一方面調度器的設計需要了解不同類型的資源拓撲,另一方面還需要對工作負載有充分的認識。

瞭解不同類型的資源拓撲,充分掌握環境拓撲信息能夠使調度工作更充分的利用資源(如經常訪問數據的任務如果距數據近可以顯著減少執行時間),並且可以基於資源拓撲信息定義更加複雜的策略。但全局資源信息的維護消耗會限制集羣的整體規模和調度執行時間,這也讓調度器難以擴展,從而限制集羣規模。

另一方面,由於不同類型的工作負載會有不同的甚至截然相反的特性,調度器還需要對工作負載有充分的認識,例如服務類任務,資源需求少,運行時間長,對調度時間並不敏感;而批處理類任務,資源需求大,運行時間短,任務可能相關,對調度時間要求較高。 同時,調度器也要滿足使用方的特殊要求。如任務儘量集中或者分散,保證多個任務同時進行等。

總的來說,好的調度器需要平衡好單次調度(調度時間,質量),同時要考慮到環境變化對調度結果的影響,保持結果最優(必要時重新調度),保證集羣規模,同時還要能夠支持用戶無感知的升級和擴展。調度的結果需要滿足但不限於下列條件,並最大可能滿足儘可能優先級較高的條件:

資源使用率最大化

滿足用戶指定的調度需求

滿足自定義優先級要求

調度效率高,能夠根據資源情況快速做出決策

能夠根據負載的變化調整調度策略

充分考慮各種層級的公平性

1.3 鎖對調度器設計的影響

對於資源的調度,一定會涉及到鎖的應用,不同類型鎖的選擇將直接決定調度器的使用場景。類似Mesos等兩層調度器,一般採用悲觀鎖的設計實現方式,當資源全部滿足任務需要時啓動任務,否則將增量繼續申請更多的資源直到調度條件滿足;而共享狀態的調度器,會考慮使用樂觀鎖的實現方式,Kubernetes默認調度器是基於樂觀鎖進行設計的。

我們首先通過一個簡單的例子,比較下悲觀鎖和樂觀鎖處理邏輯的不同,假設有如下的一個場景:

作業A讀取對象O

作業B讀取對象O

作業A在內存中更新對象O

作業B在內存中更新對象O

作業A寫入對象O實現持久化

作業B寫入對象O實現持久化

悲觀鎖的設計是對對象O實現獨佔鎖,直到作業A完成對對象O的更新並寫入持久化數據之前,阻斷其他讀取請求。樂觀鎖的設計是對對象O實現共享鎖,假設所有的工作都能夠正常完成,直到有衝突產生,記錄衝突的發生並拒絕衝突的請求。

樂觀鎖一般會結合資源版本實現,同樣是上述中的例子,當前對象O的版本爲v1,作業A首先完成對對象O的寫入持久化操作,並標記對象O的版本爲v2,作業B在更新時發現對象版本已經變化,則會取消更改。

Kubernetes調度器剖析

Kubernetes中的計算任務大多通過pod來承載運行。pod是用戶定義的一個或多個共享存儲、網絡和命名空間資源的容器的組合,是調度器可調度的最小單元。Kubernetes的調度器是控制平面的一部分,它主要監聽APIServer提供的pod任務列表,獲取待調度pod,根據預選和優選策略,爲這些pod分配運行的節點。概括來說,調度器主要依據資源消耗的描述得到一個調度結果。

2.1 Kubernetes調度器的設計

Kubernetes的調度設計參考了Omega的實現,主要採用兩層調度架構,基於全局狀態進行調度,通過樂觀鎖控制資源歸屬,同時支持多調度器的設計。

兩層架構幫助調度器屏蔽了很多底層實現細節,將策略和限制分別實現,同時過濾可用資源,讓調度器能夠更靈活適應資源變化,滿足用戶個性化的調度需求。相比單體架構而言,不僅更容易添加自定義規則、支持集羣動態伸縮,同時對大規模集羣有更好的支持(支持多調度器)。

相比於使用悲觀鎖和部分環境視圖的架構(如Mesos),基於全局狀態和樂觀鎖實現的好處是調度器可以看到集羣所有可以支配的資源,然後搶佔低優先級任務的資源,以達到策略要求的狀態。它的資源分配更符合策略要求,避免了作業囤積資源導致集羣死鎖的問題。當然這會有搶佔任務的開銷以及衝突導致的重試,但總體來看資源的使用率更高了。

Kubernetes中默認只有一個調度器,而Omega的設計本身支持資源分配管理器共享資源環境信息給多個調度器。所以從設計上來說,Kubernetes可以支持多個調度器。

2.2 Kubernetes調度器的實現

Kubernetes調度器的工作流程如下圖所示。調度器的工作本質是通過監聽pod的創建、更新、刪除等事件,循環遍歷地完成每個pod的調度流程。如調度過程順利,則基於預選和優選策略,完成pod和主機節點的綁定,最終通知kubelet完成pod啓動的過程。如遇到錯誤的調度過程,通過優先級搶佔的方式,獲取優先調度的能力,進而重新進入調度循環的過程,等待成功調度。

2.2.1 調度循環的完整邏輯

Kubernetes調度器完成調度的整體流程如下圖1所示。下面就每個步驟的實現邏輯進行說明。

Kubernetes集羣調度器原理剖析及思考
(1)基於事件驅動啓動循環過程

Kubernetes調度器維護sharedIndexInformer,來完成informer對象的初始化工作。也就是調度器會監聽pod創建、更新、刪除的操作事件,主動更新事件緩存,並持久化到內存隊列,發起調度循環。

該過程的函數入口在

https://github.com/kubernetes/kubernetes/blob/9cbccd38598e5e2750d39e183aef21a749275087/pkg/scheduler/factory/factory.go#L631

Kubernetes集羣調度器原理剖析及思考
(2)將沒有調度的pod加到調度器緩存並更新調度器隊列

Informer對象負責監聽pod的事件,主要的事件類型有:針對已調度pod的addPodToCache、updatePodInCache、deletePodFromCache和針對未被調度pod的addPodToSchedulingQueue、updatePodInSchedulingQueue、deletePodFromSchedulingQueue六種事件。該過程的函數入口在:

https://github.com/kubernetes/kubernetes/blob/9cbccd38598e5e2750d39e183aef21a749275087/pkg/scheduler/eventhandlers.go

各類事件的含義如下表2所示:

Kubernetes集羣調度器原理剖析及思考

(3)對調度器隊列中的每個pod執行調度

這裏需要指出的是,在單個pod調度的過程中,對於主機節點的調度算法是順序執行的。也就是說,pod在調度的過程中會嚴格的順序執行Kubernetes內置的策略和優先級,然後選擇最合適的節點。

單個pod的調度過程分爲預選和優選兩個階段。預選階段調度器根據一組規則過濾掉不符合要求的主機,選擇出合適的節點;優選階段通過節點優先級打分的方式(依據整體優化策略等),選擇出分值最高的節點進行調度。

單個pod的調度過程由以下函數作爲入口:https://github.com/kubernetes/kubernetes/blob/9cbccd38598e5e2750d39e183aef21a749275087/pkg/scheduler/scheduler.go#L457
Kubernetes集羣調度器原理剖析及思考

當然,調度的過程可能由於沒有滿足pod運行條件的節點而調度失敗,此時當pod有優先級指定的時候,將觸發競爭機制。具有高優先級的pod將嘗試搶佔低優先級的pod資源。相關部分代碼實現如下:

Kubernetes集羣調度器原理剖析及思考
如果資源搶佔成功,將在下一次調度循環時標記可調度過程。如果搶佔失敗,調度程序退出。調度結果不保存意味着pod仍然會出現在未分配列表中。

(4)接下來檢查用戶提供的插件的條件是否滿足

Reserve插件是Kubernets留給用戶進行擴展的接口,基於reserver插件用戶在這個階段可以設定自定義條件,從而滿足期望的調度過程。插件的入口函數在:https://github.com/kubernetes/kubernetes/blob/9cbccd38598e5e2750d39e183aef21a749275087/pkg/scheduler/plugins/registrar.go

可以在https://github.com/kubernetes/kubernetes/tree/9cbccd38598e5e2750d39e183aef21a749275087/pkg/scheduler/plugins/examples查看插件擴展reserver接口進行自定義調度的示例。

(5)找到滿足的節點後,更新Pod對象的標籤,保存被調度節點的結果

該過程的函數入口在https://github.com/kubernetes/kubernetes/blob/9cbccd38598e5e2750d39e183aef21a749275087/pkg/scheduler/scheduler.go#L517。
Kubernetes集羣調度器原理剖析及思考

(6)完成pod到節點的綁定

pod到節點的綁定需要首先完成存儲卷的掛載,最後通過pod對象的更新,完成最後的綁定。具體代碼的邏輯可以參考:https://github.com/kubernetes/kubernetes/blob/9cbccd38598e5e2750d39e183aef21a749275087/pkg/scheduler/scheduler.go#L524

(7)調度完成後,主協程返回,執行下一個調度

至此調度的完整流程就完成了,下面重點介紹下,在單個pod調度過程中Kubernetes主要是如何對節點進行選擇的,主要包括預選和優選兩種策略。

2.2.2 單個pod的調度流程

單個pod的調度過程如下圖2所示。主要包括由pre-filter、filter、post-filter的預選過程和scoring的優選過程。

Kubernetes集羣調度器原理剖析及思考
圖2:單個Pod的調度過程

(1)pod進入調度階段,首先進入預選環節

通過規則過濾找到滿足pod調度條件的節點。
Kubernetes集羣調度器原理剖析及思考

k8s內置了許多過濾規則,調度器會按照事先定義好的順序進行過濾。內置的過濾規則主要包括檢查節點是否有足夠資源(例如CPU、內存與GPU等)滿足pod的運行需求,檢查pod容器所需的HostPort是否已被節點上其它容器或服務佔用,檢查節點標籤(label)是否匹配pod的nodeSelector屬性要求,根據 taints 和 toleration 的關係判斷pod是否可以調度到節點上pod是否滿足節點容忍的一些條件,還有檢查是否滿足csi最大可掛載卷限制等。
Kubernetes集羣調度器原理剖析及思考

(2)經過預選策略對節點過濾後,進入優選階段

調度器根據預置的默認規則進行打分(優先級函數得分*權重的和),然後選擇分數最高的節點實現pod到節點的綁定。

Kubernetes集羣調度器原理剖析及思考
Kubernetes內置的優先級函數如下,主要包括平均分佈優先級(SelectorSpreadPriority)、最少訪問優先級(LeastRequestedPriority)、平衡資源分佈優先級(BalancedResourceAllocation)等。

Kubernetes集羣調度器原理剖析及思考
SelectorSpreadPriority:爲了更好的高可用,對同屬於一個service、replication controller或者replica的多個Pod副本,儘量調度到多個不同的節點上。

InterPodAffinityPriority:通過迭代 weightedPodAffinityTerm的元素計算和,如果對該節點滿足相應的PodAffinityTerm,則將 “weight” 加到和中,具有最高和的節點是最優選的。

LeastRequestedPriority:由節點空閒資源與節點總容量的比值,即由(總容量-節點上Pod的容量總和-新Pod的容量)/總容量)來決定節點的優先級。CPU和memory具有相同權重,比值越大的節點得分越高。

BalancedResourceAllocation:CPU和內存使用率越接近的節點優先級越高,該策略不能單獨使用,必須和LeastRequestedPriority同時使用,也就是說盡量選擇在部署Pod後各項資源更均衡的機器。

NodePreferAvoidPodsPriority(權重1w): 如果節點的 Anotation 沒有設置 key-value:scheduler. alpha.kubernetes.io/ preferAvoidPods = “…”,則該 節點對該 policy 的得分就是10分,加上權重10000,那麼該節點對該policy的得分至少10W分。如果節點的Anotation設置了scheduler.alpha.kubernetes.io/preferAvoidPods = “…” ,如果該 pod 對應的 Controller 是 ReplicationController 或 ReplicaSet,則該節點對該 policy 的得分就是0分。

NodeAffinityPriority:實現Kubernetes調度中的親和性機制。

TaintTolerationPriority : 使用 Pod 中 tolerationList 與 節點 Taint 進行匹配,配對成功的項越多,則得分越低。

Kubernetes調度器的不足和解決思路

3.1典型的幾個問題和解決思路

(1)調度器只根據當前資源環境情況進行一次調度,一旦完成調度就沒有機制實現調整

雖然pod只有在自己退出、用戶刪除以及集羣資源不足等情況下纔會有變化。但資源拓撲的變化是隨時都有可能發生的,如批處理任務會結束,節點會新增或崩潰。這些情況導致調度的結果可能在調度時是最優的,但在拓撲變化後調度質量由於以上情況的發生而下降。

經過社區討論,認爲需要重新找出不滿足調度策略的pod,刪除並創建替代者來重新調度,據此設計啓動了項目descheduler。

(2)調度以單個pod進行的,因而調度互相關聯的工作負載會難以實現

如大數據分析、機器學習等計算多依賴於批處理任務,這類工作負載相關性大,互相之間有依賴關係。爲了解決這個問題,社區經過討論,提出了coscheduling 一次調度一組pod的項目,以此來優化這類調度任務的執行。

(3)目前調度器的實現只關心是否能將pod與節點綁定,資源使用情況的數據未被充分利用

目前,集羣的使用量只能通過監控數據間接推導。如果k8s集羣剩餘資源不足時,並沒有直觀數據可以用來觸發擴容或者告警。

根據上述情況,社區啓動了cluster-capacity framework項目 ,提供集羣的容量數據,方便集羣的維護程序或者管理員基於這些數據做集羣擴容等。也有項目抓取監控數據自己計算集羣的整體負載情況給調度算法參考,如poseidon。

3.2 Kubernetes調度器的定製擴展

如上節所述,通用調度器在某些場景下並不能滿足用戶個性化需求,實際環境下運行的集羣的調度器,往往需要根據實際的需求做定製與二次開發。

kubernetes的調度器以插件化的形式實現的, 方便用戶對調度的定製與二次開發。定製調度器有如下幾種方式的選擇:

更改Kubernetes內置策略,通過更改默認的策略文件或者重新編譯調度器來實現。

擴展調度器在pre-filter、filter、post-filter、reserve、prebind、bind和post-bind各個階段的接口,更改調度器過濾、打分、搶佔、預留的具體實現邏輯。

更改調度器調度算法,從頭實現調度器邏輯。
Kubernetes集羣調度器原理剖析及思考

企業場景應用的案例

4.1 通用計算場景

Kubernetes default-scheduler滿足通用計算的需求,主要服務於以快速開發測試爲目標的持續集成和持續部署平臺(DevOps平臺)、以標準三層架構應用爲特點的容器應用運行與運維平臺(容器平臺)、PaaS平臺和雲原生應用的核心基礎架構平臺(aPaaS平臺)幾種場景。

通常情況下,標準Kubernetes調度器能夠滿足大多數通過計算場景的訴求,主要解決應用上雲過程中不同異構雲資源之間的調度問題,應用上雲後彈性伸縮、故障自愈等的動態調度響應,標準中間件服務和數據庫服務基於日常運維規範的調度問題以及雲原生應用在服務治理、配置管理、狀態反饋、事件鏈路跟蹤上的綜合調度過程。

4.2 批處理場景

大數據分析和機器學習類任務執行時需要大量資源,多個任務同時進行時,資源很快會用盡,部分任務會需要等待資源釋放。這類型任務的步驟往往互相關聯,單獨運行步驟可能會影響最終結果。使用默認的調度器在集羣資源緊張時,甚至會出現佔用資源的pod都在等待依賴的pod運行完畢,而集羣沒有空閒資源去運行依賴任務,導致死鎖。所以在調度這類任務時,支持羣組調度(在調度作業所需的資源都收集完成後才進行調度),減少了pod數量,因而降低調度器的負載,同時避免了很多資源緊張帶來的問題。

與默認調度器一次調度一個pod不同,kube-batch定義了PodGroup 定義一組相關的pod資源,並實現了一個全新的調度器。調度器的流程基本與默認調度器相同。Podgroup保證一組pod可以同時被調度。是Kubernetes社區在大數據分析場景中的一種實現。

4.3 特定領域業務場景

特定的業務場景需要調度器能夠快速生成調度的策略,並儘可能避免調度超時。Poseidon是大規模集羣中基於圖應用數據局部性減少任務執行時間同時混合多種調度算法提升調度速度的一種調度器。

Poseidon是基於Firmament算法的調度器,它通過接收heapster數據來構建資源使用信息。調用Firmament實現進行調度。Firmament算法受Quincy[11]啓發,構建一個從任務到節點的圖,但作者爲減少調度時間,將兩種計算最短路徑的算法合併,將全量環境信息同步改爲增量同步。讓Firmament處理短時間批量任務時快於Quincy,在資源短缺時沒有Kubernetes默認調度器超時的問題。

總結

本文主要從設計原理、代碼實現等層面介紹Kubernetes的調度器以及社區對其的補充加強,總結了Kubernetes調度器的設計原理以及在何種場景如何增強Kubernetes來滿足業務需求,提供技術選型的依據和評價標準。

相關服務請訪問: https://support.huaweicloud.com/cce/index.html?utm_content=cce_helpcenter_2019

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章