1. 程式人生 > >9.深入k8s:排程器及其原始碼分析

9.深入k8s:排程器及其原始碼分析

> 轉載請宣告出處哦~,本篇文章釋出於luozhiyun的部落格:https://www.luozhiyun.com > > 原始碼版本是[1.19](https://github.com/kubernetes/kubernetes/tree/release-1.19) 這次講解的是k8s的排程器部分的程式碼,相對來說比較複雜,慢慢的梳理清楚邏輯花費了不少的時間,不過在梳理過程中也對k8s有了一個更深刻的理解。 ![84076041_p0](https://img.luozhiyun.com/20200905191205.jpg) ## 排程的邏輯介紹 排程器的主要職責,就是為一個新創建出來的 Pod,尋找一個最合適的節點(Node)。[kube-scheduler](https://kubernetes.io/docs/reference/command-line-tools-reference/kube-scheduler/) 就是 Kubernetes 叢集的預設排程器。 預設排程器會首先呼叫一組Filter過濾器,也就是使用相應的Predicates的排程演算法來進行過濾。然後,再呼叫一組叫作 Priority 的排程演算法,來給上一步得到的結果裡的每個 Node 打分,然後根據打分來對Node進行排序,找出最優節點,如果多個節點都有最高的優先順序分數,那麼則迴圈分配,確保平均分配給pod。 排程演算法執行完成後,排程器就需要將 Pod 物件的 nodeName 欄位的值,修改為上述 Node 的名字。 Filter過濾器的作用主要是從當前叢集的所有節點中,“過濾”出一系列符合條件的節點,有如下幾種排程策略: 1. GeneralPredicates 這一組過濾規則,負責的是最基礎的排程策略。比如,計算宿主機的 CPU 和記憶體資源等是否夠用; ,等等。 2. Volume過濾規則 這一組過濾規則,負責的是跟容器持久化 Volume 相關的排程策略。如:檢查多個 Pod 宣告掛載的持久化 Volume 是否有衝突;檢查一個節點上某種型別的持久化 Volume 是不是已經超過了一定數目;檢查Pod 對應的 PV 的 nodeAffinity 欄位,是否跟某個節點的標籤相匹配等等。 3. 檢查排程 Pod 是否滿足 Node 本身的某些條件 如PodToleratesNodeTaints負責檢查的就是我們前面經常用到的 Node 的“汙點”機制。NodeMemoryPressurePredicate,檢查的是當前節點的記憶體是不是已經不夠充足。 4. 檢查親密與反親密關係 檢查待排程 Pod 與 Node 上的已有 Pod 之間的親密(affinity)和反親密(anti-affinity)關係。 在呼叫Filter過濾器的時候需要關注整個叢集的資訊,Kubernetes 排程器會在為每個待排程 Pod 執行該排程演算法之前,先將演算法需要的叢集資訊初步計算一遍,然後快取起來。這樣也可以加快執行速度。 而Priorities裡的打分規則包含如:空閒資源(CPU 和 Memory)多的宿主機可以得高權重;CPU和Memory使用都比較均衡則可以得高權重;為了避免這個演算法引發排程堆疊如果大映象分佈的節點數目很少,那麼這些節點的權重就會被調低等。 整個的流程圖如下: ![scheduler](https://img.luozhiyun.com/20200905190537.png) ## 原始碼分析 整個排程過程如流程圖: ![排程流程](https://img.luozhiyun.com/20200905190824.png) ### 例項化Scheduler物件 程式碼路徑:pkg/scheduler/scheduler.go Scheduler物件是執行kube-scheduler元件的主物件,所以kube-scheduler會在執行的時候建立一個scheduler物件: ```go sched, err := scheduler.New(...) ``` 呼叫的scheduler的New方法,這個方法會例項化scheduler物件並返回。 在建立scheduler例項的時候會根據Schedule rAlgorithm Source來例項化排程演算法函式: 程式碼路徑:pkg/scheduler/apis/config/types.go ```go type SchedulerAlgorithmSource struct { Policy *SchedulerPolicySource Provider *string } ``` Policy是通過引數--policy-config-file引數指定排程策略檔案來定義策略。 Providre是通用排程器,是kube-scheduler預設排程方式。 然後會根據設定的策略來建立不同的scheduler: ```go func New(...) (*Scheduler, error) { ... case source.Provider != nil: sc, err := configurator.createFromProvider(*source.Provider) ... case source.Policy != nil: ... sc, err := configurator.createFromConfig(*policy) ... } ``` createFromProvider方法裡面設定好Filter和Score,也就是過濾策略和打分策略: 程式碼路徑:pkg/scheduler/algorithmprovider/registry.go ```go func getDefaultConfig() *schedulerapi.Plugins { return &schedulerapi.Plugins{ ... Filter: &schedulerapi.PluginSet{ Enabled: []schedulerapi.Plugin{ {Name: nodeunschedulable.Name}, {Name: noderesources.FitName}, {Name: nodename.Name}, {Name: nodeports.Name}, {Name: nodeaffinity.Name}, {Name: volumerestrictions.Name}, {Name: tainttoleration.Name}, {Name: nodevolumelimits.EBSName}, {Name: nodevolumelimits.GCEPDName}, {Name: nodevolumelimits.CSIName}, {Name: nodevolumelimits.AzureDiskName}, {Name: volumebinding.Name}, {Name: volumezone.Name}, {Name: podtopologyspread.Name}, {Name: interpodaffinity.Name}, }, }, ... Score: &schedulerapi.PluginSet{ Enabled: []schedulerapi.Plugin{ {Name: noderesources.BalancedAllocationName, Weight: 1}, {Name: imagelocality.Name, Weight: 1}, {Name: interpodaffinity.Name, Weight: 1}, {Name: noderesources.LeastAllocatedName, Weight: 1}, {Name: nodeaffinity.Name, Weight: 1}, {Name: nodepreferavoidpods.Name, Weight: 10000}, // Weight is doubled because: // - This is a score coming from user preference. // - It makes its signal comparable to NodeResourcesLeastAllocated. {Name: podtopologyspread.Name, Weight: 2}, {Name: tainttoleration.Name, Weight: 1}, }, }, ... } } ``` 最後kube-scheduler處理完一系列的邏輯,最後會呼叫到Scheduler的run方法: ```go func (sched *Scheduler) Run(ctx context.Context) { if !cache.WaitForCacheSync(ctx.Done(), sched.scheduledPodsHasSynced) { return } sched.SchedulingQueue.Run() wait.UntilWithContext(ctx, sched.scheduleOne, 0) sched.SchedulingQueue.Close() } ``` ### 排程主邏輯 sched.scheduleOne會被wait.UntilWithContext定時呼叫,直到ctx.Done()返回true為止。sched.scheduleOne是核心實現,主要做了以下幾件事: 1. 通過sched.NextPod()函式從優先佇列中獲取一個優先順序最高的待排程Pod資源物件,如果沒有獲取到,那麼該方法會阻塞住; 2. 通過sched.Algorithm.Schedule排程函式執行Predicates的排程演算法與Priorities演算法,挑選出一個合適的節點; 3. 當沒有找到合適的節點時,排程器會嘗試呼叫prof.RunPostFilterPlugins搶佔低優先順序的Pod資源物件的節點; 4. 當排程器為Pod資源物件選擇了一個合適的節點時,通過sched.bind函式將合適的節點與Pod資源物件繫結在一起; 下面我們直接看一下sched.Algorithm.Schedule方法的實現: 程式碼路徑:pkg/scheduler/core/generic_scheduler.go ```go //將pod排程到某一node上,如果成功則返回node的名稱,如果成功則返回失敗資訊 func (g *genericScheduler) Schedule(ctx context.Context, prof *profile.Profile, state *framework.CycleState, pod *v1.Pod) (result ScheduleResult, err error) { trace := utiltrace.New("Scheduling", utiltrace.Field{Key: "namespace", Value: pod.Namespace}, utiltrace.Field{Key: "name", Value: pod.Name}) defer trace.LogIfLong(100 * time.Millisecond) //檢查pod上宣告的pvc,包括pvc是否存在,是否已被刪除等 if err := podPassesBasicChecks(pod, g.pvcLister); err != nil { return result, err } trace.Step("Basic checks done") if err := g.snapshot(); err != nil { return result, err } trace.Step("Snapshotting scheduler cache and node infos done") if g.nodeInfoSnapshot.NumNodes() == 0 { return result, ErrNoNodesAvailable } startPredicateEvalTime := time.Now() //這裡是Predicates部分的邏輯,負責選出一系列符合條件的節點 feasibleNodes, filteredNodesStatuses, err := g.findNodesThatFitPod(ctx, prof, state, pod) if err != nil { return result, err } trace.Step("Computing predicates done") //表示沒有 找到合適的節點 if len(feasibleNodes) == 0 { return result, &FitError{ Pod: pod, NumAllNodes: g.nodeInfoSnapshot.NumNodes(), FilteredNodesStatuses: filteredNodesStatuses, } } metrics.DeprecatedSchedulingAlgorithmPredicateEvaluationSecondsDuration.Observe(metrics.SinceInSeconds(startPredicateEvalTime)) metrics.DeprecatedSchedulingDuration.WithLabelValues(metrics.PredicateEvaluation).Observe(metrics.SinceInSeconds(startPredicateEvalTime)) startPriorityEvalTime := time.Now() // When only one node after predicate, just use it. //找到唯一的node節點,並返回 if len(feasibleNodes) == 1 { metrics.DeprecatedSchedulingAlgorithmPriorityEvaluationSecondsDuration.Observe(metrics.SinceInSeconds(startPriorityEvalTime)) return ScheduleResult{ SuggestedHost: feasibleNodes[0].Name, EvaluatedNodes: 1 + len(filteredNodesStatuses), FeasibleNodes: 1, }, nil } //如果節點不是唯一,那麼需要進行打分排序 priorityList, err := g.prioritizeNodes(ctx, prof, state, pod, feasibleNodes) if err != nil { return result, err } metrics.DeprecatedSchedulingAlgorithmPriorityEvaluationSecondsDuration.Observe(metrics.SinceInSeconds(startPriorityEvalTime)) metrics.DeprecatedSchedulingDuration.WithLabelValues(metrics.PriorityEvaluation).Observe(metrics.SinceInSeconds(startPriorityEvalTime)) //選擇最佳的節點 host, err := g.selectHost(priorityList) trace.Step("Prioritizing done") return ScheduleResult{ SuggestedHost: host, EvaluatedNodes: len(feasibleNodes) + len(filteredNodesStatuses), FeasibleNodes: len(feasibleNodes), }, err } ``` 這個方法邏輯還是比較清晰的,總共分為如下幾部分: 1. 對pod進行校驗,檢查是否聲明瞭pvc,以及對應的pvc是否已經被刪除等; 2. 呼叫findNodesThatFitPod方法,負責選出一系列符合條件的節點; 3. 如果沒有找到節點或唯一節點,那麼直接返回; 4. 如果找到的節點數超過1,那麼需要呼叫prioritizeNodes方法,進行打分排序; 5. 最後呼叫selectHost選出合適的唯一節點,並返回。 ### Filter過濾篩選節點 下面我們看看findNodesThatFitPod時如何實現篩選過濾的。 程式碼位置:pkg/scheduler/core/generic_scheduler.go ```go func (g *genericScheduler) findNodesThatFitPod(ctx context.Context, prof *profile.Profile, state *framework.CycleState, pod *v1.Pod) ([]*v1.Node, framework.NodeToStatusMap, error) { filteredNodesStatuses := make(framework.NodeToStatusMap) //前置過濾外掛用於預處理 Pod 的相關資訊,或者檢查叢集或 Pod 必須滿足的某些條件。 //如果 PreFilter 外掛返回錯誤,則排程週期將終止 s := prof.RunPreFilterPlugins(ctx, state, pod) if !s.IsSuccess() { if !s.IsUnschedulable() { return nil, nil, s.AsError() } // All nodes will have the same status. Some non trivial refactoring is // needed to avoid this copy. allNodes, err := g.nodeInfoSnapshot.NodeInfos().List() if err != nil { return nil, nil, err } for _, n := range allNodes { filteredNodesStatuses[n.Node().Name] = s } return nil, filteredNodesStatuses, nil } //過濾掉不符合條件的node feasibleNodes, err := g.findNodesThatPassFilters(ctx, prof, state, pod, filteredNodesStatuses) if err != nil { return nil, nil, err } //SchdulerExtender是kubernets外部擴充套件方式,使用者可以根據需求獨立構建排程服務 feasibleNodes, err = g.findNodesThatPassExtenders(pod, feasibleNodes, filteredNodesStatuses) if err != nil { return nil, nil, err } return feasibleNodes, filteredNodesStatuses, nil } ``` 這個方法首先會通過前置過濾器來校驗pod是否符合條件,然後呼叫findNodesThatPassFilters方法過濾掉不符合條件的node。findNodesThatPassExtenders是kubernets留給使用者的外部擴充套件方式,暫且不表。 下面我們接著看findNodesThatPassFilters方法: ```go func (g *genericScheduler) findNodesThatPassFilters(ctx context.Context, prof *profile.Profile, state *framework.CycleState, pod *v1.Pod, statuses framework.NodeToStatusMap) ([]*v1.Node, error) { allNodes, err := g.nodeInfoSnapshot.NodeInfos().List() if err != nil { return nil, err } //根據叢集節點數量選擇參與排程的節點的數量 numNodesToFind := g.numFeasibleNodesToFind(int32(len(allNodes))) //初始化一個大小和numNodesToFind一樣的陣列,用來存放node節點 feasibleNodes := make([]*v1.Node, numNodesToFind) ... checkNode := func(i int) { //我們從上一個排程週期中離開的節點開始檢查節點,以確保所有節點在Pod中被檢查的機會相同。 nodeInfo := allNodes[(g.nextStartNodeIndex+i)%len(allNodes)] fits, status, err := PodPassesFiltersOnNode(ctx, prof.PreemptHandle(), state, pod, nodeInfo) if err != nil { errCh.SendErrorWithCancel(err, cancel) return } //如果該節點合適,那麼放入到feasibleNodes列表中 if fits { length := atomic.AddInt32(&feasibleNodesLen, 1) if length > numNodesToFind { cancel() atomic.AddInt32(&feasibleNodesLen, -1) } else { feasibleNodes[length-1] = nodeInfo.Node() } } else { statusesLock.Lock() if !status.IsSuccess() { statuses[nodeInfo.Node().Name] = status } statusesLock.Unlock() } } ... //開啟16個執行緒尋找符合條件的node節點,數量等於feasibleNodes parallelize.Until(ctx, len(allNodes), checkNode) processedNodes := int(feasibleNodesLen) + len(statuses) //設定下次開始尋找node的位置 g.nextStartNodeIndex = (g.nextStartNodeIndex + processedNodes) % len(allNodes) feasibleNodes = feasibleNodes[:feasibleNodesLen] if err := errCh.ReceiveError(); err != nil { statusCode = framework.Error return nil, err } return feasibleNodes, nil } ``` 在這個方法中首先會根據numFeasibleNodesToFind方法選擇參與排程的節點的數量,然後呼叫parallelize.Until方法開啟16個執行緒來呼叫checkNode方法尋找合適的節點。 對於numFeasibleNodesToFind的邏輯如下: ```go func (g *genericScheduler) numFeasibleNodesToFind(numAllNodes int32) (numNodes int32) { //對於一個小於100的節點,全部節點參與排程 //percentageOfNodesToScore引數值是一個叢集中所有節點的百分比,範圍是1和100之間,0表示不啟用 if numAllNodes < minFeasibleNodesToFind || g.percentageOfNodesToScore >= 100 { return numAllNodes } adaptivePercentage := g.percentageOfNodesToScore //當numAllNodes大於100時,如果沒有設定percentageOfNodesToScore,那麼這裡需要計算出一個值 if adaptivePercentage <= 0 { basePercentageOfNodesToScore := int32(50) adaptivePercentage = basePercentageOfNodesToScore - numAllNodes/125 if adaptivePercentage < minFeasibleNodesPercentageToFind { adaptivePercentage = minFeasibleNodesPercentageToFind } } numNodes = numAllNodes * adaptivePercentage / 100 if numNodes < minFeasibleNodesToFind { return minFeasibleNodesToFind } return numNodes } ``` 找出能夠進行排程的節點,如果節點小於100,那麼全部節點參與排程。 percentageOfNodesToScore引數值是一個叢集中所有節點的百分比,範圍是1和100之間,0表示不啟用。如果叢集節點數大於100,那麼就會根據這個值來計算讓合適的節點數參與排程。 如果一個5000個節點的叢集,percentageOfNodesToScore會預設設定為10%,也就是500個節點參與排程。 因為如果一個5000節點的叢集來進行排程的話,不進行控制時,每個pod排程都需要嘗試5000次的節點預選過程時非常消耗資源的。 然後我們回到findNodesThatPassFilters方法中,我們看一下PodPassesFiltersOnNode是如何篩選出合適的節點的: ```go func PodPassesFiltersOnNode( ctx context.Context, ph framework.PreemptHandle, state *framework.CycleState, pod *v1.Pod, info *framework.NodeInfo, ) (bool, *framework.Status, error) { var status *framework.Status podsAdded := false //待檢查的 Node 是一個即將被搶佔的節點,排程器就會對這個 Node ,將同樣的 Predicates 演算法執行兩遍。 for i := 0; i < 2; i++ { stateToUse := state nodeInfoToUse := info //處理優先順序pod的邏輯 if i == 0 { var err error //查詢是否有優先順序大於或等於當前pod的NominatedPods,然後加入到nodeInfoToUse中 podsAdded, stateToUse, nodeInfoToUse, err = addNominatedPods(ctx, ph, pod, state, info) if err != nil { return false, nil, err } } else if !podsAdded || !status.IsSuccess() { break } //執行過濾器檢查該pod是否能執行在該節點上 statusMap := ph.RunFilterPlugins(ctx, stateToUse, pod, nodeInfoToUse) status = statusMap.Merge() if !status.IsSuccess() && !status.IsUnschedulable() { return false, status, status.AsError() } } return status.IsSuccess(), status, nil } ``` 這個方法用來檢測node是否能通過過濾器,此方法會在排程Schedule和搶佔Preempt的時被呼叫,如果在Schedule時被呼叫,那麼會測試nod,能否可以讓所有存在的pod以及更高優先順序的pod在該node上執行。如果在搶佔時被呼叫,那麼我們首先要移除搶佔失敗的pod,新增將要搶佔的pod。 然後RunFilterPlugins會呼叫runFilterPlugin方法來執行我們上面講的getDefaultConfig中設定的過濾器: ```go func (f *frameworkImpl) runFilterPlugin(ctx context.Context, pl framework.FilterPlugin, state *framework.CycleState, pod *v1.Pod, nodeInfo *framework.NodeInfo) *framework.Status { if !state.ShouldRecordPluginMetrics() { return pl.Filter(ctx, state, pod, nodeInfo) } startTime := time.Now() status := pl.Filter(ctx, state, pod, nodeInfo) f.metricsRecorder.observePluginDurationAsync(Filter, pl.Name(), status, metrics.SinceInSeconds(startTime)) return status } ``` 過濾器總共有這些:nodeunschedulable,noderesources,nodename,nodeports,nodeaffinity,volumerestrictions,tainttoleration,nodevolumelimits,nodevolumelimits,nodevolumelimits,nodevolumelimits,volumebinding,volumezone,podtopologyspread,interpodaffinity 過濾器太多就不一一看了,裡面的邏輯還是很清晰的,感興趣的自己可以看看具體實現。 ### prioritize為節點打分 下面我們繼續回到Schedule方法,執行完findNodesThatFitPod後會找到一系列符合條件的node節點,然後會呼叫prioritizeNodes進行打分排序: ```go func (g *genericScheduler) prioritizeNodes( ctx context.Context, prof *profile.Profile, state *framework.CycleState, pod *v1.Pod, nodes []*v1.Node, ) (framework.NodeScoreList, error) { ... scoresMap, scoreStatus := prof.RunScorePlugins(ctx, state, pod, nodes) if !scoreStatus.IsSuccess() { return nil, scoreStatus.AsError() } // Summarize all scores. result := make(framework.NodeScoreList, 0, len(nodes)) //將分數按照node維度進行彙總 for i := range nodes { result = append(result, framework.NodeScore{Name: nodes[i].Name, Score: 0}) for j := range scoresMap { result[i].Score += scoresMap[j][i].Score } } ... return result, nil } ``` prioritizeNodes裡面會呼叫RunScorePlugins方法,裡面會遍歷一系列的外掛的方式為node打分。然後遍歷scoresMap將結果按照node維度進行聚合。 ```go func (f *frameworkImpl) RunScorePlugins(ctx context.Context, state *framework.CycleState, pod *v1.Pod, nodes []*v1.Node) (ps framework.PluginToNodeScores, status *framework.Status) { ... //開啟16個執行緒為node進行打分 parallelize.Until(ctx, len(nodes), func(index int) { for _, pl := range f.scorePlugins { nodeName := nodes[index].Name s, status := f.runScorePlugin(ctx, pl, state, pod, nodeName) if !status.IsSuccess() { errCh.SendErrorWithCancel(fmt.Errorf(status.Message()), cancel) return } pluginToNodeScores[pl.Name()][index] = framework.NodeScore{ Name: nodeName, Score: int64(s), } } }) if err := errCh.ReceiveError(); err != nil { msg := fmt.Sprintf("error while running score plugin for pod %q: %v", pod.Name, err) klog.Error(msg) return nil, framework.NewStatus(framework.Error, msg) } //用於在排程程式計算節點的最終排名之前修改分數,保證 Score 外掛的輸出必須是 [MinNodeScore,MaxNodeScore]([0-100]) 範圍內的整數 parallelize.Until(ctx, len(f.scorePlugins), func(index int) { pl := f.scorePlugins[index] nodeScoreList := pluginToNodeScores[pl.Name()] if pl.ScoreExtensions() == nil { return } status := f.runScoreExtension(ctx, pl, state, pod, nodeScoreList) if !status.IsSuccess() { err := fmt.Errorf("normalize score plugin %q failed with error %v", pl.Name(), status.Message()) errCh.SendErrorWithCancel(err, cancel) return } }) if err := errCh.ReceiveError(); err != nil { msg := fmt.Sprintf("error while running normalize score plugin for pod %q: %v", pod.Name, err) klog.Error(msg) return nil, framework.NewStatus(framework.Error, msg) } // 為每個節點的分數乘上一個權重 parallelize.Until(ctx, len(f.scorePlugins), func(index int) { pl := f.scorePlugins[index] // Score plugins' weight has been checked when they are initialized. weight := f.pluginNameToWeightMap[pl.Name()] nodeScoreList := pluginToNodeScores[pl.Name()] for i, nodeScore := range nodeScoreList { // return error if score plugin returns invalid score. if nodeScore.Score >
int64(framework.MaxNodeScore) || nodeScore.Score < int64(framework.MinNodeScore) { err := fmt.Errorf("score plugin %q returns an invalid score %v, it should in the range of [%v, %v] after normalizing", pl.Name(), nodeScore.Score, framework.MinNodeScore, framework.MaxNodeScore) errCh.SendErrorWithCancel(err, cancel) return } nodeScoreList[i].Score = nodeScore.Score * int64(weight) } }) ... return pluginToNodeScores, nil } ``` RunScorePlugins裡面分別呼叫parallelize.Until方法跑三次來進行打分: 第一次會呼叫runScorePlugin方法,裡面會呼叫getDefaultConfig裡面設定的score的Plugin來進行打分; 第二次會呼叫runScoreExtension方法,裡面會呼叫Plugin的NormalizeScore方法,用來保證分數必須是0到100之間,不是每一個plugin都會實現NormalizeScore方法。 第三此會呼叫遍歷所有的scorePlugins,並對對應的算出的來的分數乘以一個權重。 打分的plugin共有:noderesources,imagelocality,interpodaffinity,noderesources,nodeaffinity,nodepreferavoidpods,podtopologyspread,tainttoleration ### selectHost選擇合適的節點 在為所有node打完分之後就會呼叫selectHost方法來挑選一個合適的node: ```go func (g *genericScheduler) selectHost(nodeScoreList framework.NodeScoreList) (string, error) { if len(nodeScoreList) == 0 { return "", fmt.Errorf("empty priorityList") } maxScore := nodeScoreList[0].Score selected := nodeScoreList[0].Name cntOfMaxScore := 1 for _, ns := range nodeScoreList[1:] { if ns.Score > maxScore { maxScore = ns.Score selected = ns.Name cntOfMaxScore = 1 } else if ns.Score == maxScore { cntOfMaxScore++ if rand.Intn(cntOfMaxScore) == 0 { // Replace the candidate with probability of 1/cntOfMaxScore selected = ns.Name } } } return selected, nil } ``` 這個方法十分簡單,就是挑選分數高的,如果分數相同,那麼則隨機挑選一個。 ## 總結 通過這篇文章我們深入分析了k8s是如何排程節點的,以及排程節點的時候具體做了什麼事情,熟悉了整個排程流程。通過對排程流程的掌握,可以直到一個pod被排程到node節點上需要經過Predicates的過濾,然後通過對node的打分,最終選擇一個合適的節點進行排程。不過介於Filter以及Score的plugin太多,沒有一一去介紹,感興趣的可以自己去逐個看看。 ## Reference https://kubernetes.io/docs/concepts/scheduling-eviction/kube-scheduler/ https://kubernetes.io/docs/concepts/scheduling-eviction/scheduler-perf-tuning/ https://kubernetes.io/docs/concepts/scheduling-eviction/scheduling-framework/ https://kubernetes.io/docs/concepts/configuration/pod-priority-preemption/ https://www.huweihuang.com/k8s-source-code-analysis/kube-scheduler/preempt.html https://my.oschina.net/u/4131034/blog/3162549 https://www.servicemesher.com/blog/202003-k8s-scheduling-fr