進擊的 Kubernetes 調度系統(一):Kubernetes scheduling framework


頭圖.png

作者 | 王慶璨(阿里雲技術專家)、張凱(阿里雲高級技術專家)

導讀:阿里雲容器服務團隊結合多年 Kubernetes 產品與客戶支持經驗,對 Kube-scheduler 進行了大量優化和擴展,逐步使其在不同場景下依然能穩定、高效地調度各種類型的復雜工作負載。《進擊的 Kubernetes 調度系統》系列文章將把我們的經驗、技術思考和實現細節全面地展現給 Kubernetes 用戶和開發者,期望幫助大家更好地了解 Kubernetes 調度系統的強大能力和未來發展方向。

前言

Kubernetes 已經成為目前事實標准上的容器集群管理平台。它為容器化應用提供了自動化部署、運維、資源調度等全生命周期管理功能。經過 3 年多的快速發展,Kubernetes 在穩定性、擴展性和規模化方面都有了長足進步。尤其是 Kubernetes 控制平面的核心組件日臻成熟。而作為決定容器能否在集群中運行的調度器 Kube-scheduler,更是由於長久以來表現穩定,且已能滿足大部分 Pod 調度場景,逐漸不被開發人員特別關注。

伴隨着 Kubernetes 在公有雲以及企業內部 IT 系統中廣泛應用,越來越多的開發人員嘗試使用 Kubernetes 運行和管理 Web 應用和微服務以外的工作負載。典型場景包括機器學習和深度學習訓練任務,高性能計算作業,基因計算工作流,甚至是傳統的大數據處理任務。此外,Kubernetes 集群所管理的資源類型也愈加豐富,不僅有 GPU,TPU 和 FPGA,RDMA 高性能網絡,還有針對領域任務的各種定制加速器,比如各種 AI 芯片,NPU,視頻編解碼器等。開發人員希望在 Kubernetes 集群中能像使用 CPU 內存那樣簡單地聲明和使用各種異構設備。

總的來說,圍繞 Kubernetes 構建一個容器服務平台,統一管理各種新算力資源,彈性運行多種類型應用,最終把服務按需交付到各種運行環境(包括公共雲、數據中心、邊緣節點,甚至是終端設備),已然成為雲原生技術的發展趨勢。

早期方案

首先,讓我們來了解一下 Kubernetes 社區都有過哪些提升調度器擴展能力的方案。

要統一管理和調度異構資源與更多復雜工作負載類型,首先面對挑戰的就是 Kube-scheduler。在 Kubernetes 社區里關於提升調度器擴展能力的討論一直不斷。sig-scheduling 給出的判斷是,越多功能加入,使得調度器代碼量龐大,邏輯復雜,導致維護的難度越來越大,很多 bug 難以發現、處理。而對於使用了自定義調度的用戶來說,跟上每一次調度器功能更新,都充滿挑戰。

在阿里雲,我們的用戶遇到了同樣的挑戰。Kubernetes 原生調度器循環處理單個 Pod 容器的固定調度邏輯,無法及時的支持不同用戶在不同場景的需求。所以針對特定的場景,我們會基於原生的 Kube-scheduler 擴展自己場景的調度策略。

最初對於 Kube-scheduler 進行擴展的方式主要有兩種,一種是調度器擴展(Scheduler Extender), 另外一種是多調度器(Multiple schedulers)。接下來我們對這兩種方式分別進行介紹和對比。

1)Scheduler Extender

社區最初提供的方案是通過 Extender 的形式來擴展 scheduler。Extender 是外部服務,支持 Filter、Preempt、Prioritize 和 Bind 的擴展,scheduler 運行到相應階段時,通過調用 Extender 注冊的 webhook 來運行擴展的邏輯,影響調度流程中各階段的決策結果。

以 Filter 階段舉例,執行過程會經過 2 個階段:

  1. scheduler 會先執行內置的Filter策略,如果執行失敗的話,會直接標識 Pod 調度失敗;
  2. 如何內置的 Filter 策略執行成功的話,scheduler 通過 Http 調用 Extender 注冊的 webhook, 將調度所需要的 Pod 和 Node 的信息發送到 Extender,根據返回 filter 結果,作為最終結果。

1.jpg

