本文原始地址(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)主要有這些對象:
大概瀏覽一下可以很快找到我們的第一個關注點應該是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的過程足夠了,這里來個流程圖吧:
不考慮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)
從如參和返回值其實可以猜到很多東西,行,今天就到這里,具體的邏輯下回我們再分析~