《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 )主要有這些物件:
大概瀏覽一下可以很快找到我們的第一個關注點應該是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)
從如參和返回值其實可以猜到很多東西,行,今天就到這裡,具體的邏輯下回我們再分析~