《k8s-1.13版本源碼分析》-調度器框架


本文原始地址(gitbook格式):https://farmer-hutao.github.io/k8s-source-code-analysis/core/scheduler/scheduler-framework.html

本項目github地址:https://github.com/farmer-hutao/k8s-source-code-analysis

1. 寫在前面

今天我們從pkg/scheduler/scheduler.go出發,分析Scheduler的整體框架。前面講Scheduler設計的時候有提到過源碼的3層結構,pkg/scheduler/scheduler.go也就是中間這一層,負責Scheduler除了具體node過濾算法外的工作邏輯~

這一層我們先盡可能找主線,順着主線走通一遍,就像走一個迷宮,一條通路走出去后心里就有地了,但是迷宮中的很多角落是未曾涉足的。我們盡快走通主流程后,再就一些主要知識點專題攻破,比如k8s里面的List-Watch,Informer等好玩的東西。

2. 調度器啟動運行

從goland的Structure中可以看到這個源文件(pkg/scheduler/scheduler.go)主要有這些對象:

1551161117264

大概瀏覽一下可以很快找到我們的第一個關注點應該是Scheduler這個struct和Scheduler的Run()方法:

pkg/scheduler/scheduler.go:58

// Scheduler watches for new unscheduled pods. It attempts to find // nodes that they fit on and writes bindings back to the api server. type Scheduler struct { config *factory.Config } 

這個struct在上一講有跟到過,代碼注釋說的是:

Scheduler watch新創建的未被調度的pods,然后嘗試尋找合適的node,回寫一個綁定關系到api server.

這個注釋有個小問題就是用了復數形式,其實最后過濾出來的只有一個node;當然這種小問題知道就好,提到github上人家會覺得你在刷commit.接着往下看,Scheduler綁定了一個Run()方法,如下:

pkg/scheduler/scheduler.go:276

// Run begins watching and scheduling. It waits for cache to be synced, then starts a goroutine and returns immediately. func (sched *Scheduler) Run() { if !sched.config.WaitForCacheSync() { return } go wait.Until(sched.scheduleOne, 0, sched.config.StopEverything) } 

注釋說這個函數開始watching and scheduling,也就是調度器主要邏輯了!注釋后半段說到Run()方法起了一個goroutine后馬上返回了,這個怎么理解呢?我們先看一下調用Run的地方:

cmd/kube-scheduler/app/server.go:240

    // Prepare a reusable runCommand function. run := func(ctx context.Context) { sched.Run() <-ctx.Done() } 

可以發現調用了sched.Run()之后就在等待ctx.Done()了,所以Run中啟動的goroutine自己不退出就ok.

wait.Until這個函數做的事情是:每隔n時間調用f一次,除非channel c被關閉。這里的n就是0,也就是一直調用,前一次調用返回下一次調用就開始了。這里的f當然就是sched.scheduleOne,c就是sched.config.StopEverything.

3. 一個pod的調度流程

於是我們的關注點就轉到了sched.scheduleOne這個方法上,看一下:

scheduleOne does the entire scheduling workflow for a single pod. It is serialized on the scheduling algorithm's host fitting.

注釋里說scheduleOne實現1個pod的完整調度工作流,這個過程是順序執行的,也就是非並發的。結合前面的wait.Until邏輯,也就是說前一個pod的scheduleOne一完成,一個return,下一個pod的scheduleOne立馬接着執行!

這里的串行邏輯也好理解,如果是同時調度N個pod,計算的時候覺得一個node很空閑,實際調度過去啟動的時候發現別人的一群pod先起來了,端口啊,內存啊,全給你搶走了!所以這里的調度算法執行過程用串行邏輯很好理解。注意哦,調度過程跑完不是說要等pod起來,最后一步是寫一個binding到apiserver,所以不會太慢。下面我們看一下scheduleOne的主要邏輯:

pkg/scheduler/scheduler.go:513

