kubernetes-kube-scheduler進程源碼分析

kubernetes scheduler server是由kube-scheduler進程實現的,它運行在kubernetes的管理節點-Master上並主要負責完成從Pod到Node的調度過程。kubernetes scheduler server跟蹤kubernetes集羣中所有Node的資源利用情況,並採取合適的調度策略,確保調度的均衡性,避免集羣中的某些節點過載。從某種意義來說,kubernetes scheduler server也是kubernetes集羣的大腦。

谷歌作爲公有云的重要供應商,積累了很多經驗並且瞭解客戶的需求。在谷歌看來,客戶並不真正關心他們的服務究竟運行在哪臺機器上,他們最關心服務的可靠性,希望發生故障後能自動恢復,遵循這一思想,kubernetes scheduler server實現了完全市場經濟的調度原則並徹底拋棄了傳統意義上的計劃經濟。

下面對其啓動過程、關鍵代碼分析及設計總結等方面進行深入分析和講解。

 

進程啓動過程:

kube-scheduler進程的入口類源碼位置如下:

cmd/kube-scheduler/scheduler.go

入口main函數的邏輯如下:

對上述代碼的風格和邏輯我們再熟悉不過了:創建一個schedulerserver對象,將命令行參數傳入,並且進入schedulerserver的run方法,無限循環下去。

首先我們看看schedulerserver的數據結構(app/server.go),下面是其定義:

這裏的關鍵屬性有以下兩個。

1.algorithmprovider:對應參數algorithm-provider,是AlgorithmProviderConfig的名稱。

2.PolicyConfigFile:用來加載調度策略文件。

從代碼上來看這兩個參數的作用其實是一樣的,都是加載一組調度規則,這組調度規則要麼在程序裏定義爲一個AlgorithmProviderConfig,要麼保存到文件中。下面的源碼清楚地解釋了這個過程:

創建了schedulerserver結構體實例後,調用此實例的方法func(s* APIServer) Run(_ []string),進入關鍵流程。首先創建一個Rest Client對象用於訪問kubernetes API Server提供的API服務:

隨後,創建一個HTTP server以提供必要的性能分析和性能指標度量(metrics)的rest服務:

接下來,啓動程序構造了configfactory,這個結構體包括了創建一個scheduler所需的必要屬性。

1.Podqueue:需要調度的Pod隊列。

2.BindPodsRateLimiter:調度過程中限制Pod綁定速度的限速器。

3.modeler:這是用於優化Pod調度過程而設計的一個特殊對象,用於預測未來,一個pod被設計調度到機器A的事實被稱爲assumed調度,即假定調度。這些調度安排被保存到特定隊列裏,此時調度過程是能看到這個預安排的,因而會影響到其他Pod的調度。

4.PodLister:負責拉取已經調度過的,以及被假定調度過的Pod列表

5.NodeLister:負責拉取Node節點(minion)列表

6.ServiceLister:負責拉取kubernetes服務列表

7.scheduledPodLister、scheduledPodPopulator:controller框架創建過程中返回的store對象與controller對象,負責定期從kubernetes API server上拉取已經調度好的Pod列表,並將這些Pod從modeler的的假定調度過的隊列中刪除。

在構造configFactory的方法factory.NewConfigFactory(kubeclient)中,我們看到下面這段代碼:

這裏沿用了之前看到的controller framework的身影,上述controller實例所做的事情就是獲取並監聽已經調度的pod列表,並將這些Pod列表從modeler中的assumed隊列中刪除。

接下來,啓動進程用上述創建好的configFactory對象作爲參數來調用schedulerserver的createConfig方法,創建一個scheduler.config對象,而此段代碼的關鍵邏輯幾種在configfactory的createFromKeys這個函數裏,其主要步驟如下:

1.創建一個與Pod相關的reflector對象並定期執行,該reflector負責查詢並監測等待調度的Pod列表,即還沒有分配主機的Pod,然後把它們放入configFactory的PodQueue中等待調度。

2.啓動configfactory的scheduledPodPopulator controller對象,負責定期從kubernetes API server上拉取已經調度好的Pod列表,並將這些Pod從modeler中的假定(assumed)調度過的隊列中刪除。相關代碼爲:go f.scheduledPodPopulator.Run(f.StopEverything)。

