kube-scheduler predicates 與 priorities 調(diào)度算法源碼分析

在上篇文章kube-scheduler 源碼分析中已經(jīng)介紹了 kube-scheduler 的設(shè)計以及從源碼角度分析了其執(zhí)行流程,這篇文章會專注介紹調(diào)度過程中 predicates 和 priorities 這兩個調(diào)度策略主要發(fā)生作用的階段。

kubernetes 版本: v1.16

predicates 調(diào)度算法源碼分析

predicates 算法主要是對集群中的 node 進行過濾,選出符合當前 pod 運行的 nodes。

調(diào)度算法說明

上節(jié)已經(jīng)提到默認的調(diào)度算法在pkg/scheduler/algorithmprovider/defaults/defaults.go中定義了:

func defaultPredicates() sets.String {
    return sets.NewString(
        predicates.NoVolumeZoneConflictPred,
        predicates.MaxEBSVolumeCountPred,
        predicates.MaxGCEPDVolumeCountPred,
        predicates.MaxAzureDiskVolumeCountPred,
        predicates.MaxCSIVolumeCountPred,
        predicates.MatchInterPodAffinityPred,
        predicates.NoDiskConflictPred,
        predicates.GeneralPred,
        predicates.CheckNodeMemoryPressurePred,
        predicates.CheckNodeDiskPressurePred,
        predicates.CheckNodePIDPressurePred,
        predicates.CheckNodeConditionPred,
        predicates.PodToleratesNodeTaintsPred,
        predicates.CheckVolumeBindingPred,
    )
}

下面是對默認調(diào)度算法的一些說明:

predicates 算法 說明
GeneralPred GeneralPred 包含 PodFitsResources、PodFitsHost,、PodFitsHostPorts、PodMatchNodeSelector 四種算法
NoDiskConflictPred 檢查多個 Pod 聲明掛載的持久化 Volume 是否有沖突
MaxGCEPDVolumeCountPred 檢查 GCE 持久化 Volume 是否超過了一定數(shù)目
MaxAzureDiskVolumeCountPred 檢查 Azure 持久化 Volume 是否超過了一定數(shù)目
MaxCSIVolumeCountPred 檢查 CSI 持久化 Volume 是否超過了一定數(shù)目(已廢棄)
MaxEBSVolumeCountPred 檢查 EBS 持久化 Volume 是否超過了一定數(shù)目
NoVolumeZoneConflictPred 檢查持久化 Volume 的 Zone(高可用域)標簽是否與節(jié)點的 Zone 標簽相匹配
CheckVolumeBindingPred 檢查該 Pod 對應(yīng) PV 的 nodeAffinity 字段是否跟某個節(jié)點的標簽相匹配,Local Persistent Volume(本地持久化卷)必須使用 nodeAffinity 來跟某個具體的節(jié)點綁定
PodToleratesNodeTaintsPred 檢查 Node 的 Taint 機制,只有當 Pod 的 Toleration 字段與 Node 的 Taint 字段能夠匹配時,這個 Pod 才能被調(diào)度到該節(jié)點上
MatchInterPodAffinityPred 檢查待調(diào)度 Pod 與 Node 上的已有 Pod 之間的親密(affinity)和反親密(anti-affinity)關(guān)系
CheckNodeConditionPred 檢查 NodeCondition
CheckNodePIDPressurePred 檢查 NodePIDPressure
CheckNodeDiskPressurePred 檢查 NodeDiskPressure
CheckNodeMemoryPressurePred 檢查 NodeMemoryPressure

默認的 predicates 調(diào)度算法主要分為五種類型:

1、第一種類型叫作 GeneralPredicates,包含 PodFitsResources、PodFitsHost、PodFitsHostPorts、PodMatchNodeSelector 四種策略,其具體含義如下所示:

  • PodFitsHost:檢查宿主機的名字是否跟 Pod 的 spec.nodeName 一致
  • PodFitsHostPorts:檢查 Pod 申請的宿主機端口(spec.nodePort)是不是跟已經(jīng)被使用的端口有沖突
  • PodMatchNodeSelector:檢查 Pod 的 nodeSelector 或者 nodeAffinity 指定的節(jié)點是否與節(jié)點匹配等
  • PodFitsResources:檢查主機的資源是否滿足 Pod 的需求,根據(jù)實際已經(jīng)分配(Request)的資源量做調(diào)度