func (sched *Scheduler) scheduleOne() { pod := sched.config.NextPod() suggestedHost, err := sched.schedule(pod) if err != nil { if fitError, ok := err.(*core.FitError); ok { preemptionStartTime := time.Now() sched.preempt(pod, fitError) } return } assumedPod := pod.DeepCopy() allBound, err := sched.assumeVolumes(assumedPod, suggestedHost) err = sched.assume(assumedPod, suggestedHost) go func() { err := sched.bind(assumedPod, &v1.Binding{ ObjectMeta: metav1.ObjectMeta{Namespace: assumedPod.Namespace, Name: assumedPod.Name, UID: assumedPod.UID}, Target: v1.ObjectReference{ Kind: "Node", Name: suggestedHost, }, }) }() } 

上面幾行代碼只保留了主干,對於我們理解scheduleOne的過程足夠了,這里來個流程圖吧:

1551419004854

不考慮scheduleOne的所有細節和各種異常情況,基本是上圖的流程了,主流程的核心步驟當然是suggestedHost, err := sched.schedule(pod)這一行,這里完成了不需要搶占的場景下node的計算,我們耳熟能詳的預選過程,優選過程等就是在這里面。

4. 潛入第三層前的一點邏輯

ok,這時候重點就轉移到了suggestedHost, err := sched.schedule(pod)這個過程,強調一下這個過程是“同步”執行的。

pkg/scheduler/scheduler.go:290

// schedule implements the scheduling algorithm and returns the suggested host. func (sched *Scheduler) schedule(pod *v1.Pod) (string, error) { host, err := sched.config.Algorithm.Schedule(pod, sched.config.NodeLister) if err != nil { pod = pod.DeepCopy() sched.config.Error(pod, err) sched.config.Recorder.Eventf(pod, v1.EventTypeWarning, "FailedScheduling", "%v", err) sched.config.PodConditionUpdater.Update(pod, &v1.PodCondition{ Type: v1.PodScheduled, Status: v1.ConditionFalse, LastProbeTime: metav1.Now(), Reason: v1.PodReasonUnschedulable, Message: err.Error(), }) return "", err } return host, err } 

schedule方法很簡短,我們關注一下第一行,調用sched.config.Algorithm.Schedule()方法,入參是pod和nodes,返回一個host,繼續看一下這個Schedule方法:

pkg/scheduler/algorithm/scheduler_interface.go:78

type ScheduleAlgorithm interface { Schedule(*v1.Pod, NodeLister) (selectedMachine string, err error) Preempt(*v1.Pod, NodeLister, error) (selectedNode *v1.Node, preemptedPods []*v1.Pod, cleanupNominatedPods []*v1.Pod, err error) Predicates() map[string]FitPredicate Prioritizers() []PriorityConfig } 

發現是個接口,這個接口有4個方法,實現ScheduleAlgorithm接口的對象意味着知道如何調度pods到nodes上。默認的實現是pkg/scheduler/core/generic_scheduler.go:98 genericScheduler這個struct.我們先繼續看一下ScheduleAlgorithm接口定義的4個方法:

  • Schedule() //給定pod和nodes,計算出一個適合跑pod的node並返回;
  • Preempt() //搶占
  • Predicates() //預選
  • Prioritizers() //優選

前面流程里講到的sched.config.Algorithm.Schedule()也就是genericScheduler.Schedule()方法了,這個方法位於:pkg/scheduler/core/generic_scheduler.go:139一句話概括這個方法就是:嘗試將指定的pod調度到給定的node列表中的一個,如果成功就返回這個node的名字。最后看一眼簽名:

func (g *genericScheduler) Schedule(pod *v1.Pod, nodeLister algorithm.NodeLister) (string, error) 

從如參和返回值其實可以猜到很多東西,行,今天就到這里,具體的邏輯下回我們再分析~

 


免責聲明!

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



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