在上篇文章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é)點上,然后更新meta
和nodeInfo
,nominatedPods
是指執(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/