kubelet 在啟動 Pod 前,會執(zhí)行一個 Admit 操作來進行二次確認,這里二次確認的規(guī)則就是執(zhí)行一遍 GeneralPredicates。

2、第二種類型是與 Volume 相關(guān)的過濾規(guī)則,主要有NoDiskConflictPred、MaxGCEPDVolumeCountPred、MaxAzureDiskVolumeCountPred、MaxCSIVolumeCountPred、MaxEBSVolumeCountPred、NoVolumeZoneConflictPred、CheckVolumeBindingPred。

3、第三種類型是宿主機相關(guān)的過濾規(guī)則,主要是 PodToleratesNodeTaintsPred。

4、第四種類型是 Pod 相關(guān)的過濾規(guī)則,主要是 MatchInterPodAffinityPred。

5、第五種類型是新增的過濾規(guī)則,與宿主機的運行狀況有關(guān),主要有 CheckNodeCondition、 CheckNodeMemoryPressure、CheckNodePIDPressure、CheckNodeDiskPressure 四種。若啟用了 TaintNodesByCondition FeatureGates 則在 predicates 算法中會將該四種算法移除,TaintNodesByCondition 基于 node conditions 當 node 出現(xiàn) pressure 時自動為 node 打上 taints 標簽,該功能在 v1.8 引入,v1.12 成為 beta 版本,目前 v1.16 中也是 beta 版本,但在 v1.13 中該功能已默認啟用。

predicates 調(diào)度算法也有一個順序,要不然在一臺資源已經(jīng)嚴重不足的宿主機上,上來就開始計算 PodAffinityPredicate 是沒有實際意義的,其默認順序如下所示:

k8s.io/kubernetes/pkg/scheduler/algorithm/predicates/predicates.go:146

var (
    predicatesOrdering = []string{CheckNodeConditionPred, CheckNodeUnschedulablePred,
        GeneralPred, HostNamePred, PodFitsHostPortsPred,
        MatchNodeSelectorPred, PodFitsResourcesPred, NoDiskConflictPred,
        PodToleratesNodeTaintsPred, PodToleratesNodeNoExecuteTaintsPred, CheckNodeLabelPresencePred,
        CheckServiceAffinityPred, MaxEBSVolumeCountPred, MaxGCEPDVolumeCountPred, MaxCSIVolumeCountPred,
        MaxAzureDiskVolumeCountPred, MaxCinderVolumeCountPred, CheckVolumeBindingPred, NoVolumeZoneConflictPred,
        CheckNodeMemoryPressurePred, CheckNodePIDPressurePred, CheckNodeDiskPressurePred, EvenPodsSpreadPred, MatchInterPodAffinityPred}
)

源碼分析

上節(jié)中已經(jīng)說到調(diào)用預(yù)選以及優(yōu)選算法的邏輯在 k8s.io/kubernetes/pkg/scheduler/core/generic_scheduler.go:189中,

func (g *genericScheduler) Schedule(pod *v1.Pod, pluginContext *framework.PluginContext) (result ScheduleResult, err error) {
    ......
        
    // 執(zhí)行 predicates 策略
    filteredNodes, failedPredicateMap, filteredNodesStatuses, err := g.findNodesThatFit(pluginContext, pod)
    
    ......

    // 執(zhí)行 priorities 策略
    priorityList, err := PrioritizeNodes(pod, g.nodeInfoSnapshot.NodeInfoMap, metaPrioritiesInterface, g.prioritizers, filteredNodes, g.extenders, g.framework,        pluginContext)
    
    ......
    
    return
}

findNodesThatFit() 是 predicates 策略的實際調(diào)用方法,其基本流程如下:

  • 設(shè)定最多需要檢查的節(jié)點數(shù),作為預(yù)選節(jié)點數(shù)組的容量,避免總節(jié)點過多影響調(diào)度效率
  • 通過NodeTree()不斷獲取下一個節(jié)點來判斷該節(jié)點是否滿足 pod 的調(diào)度條件
  • 通過之前注冊的各種 predicates 函數(shù)來判斷當前節(jié)點是否符合 pod 的調(diào)度條件
  • 最后返回滿足調(diào)度條件的 node 列表,供下一步的優(yōu)選操作

