1. 程式人生 > 其它 >[原始碼分析-kubernetes]3. 排程器框架

[原始碼分析-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)

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