我們可以發現 Extender 存在以下問題:

  1. 調用 Extender 的接口是 HTTP 請求,受到網絡環境的影響,性能遠低於本地的函數調用。同時每次調用都需要將 Pod 和 Node 的信息進行 marshaling 和 unmarshalling 的操作,會進一步降低性能;
  2. 用戶可以擴展的點比較有限,位置比較固定,無法支持靈活的擴展,例如只能在執行完默認的 Filter 策略后才能調用。

基於以上介紹,Extender 的方式在集群規模較小,調度效率要求不高的情況下,是一個靈活可用的擴展方案,但是在正常生產環境的大型集群中,Extender 無法支持高吞吐量,性能較差。

2)Multiple schedulers

Scheduler 在 Kubernetes 集群中其實類似於一個特殊的 Controller,通過監聽 Pod 和 Node 的信息,給 Pod 挑選最佳的節點,更新 Pod 的 spec.NodeName 的信息來將調度結果同步到節點。所以對於部分有特殊的調度需求的用戶,有些開發者通過自研 Custom Scheduler 來完成以上的流程,然后通過和 default scheduler 同時部署的方式,來支持自己特殊的調度需求。

2.jpg

Custom Scheduler 會存在一下問題:

  1. 如果與 default scheduler 同時部署,因為每個調度器所看到的資源視圖都是全局的,所以在調度決策中可能會在同一時刻在同一個節點資源上調度不同的 Pod,導致節點資源沖突的問題;
  2. 有些用戶將調度器所能調度的資源通過 Label 划分不同的池子,可以避免資源沖突的現象出現。但是這樣又會導致整體集群資源利用率的下降;
  3. 有些用戶選擇通過完全自研的方式來替換 default scheduler,這種會帶來比較高的研發成本,以及 Kubernetes 版本升級后可能存在的兼容性問題。

Scheduler Extender 的性能較差可是維護成本較小,Custom Scheduler 的研發和維護的成本特別高但是性能較好,這種情況是開發者面臨這種兩難處境。這時候 Kubernetes Scheduling Framework V2 橫空出世,給我們帶來魚和熊掌可以兼得的方案。

3.png

新一代調度框架 Scheduling Framework 之解析

社區也逐漸的發現開發者所面臨的困境,為了解決如上問題,使 Kube-scheduler 擴展性更好、代碼更簡潔,社區從 Kubernetes 1.16 版本開始, 構建了一種新的調度框架 Kubernetes Scheduling Framework 的機制。

Scheduling Framework 在原有的調度流程中, 定義了豐富擴展點接口,開發者可以通過實現擴展點所定義的接口來實現插件,將插件注冊到擴展點。Scheduling Framework 在執行調度流程時,運行到相應的擴展點時,會調用用戶注冊的插件,影響調度決策的結果。通過這種方式來將用戶的調度邏輯集成到 Scheduling Framework 中。

4.jpg

Framework 的調度流程是分為兩個階段 scheduling cycle 和 binding cycle。

  • scheduling cycle 是同步執行的,同一個時間只有一個 scheduling cycle,是線程安全的;
  • binding cycle 是異步執行的,同一個時間中可能會有多個 binding cycle在運行,是線程不安全的。

1. scheduling cycle

scheduling cycle 是調度的核心流程,主要的工作是進行調度決策,挑選出唯一的節點。

1)Queue sort

// QueueSortPlugin is an interface that must be implemented by "QueueSort" plugins.
// These plugins are used to sort pods in the scheduling queue. Only one queue sort
// plugin may be enabled at a time.
type QueueSortPlugin interface {
    Plugin
    // Less are used to sort pods in the scheduling queue.
    Less(*PodInfo, *PodInfo) bool
}

Scheduler 中的優先級隊列是通過 heap 實現的,我們可以在 QueueSortPlugin 中定義 heap 的比較函數來決定的排序結構。但是需要注意的是 heap 的比較函數在同一時刻只有一個,所以 QueueSort 插件只能 Enable 一個,如果用戶 Enable 了 2 個則調度器啟動時會報錯退出。下面是默認的比較函數,可供參考。

// Less is the function used by the activeQ heap algorithm to sort pods.
// It sorts pods based on their priority. When priorities are equal, it uses
// PodQueueInfo.timestamp.
func (pl *PrioritySort) Less(pInfo1, pInfo2 *framework.QueuedPodInfo) bool {
    p1 := pod.GetPodPriority(pInfo1.Pod)
    p2 := pod.GetPodPriority(pInfo2.Pod)
    return (p1 > p2) || (p1 == p2 && pInfo1.Timestamp.Before(pInfo2.Timestamp))
}