checkNode()是一個校驗 node 是否符合要求的函數(shù),其實際調(diào)用到的核心函數(shù)是podFitsOnNode(),再通過workqueue() 并發(fā)執(zhí)行checkNode() 函數(shù),workqueue() 會啟動 16 個 goroutine 來并行計算需要篩選的 node 列表,其主要流程如下:

  • 通過 cache 中的 NodeTree() 不斷獲取下一個 node
  • 將當前 node 和 pod 傳入podFitsOnNode() 方法中來判斷當前 node 是否符合要求
  • 如果當前 node 符合要求就將當前 node 加入預(yù)選節(jié)點的數(shù)組中filtered
  • 如果當前 node 不滿足要求,則加入到失敗的數(shù)組中,并記錄原因
  • 通過workqueue.ParallelizeUntil()并發(fā)執(zhí)行checkNode()函數(shù),一旦找到足夠的可行節(jié)點數(shù)后就停止篩選更多節(jié)點
  • 若配置了 extender 則再次進行過濾已篩選出的 node

k8s.io/kubernetes/pkg/scheduler/core/generic_scheduler.go:464

func (g *genericScheduler) findNodesThatFit(pluginContext *framework.PluginContext, pod *v1.Pod) ([]*v1.Node, FailedPredicateMap, framework.NodeToStatusMap, error) {
    var filtered []*v1.Node
    failedPredicateMap := FailedPredicateMap{}
    filteredNodesStatuses := framework.NodeToStatusMap{}

    if len(g.predicates) == 0 {
        filtered = g.cache.ListNodes()
    } else {
        allNodes := int32(g.cache.NodeTree().NumNodes())
        // 1.設(shè)定最多需要檢查的節(jié)點數(shù)
        numNodesToFind := g.numFeasibleNodesToFind(allNodes)

        filtered = make([]*v1.Node, numNodesToFind)
        ......
        
        // 2.獲取該 pod 的 meta 值 
        meta := g.predicateMetaProducer(pod, g.nodeInfoSnapshot.NodeInfoMap)

        // 3.checkNode 為執(zhí)行預(yù)選算法的函數(shù)
        checkNode := func(i int) {
            nodeName := g.cache.NodeTree().Next()

            // 4.podFitsOnNode 最終執(zhí)行預(yù)選算法的函數(shù) 
            fits, failedPredicates, status, err := g.podFitsOnNode(
                ......
            )
            if err != nil {
                ......
            }
            if fits {
                length := atomic.AddInt32(&filteredLen, 1)
                if length > numNodesToFind {
                    cancel()
                    atomic.AddInt32(&filteredLen, -1)
                } else {
                    filtered[length-1] = g.nodeInfoSnapshot.NodeInfoMap[nodeName].Node()
                }
            } else {
                ......
            }
        }

        // 5.啟動 16 個 goroutine 并發(fā)執(zhí)行 checkNode 函數(shù)
        workqueue.ParallelizeUntil(ctx, 16, int(allNodes), checkNode)

        filtered = filtered[:filteredLen]
        if len(errs) > 0 {
            ......
        }
    }

    // 6.若配置了 extender 則再次進行過濾
    if len(filtered) > 0 && len(g.extenders) != 0 {
        ......
    }
    return filtered, failedPredicateMap, filteredNodesStatuses, nil
}

然后繼續(xù)看如何設(shè)定最多需要檢查的節(jié)點數(shù),此過程由numFeasibleNodesToFind()進行處理,基本流程如下:

  • 如果總的 node 節(jié)點小于minFeasibleNodesToFind(默認為100)則直接返回總節(jié)點數(shù)
  • 如果節(jié)點數(shù)超過 100,則取指定百分比 percentageOfNodesToScore(默認值為 50)的節(jié)點數(shù) ,當該百分比后的數(shù)目仍小于minFeasibleNodesToFind,則返回minFeasibleNodesToFind
  • 如果百分比后的數(shù)目大于minFeasibleNodesToFind,則返回該百分比的節(jié)點數(shù)

