首頁 > 軟體

Go語言開發kube-scheduler整體架構深度剖析

2023-12-07 14:01:13

k8s 的排程器 kube-scheduler

kube-scheduler 作為 k8s 的排程器,就好比人的大腦,將行動指定傳遞到手腳等器官,進而執行對應的動作,對於 kube-scheduler 則是將 Pod 分配(排程)到叢集內的各個節點,進而建立容器執行程序,對於k8s來說至關重要。

為了深入學習 kube-scheduler,本系從原始碼和實戰角度深度學 習kube-scheduler,該系列一共分6篇文章,如下:

  • kube-scheduler 整體架構
  • 初始化一個 scheduler
  • 一個 Pod 是如何被排程的
  • 如何開發一個屬於自己的scheduler外掛
  • 開發一個 prefilter 擴充套件點的外掛
  • 開發一個 socre 擴充套件點的外掛

本篇先熟悉 kube-scheduler 的整體架構設計,看清全域性,做到心裡有數,在後面的篇章再庖丁解牛,一步步挖掘細節。

官方描述scheduler

我們先看看官方是怎麼描述 scheduler 的

The Kubernetes scheduler is a control plane process which assigns Pods to Nodes. The scheduler determines which Nodes are valid placements for each Pod in the scheduling queue according to constraints and available resources. The scheduler then ranks each valid Node and binds the Pod to a suitable Node. Multiple different schedulers may be used within a cluster; kube-scheduler is the reference implementation.

k8s scheduler 是一個控制面程序,它分配 Pod 到 Nodes。根據限制和可用資源,scheduler 確定哪些節點符合排程佇列裡的 Pod。然後對這些符合的節點進行打分,然後把Pod繫結到合適的節點上。一個叢集內可以存在多個scheduler,而 kube-scheduler 是一個參考實現。

這段話簡單概括下就是:當有 pod 需要 scheduler 排程的時候, scheduler 會根據一些列規則挑選出最符合的節點,然後將Pod繫結到這個Node。

所以 scheduler 主要要做的事就是根據 Nodes 當前狀態和 pod 對資源的需求,按照順序執行一系列指定的演演算法來挑選出一個Node。

我們可以通過下圖,對上述說的列演演算法有一個初步的認識,後面我們在展開詳細說

如圖中所示,圖中每一個綠色箭頭在k8s中叫擴充套件點(extension point),從圖中可以看到一共有10個擴充套件點,我們可以分個類,如下圖

每一個擴充套件點可以執行一個或多個演演算法,在k8s中把這種演演算法叫做外掛(Plugin)。顧名思義,擴充套件點就是可以擴充套件的,所以使用者可以開發自己的外掛嵌入擴充套件點中,我們既可以將自己開發的外掛和系統預設外掛同時執行,也可以關閉系統自帶的外掛只執行自己的外掛,這部分在後面開發實踐階段會詳細介紹。

各個型別擴充套件點

  • sort

sort 型別的擴充套件點只有一個:sort,而且這個擴充套件點下面只能有一個外掛可以執行,如果同時 enable 多個 sort 外掛,scheduler 會退出。

在 k8s 中,待排程的 Pod 會放在一個叫 activeQ 佇列中,這個佇列是一個基於堆實現的優先佇列(priority queue),為什麼是優先佇列呢?

因為你可以對 Pod 設定優先順序,將你認為需要優先排程的 Pod 優先順序調大,如果佇列裡有多個 Pod 需要排程,就會出現搶佔現象,優先順序高的 Pod 會移動到佇列頭部,scheduler 會優先取出這個 Pod 進行排程。那麼這個優先順序怎麼設定呢?有兩種方法:

  • 如使用 k8s 預設 sort 外掛,則可以給 Pod (deployment等方式) 設定 PriorityClass(建立 PriorityClass 資源並設定deployment);如果你的所有 Pod 都沒有設定 PriorityClass,那麼會根據 Pod 建立的時間先後順序進行排程。PriorityClass 和 Pod 建立時間是系統預設的排序依據。

  • 實現自己的 sort 外掛客製化排序演演算法,根據該排序演演算法實現搶佔,例如你可以將包含特定標籤的 Pod 移到隊頭。後面會詳細講述如何實現自己的外掛來改變系統預設行為。

  • filter

filter 型別擴充套件點有3個:prefilter,filter,postfilter。各個擴充套件點有多個外掛組成的外掛集合根據 Pod 的設定共同過濾 Node,如下圖:

preFilter 擴充套件點主要有兩個作用,一是為後面的擴充套件點計算 Pod 的一些資訊,例如 preFilter 階段的 NodeResourcesFit 演演算法不會去判斷節點合適與否,而是計算這個Pod需要多少資源,然後儲存這個資訊,在 filter 擴充套件點的 NodeResourcesFit 外掛中會把之前算出來的資源拿出來做判斷;另外一個作用就是過濾一些明顯不符合要求的節點,這樣可以減少後續擴充套件點外掛一些無意義的計算。

filter 擴充套件點主要的作用就是根據各個外掛定義的順序依次執行,篩選出符合 Pod 的節點,這些外掛會在 preFilter 後留下的每個 Node 上執行,如果能夠通過所有外掛的”考驗“,那麼這個節點就留下來了。如果某個外掛判斷這個節點不符合,那麼剩餘的所有外掛都不會對該節點做計算。

postFilter 擴充套件點只會在filter結束後沒有任何 Node 符合 Pod 的情況下才會執行,否則這個擴充套件點會被跳過。我們可以看到,這個擴充套件點在系統只有一個預設的外掛, 這個預設外掛的作用遍歷這個 Pod 所在的名稱空間下面的所有 Pod,查詢是否有可以被搶佔的 Pod,如果有的話選出一個最合適的 Pod 然後 delete 掉這個Pod,並在待排程的 Pod 的 status 欄位下面設定 nominateNode 為這個被搶佔的 Pod。

  • score

這個型別的擴充套件點的作用就是為上面 filter 擴充套件點篩選出來的所有 Node 進行打分,挑選出一個得分最高(最合適的),這個 Node 就是 Pod 要被排程上去的節點。這個這個型別的擴充套件有 preScore 和 score 兩個,前者是為後者打分做前置準備的,preScore 的各個外掛會計算一些資訊供 score使用,這個和 prefilter 比較類似。

  • reserve

reserve 型別擴充套件點系統預設只實現了一個外掛:VolumeBinding,更新 Pod 宣告的 PVC 和對應的 PV快取資訊,表示該 PV 已經被 Pod佔用。

  • permit

該型別擴充套件點,系統沒有實現預設的外掛,我們就不說了

  • bind

該型別擴充套件點有三個擴充套件點:preBind、bind和postBind。

preBind 擴充套件點有一個內建外掛 VolumeBinding,這個外掛會呼叫 pv controller 完成繫結操作,在前面的 reserve 也有同名外掛,這個外掛只是更新了本地快取中的資訊,沒有實際做繫結。

bind 擴充套件點也只有一個預設的內建外掛:DefaultBinder,這個外掛只做了一件很簡單的事,將 Pod.Spec.nodeName 更新為選出來的那個 node。後面的“故事”就是 kubelet 監聽到了 nodeName=Kubelet所在nodename,然後開始建立Pod(容器)。 到了這裡,整個排程流程就結束了。

從文章開頭的那張圖中我們能夠看到 scheduler 分兩個 cycle: scheduling cycle 和 binding cycle。區分這兩個 cycle 的原因是為了提升排程效率。從上面的描述中我們能夠看到,在 bind cycle 中,會有兩次外部 api 呼叫:呼叫 pv controller 繫結 pv 和呼叫 kube-apiserver 繫結 Node,api呼叫是耗時的,所以將 bind 擴充套件點拆分出來,另起一個 go 協程進行 bind。而在 scheduling cycle 中為了提升效率的一個重要原則就是 Pod、 Node 等資訊從本地快取中獲取,而具體的實現原理就是先使用 list 獲取所有 Node、Pod 的資訊,然後再 watch 他們的變化更新本地快取。

上面我們主要從擴充套件點和外掛方面說明了 scheduler 的架構。下面我們從原始碼架構說說 scheduler 是怎麼工作。

kube-scheduler 程式碼的主要框架

我們先來看看 kube-scheduler 中的幾個關鍵元件

  • schedulerCache

schedulerCache 快取 Pod,Node 等資訊,各個擴充套件點的外掛在計算時所需要的 Node 和 Pod 資訊都是從 schedulerCache 獲取。schedulerCache 具體在內部是一個實現了 Cache 介面的 結構體 cacheImpl,我們看下這個結構體:

type cacheImpl struct {
	stop   <-chan struct{}
	ttl    time.Duration
	period time.Duration
	// This mutex guards all fields within this cache struct.
	mu sync.RWMutex
	// a set of assumed pod keys.
	// The key could further be used to get an entry in podStates.
	assumedPods sets.String
	// a map from pod key to podState.
	podStates map[string]*podState
	nodes     map[string]*nodeInfoListItem
	// headNode points to the most recently updated NodeInfo in "nodes". It is the
	// head of the linked list.
	headNode *nodeInfoListItem
	nodeTree *nodeTree
	// A map from image name to its imageState.
	imageStates map[string]*imageState
}

