Go語言開發(fā)kube-scheduler整體架構(gòu)深度剖析
k8s 的調(diào)度器 kube-scheduler
kube-scheduler 作為 k8s 的調(diào)度器,就好比人的大腦,將行動指定傳遞到手腳等器官,進而執(zhí)行對應(yīng)的動作,對于 kube-scheduler 則是將 Pod 分配(調(diào)度)到集群內(nèi)的各個節(jié)點,進而創(chuàng)建容器運行進程,對于k8s來說至關(guān)重要。
為了深入學(xué)習(xí) kube-scheduler,本系從源碼和實戰(zhàn)角度深度學(xué) 習(xí)kube-scheduler,該系列一共分6篇文章,如下:
- kube-scheduler 整體架構(gòu)
- 初始化一個 scheduler
- 一個 Pod 是如何被調(diào)度的
- 如何開發(fā)一個屬于自己的scheduler插件
- 開發(fā)一個 prefilter 擴展點的插件
- 開發(fā)一個 socre 擴展點的插件
本篇先熟悉 kube-scheduler 的整體架構(gòu)設(shè)計,看清全局,做到心里有數(shù),在后面的篇章再庖丁解牛,一步步挖掘細節(jié)。
官方描述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。根據(jù)限制和可用資源,scheduler 確定哪些節(jié)點符合調(diào)度隊列里的 Pod。然后對這些符合的節(jié)點進行打分,然后把Pod綁定到合適的節(jié)點上。一個集群內(nèi)可以存在多個scheduler,而 kube-scheduler 是一個參考實現(xiàn)。
這段話簡單概括下就是:當(dāng)有 pod 需要 scheduler 調(diào)度的時候, scheduler 會根據(jù)一些列規(guī)則挑選出最符合的節(jié)點,然后將Pod綁定到這個Node。
所以 scheduler 主要要做的事就是根據(jù) Nodes 當(dāng)前狀態(tài)和 pod 對資源的需求,按照順序運行一系列指定的算法來挑選出一個Node。
我們可以通過下圖,對上述說的列算法有一個初步的認識,后面我們在展開詳細說
如圖中所示,圖中每一個綠色箭頭在k8s中叫擴展點(extension point),從圖中可以看到一共有10個擴展點,我們可以分個類,如下圖
每一個擴展點可以運行一個或多個算法,在k8s中把這種算法叫做插件(Plugin)。顧名思義,擴展點就是可以擴展的,所以用戶可以開發(fā)自己的插件嵌入擴展點中,我們既可以將自己開發(fā)的插件和系統(tǒng)默認插件同時運行,也可以關(guān)閉系統(tǒng)自帶的插件只運行自己的插件,這部分在后面開發(fā)實踐階段會詳細介紹。
各個類型擴展點
- sort
sort 類型的擴展點只有一個:sort,而且這個擴展點下面只能有一個插件可以運行,如果同時 enable 多個 sort 插件,scheduler 會退出。
在 k8s 中,待調(diào)度的 Pod 會放在一個叫 activeQ 隊列中,這個隊列是一個基于堆實現(xiàn)的優(yōu)先隊列(priority queue),為什么是優(yōu)先隊列呢?
因為你可以對 Pod 設(shè)置優(yōu)先級,將你認為需要優(yōu)先調(diào)度的 Pod 優(yōu)先級調(diào)大,如果隊列里有多個 Pod 需要調(diào)度,就會出現(xiàn)搶占現(xiàn)象,優(yōu)先級高的 Pod 會移動到隊列頭部,scheduler 會優(yōu)先取出這個 Pod 進行調(diào)度。那么這個優(yōu)先級怎么設(shè)置呢?有兩種方法:
如使用 k8s 默認 sort 插件,則可以給 Pod (deployment等方式) 設(shè)置 PriorityClass(創(chuàng)建 PriorityClass 資源并配置deployment);如果你的所有 Pod 都沒有設(shè)置 PriorityClass,那么會根據(jù) Pod 創(chuàng)建的時間先后順序進行調(diào)度。PriorityClass 和 Pod 創(chuàng)建時間是系統(tǒng)默認的排序依據(jù)。
實現(xiàn)自己的 sort 插件定制排序算法,根據(jù)該排序算法實現(xiàn)搶占,例如你可以將包含特定標(biāo)簽的 Pod 移到隊頭。后面會詳細講述如何實現(xiàn)自己的插件來改變系統(tǒng)默認行為。
- filter
filter 類型擴展點有3個:prefilter,filter,postfilter。各個擴展點有多個插件組成的插件集合根據(jù) Pod 的配置共同過濾 Node,如下圖:
preFilter 擴展點主要有兩個作用,一是為后面的擴展點計算 Pod 的一些信息,例如 preFilter 階段的 NodeResourcesFit 算法不會去判斷節(jié)點合適與否,而是計算這個Pod需要多少資源,然后存儲這個信息,在 filter 擴展點的 NodeResourcesFit 插件中會把之前算出來的資源拿出來做判斷;另外一個作用就是過濾一些明顯不符合要求的節(jié)點,這樣可以減少后續(xù)擴展點插件一些無意義的計算。
filter 擴展點主要的作用就是根據(jù)各個插件定義的順序依次執(zhí)行,篩選出符合 Pod 的節(jié)點,這些插件會在 preFilter 后留下的每個 Node 上運行,如果能夠通過所有插件的”考驗“,那么這個節(jié)點就留下來了。如果某個插件判斷這個節(jié)點不符合,那么剩余的所有插件都不會對該節(jié)點做計算。
postFilter 擴展點只會在filter結(jié)束后沒有任何 Node 符合 Pod 的情況下才會運行,否則這個擴展點會被跳過。我們可以看到,這個擴展點在系統(tǒng)只有一個默認的插件, 這個默認插件的作用遍歷這個 Pod 所在的命名空間下面的所有 Pod,查找是否有可以被搶占的 Pod,如果有的話選出一個最合適的 Pod 然后 delete 掉這個Pod,并在待調(diào)度的 Pod 的 status 字段下面配置 nominateNode 為這個被搶占的 Pod。
- score
這個類型的擴展點的作用就是為上面 filter 擴展點篩選出來的所有 Node 進行打分,挑選出一個得分最高(最合適的),這個 Node 就是 Pod 要被調(diào)度上去的節(jié)點。這個這個類型的擴展有 preScore 和 score 兩個,前者是為后者打分做前置準(zhǔn)備的,preScore 的各個插件會計算一些信息供 score使用,這個和 prefilter 比較類似。
- reserve
reserve 類型擴展點系統(tǒng)默認只實現(xiàn)了一個插件:VolumeBinding,更新 Pod 聲明的 PVC 和對應(yīng)的 PV緩存信息,表示該 PV 已經(jīng)被 Pod占用。
- permit
該類型擴展點,系統(tǒng)沒有實現(xiàn)默認的插件,我們就不說了
- bind
該類型擴展點有三個擴展點:preBind、bind和postBind。
preBind 擴展點有一個內(nèi)置插件 VolumeBinding,這個插件會調(diào)用 pv controller 完成綁定操作,在前面的 reserve 也有同名插件,這個插件只是更新了本地緩存中的信息,沒有實際做綁定。
bind 擴展點也只有一個默認的內(nèi)置插件:DefaultBinder,這個插件只做了一件很簡單的事,將 Pod.Spec.nodeName 更新為選出來的那個 node。后面的“故事”就是 kubelet 監(jiān)聽到了 nodeName=Kubelet所在nodename,然后開始創(chuàng)建Pod(容器)。 到了這里,整個調(diào)度流程就結(jié)束了。
從文章開頭的那張圖中我們能夠看到 scheduler 分兩個 cycle: scheduling cycle 和 binding cycle。區(qū)分這兩個 cycle 的原因是為了提升調(diào)度效率。從上面的描述中我們能夠看到,在 bind cycle 中,會有兩次外部 api 調(diào)用:調(diào)用 pv controller 綁定 pv 和調(diào)用 kube-apiserver 綁定 Node,api調(diào)用是耗時的,所以將 bind 擴展點拆分出來,另起一個 go 協(xié)程進行 bind。而在 scheduling cycle 中為了提升效率的一個重要原則就是 Pod、 Node 等信息從本地緩存中獲取,而具體的實現(xiàn)原理就是先使用 list 獲取所有 Node、Pod 的信息,然后再 watch 他們的變化更新本地緩存。
上面我們主要從擴展點和插件方面說明了 scheduler 的架構(gòu)。下面我們從源碼架構(gòu)說說 scheduler 是怎么工作。
kube-scheduler 代碼的主要框架
我們先來看看 kube-scheduler 中的幾個關(guān)鍵組件
- schedulerCache
schedulerCache 緩存 Pod,Node 等信息,各個擴展點的插件在計算時所需要的 Node 和 Pod 信息都是從 schedulerCache 獲取。schedulerCache 具體在內(nèi)部是一個實現(xiàn)了 Cache 接口的 結(jié)構(gòu)體 cacheImpl,我們看下這個結(jié)構(gòu)體:
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 }
說他是緩存,從這個結(jié)構(gòu)體可以看到,實際上就是map,用來存儲 Pod 和 Node 的信息。那么這些數(shù)據(jù)是怎么來的呢?我們來看下一個組件informer
- informer
informer 是 client-go 提供的能力,他的作用是監(jiān)聽目標(biāo)資源的變化,同步到本地緩存。幾乎,在 k8s 的所有組件包括 controller-manager,kube-proxy,kubelet 等都使用了 informer 來監(jiān)聽 kube-apiserver 來獲取資源的變化。舉個例子,比如你執(zhí)行了 kubectl edit 命令改變了一個 deployment 的鏡像版本,k8s 是怎么感知到這個變化,進一步做 Pod 的重建的工作的呢?就是 kube-scheduler 使用了 informer 來監(jiān)聽 Pod 的變化實現(xiàn)的。
具體來說,kube-scheduler 使用 informer 監(jiān)聽了:Node, Pod, CSINode, CSIDriver, CSIStorageCapacity, PersistentVolume, PersistentVolumeClaim, StorageClass。監(jiān)聽 Node,Pod 我們可以理解,那么為什么要監(jiān)聽后面那些資源呢?后面的那些資源都是跟存儲有關(guān),在 preFilter 和 filter 擴展點的插件里面有 Volumebinding 這么一個插件,是檢查系統(tǒng)當(dāng)前是否能夠滿足 Pod 聲明的 PVC,如果不能滿足,那么只能把 Pod 放入 unscheduleableQ 里。但是,后續(xù)如果系統(tǒng)如果可以滿足 Pod 對存儲的需要了,這個 Pod 需要第一時間能夠被創(chuàng)建出來,所以系統(tǒng)必須要能夠?qū)崟r感知到系統(tǒng) PVC 等資源的變化及時將 unscheduleableQ 里面調(diào)度失敗的 Pod 進行重新調(diào)度。這就是 informer 存在的意義了。具體的 informer 的實現(xiàn)原理可以參考這篇文章。
- schedulerQueue
schedulerQueue包含三個隊列:activeQ, podBackoffQ,unschedulablePods。
activeQ 是一個優(yōu)先隊列,基于堆實現(xiàn),用于存放待調(diào)度的 Pod,優(yōu)先級高的會放在隊列頭部,優(yōu)先被調(diào)度。該隊列存放的 Pod 可能的情況有:剛創(chuàng)建未被調(diào)度的Pod;backOffPod 隊列中轉(zhuǎn)移過來的Pod;unschedule 隊列里轉(zhuǎn)移過來的 Pod。
podBackoffQ 也是一個優(yōu)先隊列,用于存放那些異常的Pod,這種 Pod 需要等待一定的時間才能夠被再次調(diào)度,會有協(xié)程定期去讀取這個隊列,然后加入到 activeQ 隊列然后重新調(diào)度。
unschedulablePods 嚴(yán)格上來說不屬于隊列,用于存放調(diào)度失敗的 Pod。這個隊列也會有協(xié)程定期(默認30s)去讀取,然后判斷當(dāng)前時間距離上次調(diào)度時間的差是否超過5Min,如果超過這個時間則把 Pod 移動到 activeQ 重新調(diào)度。
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) }
說完這幾個組件,我們再來看看,當(dāng)一個新的 Pod 創(chuàng)建出來后,這個流程是怎么走的
- informer 監(jiān)聽到了有新建 Pod,根據(jù) Pod 的優(yōu)先級把 Pod 加入到 activeQ 中適當(dāng)位置(即執(zhí)行sort插件);
- scheduler 從 activeQ 隊頭取一個Pod(如果隊列沒有Pod可取,則會一直阻塞;此時假設(shè)就是上述說的新建的 Pod),開始調(diào)度;
- 執(zhí)行 filter 類型擴展點(包括preFilter,filter,postFilter)插件,選出所有符合 Pod 的 Node,如果無法找到符合的 Node, 則把 Pod 加入 unscheduleableQ 中,此次調(diào)度結(jié)束;
- 執(zhí)行 score 擴展點插件,找出最符合 Pod 的 那個Node;
- assume Pod。這一步就是樂觀假設(shè) Pod 已經(jīng)調(diào)度成功,更新緩存中 Node 和 PodStats 信息,到了這里scheduling cycle就已經(jīng)結(jié)束了,然后會開啟新的一輪調(diào)度。至于真正的綁定,則會新起一個協(xié)程。
- 執(zhí)行 reserve 插件;
- 啟動協(xié)程綁定 Pod 到 Node上。實際上就是修改 Pod.spec.nodeName: 選定的node名字,然后調(diào)用 kube-apiserver 接口寫入 etcd。如果綁定失敗了,那么移除緩存中此前加入的信息,然后把 Pod 放入activeQ 中,后續(xù)重新調(diào)度。
- 執(zhí)行 postBinding,該步?jīng)]有實現(xiàn)的插件沒所以沒有做任何事。
以上就是 kube-scheduler 的基本原理。
在后面的文章中,我們會繼續(xù)聊聊 kube-scheduler 是怎么初始化出來的,要想開發(fā)一個自己的插件要做哪些事,更多關(guān)于Go kube-scheduler架構(gòu)的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
探索Golang?Redis實現(xiàn)發(fā)布訂閱功能實例
這篇文章主要介紹了Golang?Redis發(fā)布訂閱功能實例探索,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2024-01-01Go語言如何利用Mutex保障數(shù)據(jù)讀寫正確
這篇文章主要介紹了互斥鎖的實現(xiàn)機制,以及?Go?標(biāo)準(zhǔn)庫的互斥鎖?Mutex?的基本使用方法,文中的示例代碼講解詳細,需要的小伙伴可以參考一下2023-05-05Go語言中init函數(shù)與匿名函數(shù)使用淺析
這篇文章主要介紹了Go語言中init函數(shù)與匿名函數(shù)使用淺析,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)吧2023-01-01