所以當節(jié)點數(shù)小于 100 時直接返回,大于 100 時只返回其總數(shù)的 50%。percentageOfNodesToScore 參數(shù)在 v1.12 引入,默認值為 50,kube-scheduler 在啟動時可以設(shè)定該參數(shù)的值。

k8s.io/kubernetes/pkg/scheduler/core/generic_scheduler.go:441

func (g *genericScheduler) numFeasibleNodesToFind(numAllNodes int32) (numNodes int32) {
    if numAllNodes < minFeasibleNodesToFind || g.percentageOfNodesToScore >= 100 {
        return numAllNodes
    }

    adaptivePercentage := g.percentageOfNodesToScore
    if adaptivePercentage <= 0 {
        adaptivePercentage = schedulerapi.DefaultPercentageOfNodesToScore - numAllNodes/125
        if adaptivePercentage < minFeasibleNodesPercentageToFind {
            adaptivePercentage = minFeasibleNodesPercentageToFind
        }
    }

    numNodes = numAllNodes * adaptivePercentage / 100
    if numNodes < minFeasibleNodesToFind {
        return minFeasibleNodesToFind
    }

    return numNodes
}

pridicates 調(diào)度算法的核心是 podFitsOnNode() ,scheduler 的搶占機制也會執(zhí)行該函數(shù),podFitsOnNode()基本流程如下:

  • 遍歷已經(jīng)注冊好的預(yù)選策略predicates.Ordering(),按順序執(zhí)行對應(yīng)的策略函數(shù)
  • 遍歷執(zhí)行每個策略函數(shù),并返回是否合適,預(yù)選失敗的原因和錯誤
  • 如果預(yù)選函數(shù)執(zhí)行失敗,則加入預(yù)選失敗的數(shù)組中,直接返回,后面的預(yù)選函數(shù)不會再執(zhí)行
  • 如果該 node 上存在 nominated pod 則執(zhí)行兩次預(yù)選函數(shù)

因為引入了搶占機制,此處主要說明一下執(zhí)行兩次預(yù)選函數(shù)的原因:

第一次循環(huán),若該 pod 為搶占者(nominatedPods),調(diào)度器會假設(shè)該 pod 已經(jīng)運行在這個節(jié)點上,然后更新metanodeInfonominatedPods是指執(zhí)行了搶占機制且已經(jīng)分配到了 node(pod.Status.NominatedNodeName 已被設(shè)定) 但是還沒有真正運行起來的 pod,然后再執(zhí)行所有的預(yù)選函數(shù)。

第二次循環(huán),不將nominatedPods加入到 node 內(nèi)。

而只有這兩遍 predicates 算法都能通過時,這個 pod 和 node 才會被認為是可以綁定(bind)的。這樣做是因為考慮到 pod affinity 等策略的執(zhí)行,如果當前的 pod 與nominatedPods有依賴關(guān)系就會有問題,因為nominatedPods不能保證一定可以調(diào)度且在已指定的 node 運行成功,也可能出現(xiàn)被其他高優(yōu)先級的 pod 搶占等問題,關(guān)于搶占問題下篇會詳細介紹。