3.創建一個Node相關的Reflector對象並定期執行,該Reflector負責查詢並監測可用的Node列表(可用意味着Node的spec.unschedulable屬性爲false),這些Node被放入configFactory的NodeLister.store裏,相關代碼爲:cache.NewReflector。

4.創建一個service相關的reflector對象並定期執行,該reflector負責查詢並監測已定義的service列表,並放入configFactory的serviceLister.Store裏,這個過程的目的是scheduler需要知道一個service當前創建的所有Pod,以便能正確進行調度。相關代碼爲:cache.NewReflector

5.創建一個實現了algorithm.scheduleAlgorithm接口的對象geneticscheduler,它負責完成從Pod到Node的具體調度工作。調度完成的Pod放入configFactory的PodLister裏,相關代碼爲:algo:=scheduler.NewGenericScheduler()。

6.最後一步,使用之前的這些信息創建scheduler.config對象並返回。

從上面分析我們看出,其實在創建scheduler.config的過程中已經完成了kubernetes scheduler server進程中的很多啓動工作,於是整個進程的啓動過程的最後一步簡單明瞭:使用剛剛創建好的config對象來構造一個scheduler對象並啓動運行,即下面兩行代碼:

而scheduler的Run'方法就是不停執行scheduleOne方法:

scheduleOne方法的邏輯也比較清晰,即獲取下一個待調度的Pod,然後交給genericscheduler進行調度(完成Pod到某個Node的綁定過程),調度成功後通知modeler。這個過程同時增加了限流和性能指標的邏輯。

 

 

關鍵代碼分析:

上面對啓動過程進行詳細分析後。我們大致明白了kubernetes scheduler server的工作流程,但由於代碼中涉及多個Pod隊列和Pod狀態切換邏輯,因此這裏有必要對這個問題進行詳細分析,以弄清在這個調度過程中Pod的來龍去脈。首先我們知道configFactory裏的PodQueue是“待調度的Pod隊列”,這個過程是通過無限循環執行一個reflector來從kubernetes API Server上獲取待調度的Pod列表並填充到隊列中實現的,因此Reflector框架已經實現了通用的代碼,所以在kubernetes scheduler server這裏,通過一行代碼就能完成這個複雜的過程:

上述代碼中的createUnassignedPodLW是查詢和監測spec.nodeName爲空的Pod列表,此外,我們注意到scheduler.config裏提供了NextPod這個函數指針來從上述隊列中消費一個元素,下面是相關代碼片段:

然後,這個PodQueue是如何被消費的呢?就在之前提到的scheduler.scheduleOne的方法裏,每次調用NextPod方法會獲取一個可用的Pod,然後交給genericScheduler進行調度,下面是相關代碼片段:

genericscheduler.schedule方法只是給出該Pod調度到的目標Node,如果調度成功,則設置該Pod的spec.nodeName爲目標Node,然後通過HTTP Rest調用寫入kubernetes API Server裏完成Pod的Binding操作,最後通知ConfigFactory的modeler,將此Pod放入Assumed Pod隊列,下面是相關代碼片段:

當Pod執行Bind操作成功後,kubernetes API Server上Pod已經滿足已調度的條件,因爲spec.nodeName已經被設置爲目標Node地址,此時ConfigFactory的scheduledPodPopulator這個controller就會監聽到此變化,將此Pod從modeler中的assumed隊列中刪除,下面是相關代碼片段:

谷歌的大神在源碼中說明modeler的存在是爲了調度的優化,那麼這個優化具體體現在哪呢?由於Rest Watch API存在延時,當前已經調度好的Pod很可能還未通知給scheduler,於是爲每個剛剛調度完成的Pod發放一個“暫住證”,安排暫住到assumed隊列裏,然後設計一個獲取已調度的Pod隊列的新方法,該方法合併assumed隊列與watch緩存隊列,這樣一來,就得到了最佳答案。

接下來,我們深入分析Pod調度中所用到的流控技術,從下面這段代碼開始:

上述代碼中的BindPodsRateLimiter採用了開源項目juju的一個子項目ratelimit,項目地址爲http://github.com/juju/ratelimit,它實現了一個高效的基於經典令牌桶(Token Bucket)的流控算法,如下圖所示是經典令牌桶流控算法的原理:

簡單地說,控制現場以固定速率向一個固定容量的桶(bucket)中投放令牌(token),消費者線程則等待並獲取到一個令牌後才能繼續接下來的任務,否則需要等待可用令牌的到來。具體來說,加入用戶配置的平均限流速率爲r,則每隔1/r秒就會有一個令牌被加入桶中,而令牌桶最多可以存儲b個令牌,如果令牌到達時令牌桶已經滿了,那麼這個令牌會被丟棄。從長期運行結果來看,消費者的處理速率被限制成常量r,令牌桶流控算法除了能限制平均處理速度,還允許某種程度的突發速率。

juju的ratelimit模塊通過以下API提供了構造一個令牌桶的簡單做法,其中,rate參數表示每秒填充到桶裏的令牌數量,capacity則是桶的容量:

我們回頭再看看kubernetes scheduler server中BindPodsRateLimiter的賦值代碼:

根據進去,發現它就是調用了剛纔所提到的juju函數Limiter=ratelimit.NewBuckerWithRate(float 64(qps), init 64(burst)),其中qps目前常量爲15,而burst爲20。

最後我們一起深入分析kubernetes scheduler server中關於Pod調度的細節。首先,我們需要理解啓動過程中國schedulerserver加載調度策略相關配置的這段代碼:

這裏加載了兩組策略,其中predicateFuncs是一個map,key爲FitPredicate的名稱,value爲對應的algorithm.FitPredicate函數,它表明一個候選的Node是否滿足當前Pod的調度要求,FitPredicate函數的具體定義如下:

FitPredicate是Pod調度過程中必須滿足的規則,只有順利通過由多有FitPredicate組成的這道封鎖線,一個Node才能成爲一個合格的候選人,等待下一步評審。目前系統提供的具體的FitPredicate實現都在predicates.go裏,系統默認加載註冊FitPredicate的地方在defaultPredicates方法裏。

當有一組Node通過篩選成爲候選人時,需要有一種方法選擇最優的Node,這就是我們接下來要介紹的priorityConfigs要做的事了。priorityConfigs是一個數組,類型爲algorithm.PriorityConfig,PriorityConfig包括一個PriorityFunction函數,用來計算並給出一組Node的優先級,下面是相關代碼:

如果看到這裏還是不明白它的用途,那麼認真讀下面這段來自genericScheduler的計算候選節點優先級的PrioritizeNodes方法,你就頓悟了:一個候選節點的優先級總分是所有評委老師(PriorityConfig)一起給出的加權總分,評委越是好weight越大,評分的影響力越大。

接下來我們看看系統初始化加載的默認的predicate和priority有哪些,默認加載的代碼位\pkg\scheduler\algorithmprovider\defaults\defaults.go中的init函數中:

跟蹤進去後,可以看到系統默認加載的predicate有如下幾種:

1.PodFitsResources

2.MatchNodeSelector

3,.HostName

而默認加載的priority有如下:

1.LeastRequestdPriority

2.BalancedResourceAllocation

3.ServiceSpreadingPriority

從上述信息看,kubernetes默認的調度指導原則是儘量均勻分佈Pod到不同的Node上,並且確保各個Node上的資源利用率基本保持一致,也就是說如果有100臺機器,則可能每個機器都被調度到,而不是隻有20%,哪怕每臺機器都只利用了不到10%的資源。

 

接下來我們以服務親和性這個默認沒有加載的Predicate爲例,看看kubernetes如何通過policy文件註冊加載它的,下面是我們定義的一個policy文件:

首先,這個文件被映射成api.policy對象(pkg/scheduler/api/types.go),下面是其結構體定義:

我們看到policy文件中的predicate部分被映射稱爲PredicatePolicy數組:

而PredicateArgument的定義如下,包括服務親和性的相關屬性serviceAffinity

策略文件被映射稱爲api.policy對象後,PredicatePolicy部分的處理邏輯則交給下面的函數進行處理(pkg/scheduler/factory/plugin.go)

在上面的代碼中,當serviceaffinity屬性不空時,就會調用predicate.NewServiceAffinityPredicate方法來創建一個處理服務親和性的FitPredicate,隨後被加載到全局的predicateFactory中生效。

最後,genericScheduler.Schedule方法纔是真正實現Pod調度的方法,我們看看這段完整代碼:

這段代碼很簡單,因爲該乾的活已經被predicate和priority幹完了。

架構之美,在於程序邏輯分析分解到恰到好處。

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