說他是快取,從這個結構體可以看到,實際上就是map,用來儲存 Pod 和 Node 的資訊。那麼這些資料是怎麼來的呢?我們來看下一個元件informer

  • informer

informer 是 client-go 提供的能力,他的作用是監聽目標資源的變化,同步到本地快取。幾乎,在 k8s 的所有元件包括 controller-manager,kube-proxy,kubelet 等都使用了 informer 來監聽 kube-apiserver 來獲取資源的變化。舉個例子,比如你執行了 kubectl edit 命令改變了一個 deployment 的映象版本,k8s 是怎麼感知到這個變化,進一步做 Pod 的重建的工作的呢?就是 kube-scheduler 使用了 informer 來監聽 Pod 的變化實現的。

具體來說,kube-scheduler 使用 informer 監聽了:Node, Pod, CSINode, CSIDriver, CSIStorageCapacity, PersistentVolume, PersistentVolumeClaim, StorageClass。監聽 Node,Pod 我們可以理解,那麼為什麼要監聽後面那些資源呢?後面的那些資源都是跟儲存有關,在 preFilter 和 filter 擴充套件點的外掛裡面有 Volumebinding 這麼一個外掛,是檢查系統當前是否能夠滿足 Pod 宣告的 PVC,如果不能滿足,那麼只能把 Pod 放入 unscheduleableQ 裡。但是,後續如果系統如果可以滿足 Pod 對儲存的需要了,這個 Pod 需要第一時間能夠被建立出來,所以系統必須要能夠實時感知到系統 PVC 等資源的變化及時將 unscheduleableQ 裡面排程失敗的 Pod 進行重新排程。這就是 informer 存在的意義了。具體的 informer 的實現原理可以參考這篇文章

  • schedulerQueue

schedulerQueue包含三個佇列:activeQ, podBackoffQ,unschedulablePods。

activeQ 是一個優先佇列,基於堆實現,用於存放待排程的 Pod,優先順序高的會放在佇列頭部,優先被排程。該佇列存放的 Pod 可能的情況有:剛建立未被排程的Pod;backOffPod 佇列中轉移過來的Pod;unschedule 佇列裡轉移過來的 Pod。

podBackoffQ 也是一個優先佇列,用於存放那些異常的Pod,這種 Pod 需要等待一定的時間才能夠被再次排程,會有協程定期去讀取這個佇列,然後加入到 activeQ 佇列然後重新排程。

unschedulablePods 嚴格上來說不屬於佇列,用於存放排程失敗的 Pod。這個佇列也會有協程定期(預設30s)去讀取,然後判斷當前時間距離上次排程時間的差是否超過5Min,如果超過這個時間則把 Pod 移動到 activeQ 重新排程。

func (p *PriorityQueue) Run() {
	go wait.Until(p.flushBackoffQCompleted, 1.0*time.Second, p.stop)
	go wait.Until(p.flushUnschedulablePodsLeftover, 30*time.Second, p.stop)
}

說完這幾個元件,我們再來看看,當一個新的 Pod 建立出來後,這個流程是怎麼走的

  • informer 監聽到了有新建 Pod,根據 Pod 的優先順序把 Pod 加入到 activeQ 中適當位置(即執行sort外掛);
  • scheduler 從 activeQ 隊頭取一個Pod(如果佇列沒有Pod可取,則會一直阻塞;此時假設就是上述說的新建的 Pod),開始排程;
  • 執行 filter 型別擴充套件點(包括preFilter,filter,postFilter)外掛,選出所有符合 Pod 的 Node,如果無法找到符合的 Node, 則把 Pod 加入 unscheduleableQ 中,此次排程結束;
  • 執行 score 擴充套件點外掛,找出最符合 Pod 的 那個Node;
  • assume Pod。這一步就是樂觀假設 Pod 已經排程成功,更新快取中 Node 和 PodStats 資訊,到了這裡scheduling cycle就已經結束了,然後會開啟新的一輪排程。至於真正的繫結,則會新起一個協程。
  • 執行 reserve 外掛;
  • 啟動協程繫結 Pod 到 Node上。實際上就是修改 Pod.spec.nodeName: 選定的node名字,然後呼叫 kube-apiserver 介面寫入 etcd。如果繫結失敗了,那麼移除快取中此前加入的資訊,然後把 Pod 放入activeQ 中,後續重新排程。
  • 執行 postBinding,該步沒有實現的外掛沒所以沒有做任何事。

以上就是 kube-scheduler 的基本原理。

在後面的文章中,我們會繼續聊聊 kube-scheduler 是怎麼初始化出來的,要想開發一個自己的外掛要做哪些事,更多關於Go kube-scheduler架構的資料請關注it145.com其它相關文章!


IT145.com E-mail:sddin#qq.com