func (g *genericScheduler) podFitsOnNode(......) (bool, []predicates.PredicateFailureReason, *framework.Status, error) {
    var failedPredicates []predicates.PredicateFailureReason
    var status *framework.Status

    podsAdded := false

    for i := 0; i < 2; i++ {
        metaToUse := meta
        nodeInfoToUse := info
        if i == 0 {
            // 1.第一次循環(huán)加入 NominatedPods,計算 meta, nodeInfo
            podsAdded, metaToUse, nodeInfoToUse = addNominatedPods(pod, meta, info, queue)
        } else if !podsAdded || len(failedPredicates) != 0 {
            break
        }
        // 2.按順序執(zhí)行所有預(yù)選函數(shù)
        for _, predicateKey := range predicates.Ordering() {
            var (
                fit     bool
                reasons []predicates.PredicateFailureReason
                err     error
            )
            if predicate, exist := predicateFuncs[predicateKey]; exist {
                fit, reasons, err = predicate(pod, metaToUse, nodeInfoToUse)
                if err != nil {
                    return false, []predicates.PredicateFailureReason{}, nil, err
                }
                                
                // 3.任何一個預(yù)選函數(shù)執(zhí)行失敗則直接返回
                if !fit {
                    failedPredicates = append(failedPredicates, reasons...)                   
                    if !alwaysCheckAllPredicates {
                        klog.V(5).Infoln("since alwaysCheckAllPredicates has not been set, the predicate " +
                            "evaluation is short circuited and there are chances " +
                            "of other predicates failing as well.")
                        break
                    }
                }
            }
        }
        // 4.執(zhí)行 Filter Plugin
        status = g.framework.RunFilterPlugins(pluginContext, pod, info.Node().Name)
        if !status.IsSuccess() && !status.IsUnschedulable() {
            return false, failedPredicates, status, status.AsError()
        }
    }

    return len(failedPredicates) == 0 && status.IsSuccess(), failedPredicates, status, nil
}

至此,關(guān)于 predicates 調(diào)度算法的執(zhí)行過程已經(jīng)分析完。

priorities 調(diào)度算法源碼分析

priorities 調(diào)度算法是在 pridicates 算法后執(zhí)行的,主要功能是對已經(jīng)過濾出的 nodes 進行打分并選出最佳的一個 node。

調(diào)度算法說明

默認的調(diào)度算法在pkg/scheduler/algorithmprovider/defaults/defaults.go中定義了:

func defaultPriorities() sets.String {
    return sets.NewString(
        priorities.SelectorSpreadPriority,
        priorities.InterPodAffinityPriority,
        priorities.LeastRequestedPriority,
        priorities.BalancedResourceAllocation,
        priorities.NodePreferAvoidPodsPriority,
        priorities.NodeAffinityPriority,
        priorities.TaintTolerationPriority,
        priorities.ImageLocalityPriority,
    )
}

默認調(diào)度算法的一些說明:

priorities 算法 說明
SelectorSpreadPriority 按 service,rs,statefulset 歸屬計算 Node 上分布最少的同類 Pod數(shù)量,數(shù)量越少得分越高,默認權(quán)重為1
InterPodAffinityPriority pod 親和性選擇策略,默認權(quán)重為1
LeastRequestedPriority 選擇空閑資源(CPU 和 Memory)最多的節(jié)點,默認權(quán)重為1,其計算方式為:score = (cpu((capacity-sum(requested))10/capacity) + memory((capacity-sum(requested))10/capacity))/2
BalancedResourceAllocation CPU、Memory 以及 Volume 資源分配最均衡的節(jié)點,默認權(quán)重為1,其計算方式為:score = 10 - variance(cpuFraction,memoryFraction,volumeFraction)*10
NodePreferAvoidPodsPriority 判斷 node annotation 是否有scheduler.alpha.kubernetes.io/preferAvoidPods 標簽,類似于 taints 機制,過濾標簽中定義類型的 pod,默認權(quán)重為10000
NodeAffinityPriority 節(jié)點親和性選擇策略,默認權(quán)重為1
TaintTolerationPriority Pod 是否容忍節(jié)點上的 Taint,優(yōu)先調(diào)度到標記了 Taint 的節(jié)點,默認權(quán)重為1
ImageLocalityPriority 待調(diào)度 Pod 需要使用的鏡像是否存在于該節(jié)點,默認權(quán)重為1

源碼分析

執(zhí)行 priorities 調(diào)度算法的邏輯是在 PrioritizeNodes()函數(shù)中,其目的是執(zhí)行每個 priority 函數(shù)為 node 打分,分數(shù)為 0-10,其功能主要有:

  • PrioritizeNodes() 通過并行運行各個優(yōu)先級函數(shù)來對節(jié)點進行打分
  • 每個優(yōu)先級函數(shù)會給節(jié)點打分,打分范圍為 0-10 分,0 表示優(yōu)先級最低的節(jié)點,10表示優(yōu)先級最高的節(jié)點
  • 每個優(yōu)先級函數(shù)有各自的權(quán)重
  • 優(yōu)先級函數(shù)返回的節(jié)點分數(shù)乘以權(quán)重以獲得加權(quán)分數(shù)
  • 最后計算所有節(jié)點的總加權(quán)分數(shù)