2)PreFilter

PreFilter 在 scheduling cycle 開始時就被調用,只有當所有的 PreFilter 插件都返回 success 時,才能進入下一個階段,否則 Pod 將會被拒絕掉,標識此次調度流程失敗。PreFilter 類似於調度流程啟動之前的預處理,可以對 Pod 的信息進行加工。同時 PreFilter 也可以進行一些預置條件的檢查,去檢查一些集群維度的條件,判斷否滿足 pod 的要求。

3)Filter

Filter 插件是 scheduler v1 版本中的 Predicate 的邏輯,用來過濾掉不滿足 Pod 調度要求的節點。為了提升效率,Filter 的執行順序可以被配置,這樣用戶就可以將可以過濾掉大量節點的 Filter 策略放到前邊執行,從而減少后邊 Filter 策略執行的次數,例如我們可以把 NodeSelector 的 Filter 放到第一個,從而過濾掉大量的節點。Node 節點執行 Filter 策略是並發執行的,所以在同一調度周期中多次調用過濾器。

4)PostFilter

新的 PostFilter 的接口定義在 1.19 的版本會發布,主要是用於處理當 Pod 在 Filter 階段失敗后的操作,例如搶占,Autoscale 觸發等行為。

5)PreScore

PreScore 在之前版本稱為 PostFilter,現在修改為 PreScore,主要用於在 Score 之前進行一些信息生成。此處會獲取到通過 Filter 階段的節點列表,我們也可以在此處進行一些信息預處理或者生成一些日志或者監控信息。

6)Scoring

Scoring 擴展點是 scheduler v1 版本中 Priority 的邏輯,目的是為了基於 Filter 過濾后的剩余節點,根據 Scoring 擴展點定義的策略挑選出最優的節點。Scoring 擴展點分為兩個階段:

  • 打分:打分階段會對 Filter 后的節點進行打分,scheduler 會調用所配置的打分策略
  • 歸一化: 對打分之后的結構在 0-100 之間進行歸一化處理

7)Reserve

Reserve 擴展點是 scheduler v1 版本的 assume 的操作,此處會對調度結果進行緩存,如果在后邊的階段發生了錯誤或者失敗的情況,會直接進入 Unreserve 階段,進行數據回滾。

8)Permit

Permit 擴展點是 framework v2 版本引入的新功能,當 Pod 在 Reserve 階段完成資源預留之后,Bind 操作之前,開發者可以定義自己的策略在 Permit 節點進行攔截,根據條件對經過此階段的 Pod 進行 allow、reject 和 wait 的 3 種操作。allow 表示 pod 允許通過 Permit 階段。reject 表示 pod 被 Permit 階段拒絕,則 Pod 調度失敗。wait 表示將 Pod 處於等待狀態,開發者可以設置超時時間。

2. binding cycle

binding cycle 需要調用 apiserver 的接口,耗時較長,為了提高調度的效率,需要異步執行,所以此階段線程不安全。

1)Bind

Bind 擴展點是 scheduler v1 版本中的 Bind 操作,會調用 apiserver 提供的接口,將 pod 綁定到對應的節點上。

2)PreBind 和 PostBind

開發者可以在 PreBind 和 PostBind 分別在 Bind 操作前后執行,這兩個階段可以進行一些數據信息的獲取和更新。

3)UnReserve

UnReserve 擴展點的功能是用於清理到 Reserve 階段的的緩存,回滾到初始的狀態。當前版本 UnReserve 與 Reserve 是分開定義的,未來會將 UnReserve 與 Reserve 統一到一起,即要求開發者在實現 Reserve 同時需要定義 UnReserve,保證數據能夠有效的清理,避免留下臟數據。

實現自己的調度插件

scheduler-plugins

Kubernetes 負責 Kube-scheduler 的小組 sig-scheduling 為了更好的管理調度相關的 Plugin,新建了項目 scheduler-plugins 來方便用戶管理不同的插件,用戶可以直接基於這個項目來定義自己的插件。接下來我們以其中的 Qos 的插件來為例,演示是如何開發自己的插件。

Qos 的插件主要基於 Pod 的 QoS(Quality of Service) class 來實現的,目的是為了實現調度過程中如果 Pod 的優先級相同時,根據 Pod 的 Qos 來決定調度順序,調度順序是: 1. Guaranteed (requests == limits) 2. Burstable (requests < limits) 3. BestEffort (requests and limits not set)

1)插件構造

