天天看點

進擊的 Kubernetes 排程系統(一):Kubernetes scheduling framework前言新一代排程架構 Scheduling Framework 之解析實作自己的排程插件後續工作

進擊的 Kubernetes 排程系統(一):Kubernetes scheduling framework前言新一代排程架構 Scheduling Framework 之解析實作自己的排程插件後續工作

作者 | 王慶璨(阿裡雲技術專家)、張凱(阿裡雲進階技術專家)

導讀:阿裡雲容器服務團隊結合多年 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 結果,作為最終結果。
進擊的 Kubernetes 排程系統(一):Kubernetes scheduling framework前言新一代排程架構 Scheduling Framework 之解析實作自己的排程插件後續工作

我們可以發現 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 同時部署的方式,來支援自己特殊的排程需求。

進擊的 Kubernetes 排程系統(一):Kubernetes scheduling framework前言新一代排程架構 Scheduling Framework 之解析實作自己的排程插件後續工作

Custom Scheduler 會存在一下問題:

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

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

進擊的 Kubernetes 排程系統(一):Kubernetes scheduling framework前言新一代排程架構 Scheduling Framework 之解析實作自己的排程插件後續工作

新一代排程架構 Scheduling Framework 之解析

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

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

進擊的 Kubernetes 排程系統(一):Kubernetes scheduling framework前言新一代排程架構 Scheduling Framework 之解析實作自己的排程插件後續工作

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 等技術領域、聚焦雲原生流行技術趨勢、雲原生大規模的落地實踐,做最懂雲原生開發者的公衆号。”

繼續閱讀