k8s.io/kubernetes/pkg/scheduler/core/generic_scheduler.go:691

func PrioritizeNodes(......) (schedulerapi.HostPriorityList, error) {
    // 1.檢查是否有自定義配置
    if len(priorityConfigs) == 0 && len(extenders) == 0 {
        result := make(schedulerapi.HostPriorityList, 0, len(nodes))
        for i := range nodes {
            hostPriority, err := EqualPriorityMap(pod, meta, nodeNameToInfo[nodes[i].Name])
            if err != nil {
                return nil, err
            }
            result = append(result, hostPriority)
        }
        return result, nil
    }
    ......

    results := make([]schedulerapi.HostPriorityList, len(priorityConfigs), len(priorityConfigs))

    ......
    // 2.使用 workqueue 啟動 16 個 goroutine 并發(fā)為 node 打分
    workqueue.ParallelizeUntil(context.TODO(), 16, len(nodes), func(index int) {
        nodeInfo := nodeNameToInfo[nodes[index].Name]
        for i := range priorityConfigs {
            if priorityConfigs[i].Function != nil {
                continue
            }

            var err error
            results[i][index], err = priorityConfigs[i].Map(pod, meta, nodeInfo)
            if err != nil {
                appendError(err)
                results[i][index].Host = nodes[index].Name
            }
        }
    })

    // 3.執(zhí)行自定義配置
    for i := range priorityConfigs {
                ......
    }
    
    wg.Wait()
    if len(errs) != 0 {
        return schedulerapi.HostPriorityList{}, errors.NewAggregate(errs)
    }

    // 4.運行 Score plugins
    scoresMap, scoreStatus := framework.RunScorePlugins(pluginContext, pod, nodes)
    if !scoreStatus.IsSuccess() {
        return schedulerapi.HostPriorityList{}, scoreStatus.AsError()
    }
    
    result := make(schedulerapi.HostPriorityList, 0, len(nodes))
    // 5.為每個 node 匯總分數(shù)
    for i := range nodes {
        result = append(result, schedulerapi.HostPriority{Host: nodes[i].Name, Score: 0})
        for j := range priorityConfigs {
            result[i].Score += results[j][i].Score * priorityConfigs[j].Weight
        }

        for j := range scoresMap {
            result[i].Score += scoresMap[j][i].Score
        }
    }
    
    // 6.執(zhí)行 extender 
    if len(extenders) != 0 && nodes != nil {
        ......
    }
        ......
    return result, nil
}

總結(jié)

本文主要講述了 kube-scheduler 中的 predicates 調(diào)度算法與 priorities 調(diào)度算法的執(zhí)行流程,可以看到 kube-scheduler 中有許多的調(diào)度策略,但是想要添加自己的策略并不容易,scheduler 目前已經(jīng)朝著提升性能與擴展性的方向演進了,其調(diào)度部分進行性能優(yōu)化的一個最根本原則就是盡最大可能將集群信息 cache 化,以便從根本上提高 predicates 和 priorities 調(diào)度算法的執(zhí)行效率。第二個就是在 bind 階段進行異步處理,只會更新其 cache 里的 pod 和 node 的信息,這種基于“樂觀”假設(shè)的 API 對象更新方式,在 kubernetes 里被稱作 assume,如果這次異步的 bind 過程失敗了,其實也沒有太大關(guān)系,等 scheduler cache 同步之后一切又恢復(fù)正常了。除了上述的“cache 化”和“樂觀綁定”,還有一個重要的設(shè)計,那就是“無鎖化”,predicates 調(diào)度算法與 priorities 調(diào)度算法的執(zhí)行都是并行的,只有在調(diào)度隊列和 scheduler cache 進行操作時,才需要加鎖,而對調(diào)度隊列的操作并不影響主流程。

參考:

https://kubernetes.io/docs/concepts/configuration/scheduling-framework/

predicates-ordering.md

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
禁止轉(zhuǎn)載,如需轉(zhuǎn)載請通過簡信或評論聯(lián)系作者。

推薦閱讀更多精彩內(nèi)容