[原始碼分析-kubernetes]3. 排程器框架
排程器框架
寫在前面
今天我們從pkg/scheduler/scheduler.go
出發,分析Scheduler的整體框架。前面講Scheduler設計的時候有提到過原始碼的3層結構,pkg/scheduler/scheduler.go
也就是中間這一層,負責Scheduler除了具體node過濾演算法外的工作邏輯~
排程器啟動執行
從goland的Structure中可以看到這個原始檔(pkg/scheduler/scheduler.go)主要有這些物件:
大概瀏覽一下可以很快找到我們的第一個關注點應該是Scheduler這個struct和Scheduler的Run()方法:
!FILENAME 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.
!FILENAME 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的地方:
!FILENAME 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
.
一個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的主要邏輯:
!FILENAME 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的計算,我們耳熟能詳的預選過程,優選過程等就是在這裡面。
潛入第三層前的一點邏輯
ok,這時候重點就轉移到了suggestedHost, err := sched.schedule(pod)
這個過程,強調一下這個過程是“同步”執行的。
!FILENAME 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方法:
!FILENAME 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)
從如參和返回值其實可以猜到很多東西,行,今天就到這裡,具體的邏輯下回我們再分析~