首先插件要定義插件的對象和構造函數:

// QoSSort is a plugin that implements QoS class based sorting.
type Sort struct{}
// New initializes a new plugin and returns it.
func New(_ *runtime.Unknown, _ framework.FrameworkHandle) (framework.Plugin, error) {
    return &Sort{}, nil
}

然后,根據我們插件要對應的 extention point 來實現對應的接口,Qos 是作用於 QueueSort 的部分,所以我們要實現 QueueSort 接口的函數。如下所示,QueueSortPlugin 接口只定義了一個函數 Less,所以我們實現這個函數即可。

// QueueSortPlugin is an interface that must be implemented by "QueueSort" plugins.
// These plugins are used to sort pods in the scheduling queue. Only one queue sort
// plugin may be enabled at a time.
type QueueSortPlugin interface {
    Plugin
    // Less are used to sort pods in the scheduling queue.
    Less(*PodInfo, *PodInfo) bool
}

實現的函數如下。默認的 default QueueSort 在比較的時候,首先比較優先級,然后再比較 pod 的 timestamp。我們重新定義了 Less 函數,在優先級相同的情況下,通過比較 Qos 來決定優先級。

// Less is the function used by the activeQ heap algorithm to sort pods.
// It sorts pods based on their priority. When priorities are equal, it uses
// PodInfo.timestamp.
func (*Sort) Less(pInfo1, pInfo2 *framework.PodInfo) bool {
    p1 := pod.GetPodPriority(pInfo1.Pod)
    p2 := pod.GetPodPriority(pInfo2.Pod)
    return (p1 > p2) || (p1 == p2 && compQOS(pInfo1.Pod, pInfo2.Pod))
}
func compQOS(p1, p2 *v1.Pod) bool {
    p1QOS, p2QOS := v1qos.GetPodQOS(p1), v1qos.GetPodQOS(p2)
    if p1QOS == v1.PodQOSGuaranteed {
        return true
    } else if p1QOS == v1.PodQOSBurstable {
        return p2QOS != v1.PodQOSGuaranteed
    } else {
        return p2QOS == v1.PodQOSBestEffort
    }
}

2)插件注冊

我們在啟動的 main 函數中注冊自己定義的插件和相應的構造函數:

// cmd/main.go
func main() {
    rand.Seed(time.Now().UnixNano())
    command := app.NewSchedulerCommand(
        app.WithPlugin(qos.Name, qos.New),
    )
    if err := command.Execute(); err != nil {
        os.Exit(1)
    }
}

3)代碼編譯

$ make

4)Scheduler 啟動

kube-scheduler 啟動時,配置 ./manifests/qos/scheduler-config.yaml 中 kubeconfig 的路徑,啟動時傳入集群的 kubeconfig 文件以及插件的配置文件即可。

$ bin/kube-scheduler --kubeconfig=scheduler.conf --config=./manifests/qos/scheduler-config.yaml

至此,相信大家已經通過我們的介紹和示例了解了 Kubernetes Scheduling Framework 的架構和開發方法。

后續工作

Kubernetes Scheduling Framework 作為調度器的新架構方向,在可擴展性和定制化方面進步很大。基於此 Kubernetes 可以逐步承載更多類型的應用負載了, 一個平台一套 IT 架構和技術堆棧的願景向前演進。同時為了更好的支持數據計算類型的任務遷移到 Kubernetes 平台中,我們也在努力將數據計算類型中常用Coscheduling/Gang Scheduling、Capacity Scheduling、Dominant Resource Fairness 和多隊列管理等特性,通過 Scheduling Framework 的插件機制來融入到原生的 Kube-scheduler 中。接下來,本系列文章將圍繞 AI、大數據處理和高規格計算資源集群等場景,介紹我們是如何開發相應調度器插件的。敬請期待!

體驗有禮:5 分鍾極速上手 Serverless

“Serverless” 近年來非常火爆。人人都熱衷於探討它出現的意義,但對於如何上手使用或在生產環境落地,卻談之甚少。我們設計了體驗場景,手把手帶你 5 分鍾上手 Serverless,還送 2000 個阿里雲“第一行代碼”鎏金限量馬克杯!

點擊查看詳情https://developer.aliyun.com/adc/series/fc/

阿里巴巴雲原生關注微服務、Serverless、容器、Service Mesh 等技術領域、聚焦雲原生流行技術趨勢、雲原生大規模的落地實踐,做最懂雲原生開發者的公眾號。”


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM