图解kubernetes调度器预选设计实现学习

Scheduler中在进行node选举的时候会首先进行一轮预选流程,即从当前集群中选择一批node节点,本文主要分析k8s在预选流程上一些优秀的筛选设计思想,欢迎大佬们指正

1. 基础设计

1.1 预选场景

预选顾名思义就是从当前集群中的所有的node中,选择出满足当前pod资源和亲和性等需求的node节点,如何在集群中快速选择这样的节点,是个复杂的问题

1.2 平均分布

平均分布主要是通过让一个分配索引来进行即只有当所有的node都在本轮分配周期内分配一次后,才开始从头进行分配,从而保证集群的平均分布

1.3 预选中断

预选终端即在预选的过程中如果发现node已经不能满足当前pod资源需求的时候,就进行中断预选流程,尝试下一个节点

1.4 并行筛选

在当前k8s版本中,默认会启动16个goroutine来进行并行的预选,从而提高性能,从而提高预选的性能

1.5 局部最优解

预选流程需要从当前集群中选择一台符合要求的node随着集群规模的增长,如果每次遍历所有集群node则会必然导致性能的下降,于是通过局部最优解的方式,缩小筛选节点的数量

2. 源码分析

预选的核心流程是通过findNodesThatFit来完成,其返回预选结果供优选流程使用

2.1 取样逻辑

取样是通过当前集群中的node数量和默认的最小值来决定本次预选阶段需要获取的node节点数量

        // 获取所有的节点数量,并通过计算百分比,获取本次选举选择的节点数量
        allNodes := int32(g.cache.NodeTree().NumNodes())
        // 确定要查找node数量
        numNodesToFind := g.numFeasibleNodesToFind(allNodes)

2.2 取样算法

取样算法很简单从集群中获取指定百分比的节点默认是50%,如果50%的节点数量小于minFeasibleNodesToFind则按照minFeasibleNodesToFind(最小取样节点数量)来取样,

func (g *genericScheduler) numFeasibleNodesToFind(numAllNodes int32) (numNodes int32) {
    // 如果当前节点数量小于minFeasibleNodesToFind即小于100台node
    // 同理百分比如果大于100就是全量取样
    // 这两种情况都直接遍历整个集群中所有节点
    if numAllNodes < minFeasibleNodesToFind || g.percentageOfNodesToScore >= 100 {
        return numAllNodes
    }

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

    // 正常取样计算:比如numAllNodes为5000,而adaptivePercentage为50%
    // 则numNodes=50000*0.5/100=250
    numNodes = numAllNodes * adaptivePercentage / 100
    if numNodes < minFeasibleNodesToFind { // 如果小于最少取样则按照最少取样进行取样
        return minFeasibleNodesToFind
    }

    return numNodes
}

2.3 取样元数据准备

通过filtered来进行预选结果的存储,通过filteredLen来进行原子保护协作多个取样goroutine, 并通过predicateMetaProducer和当前的snapshot来进行元数据构建

        filtered = make([]*v1.Node, numNodesToFind)
        errs := errors.MessageCountMap{}
        var (
            predicateResultLock sync.Mutex
            filteredLen         int32
        )

        ctx, cancel := context.WithCancel(context.Background())

        // We can use the same metadata producer for all nodes.
        meta := g.predicateMetaProducer(pod, g.nodeInfoSnapshot.NodeInfoMap)

2.4 通过channel协作并行取样

并行取样主要通过调用下面的函数来启动16个goroutine来进行并行取样,并通过ctx来协调退出

workqueue.ParallelizeUntil(ctx, 16, int(allNodes), checkNode)


通过channel来构建取样索引的管道,每个worker会负责从channel获取的指定索引取样node的填充

func ParallelizeUntil(ctx context.Context, workers, pieces int, doWorkPiece DoWorkPieceFunc) {
    var stop <-chan struct{}
    if ctx != nil {
        stop = ctx.Done()
    }

    // 生成指定数量索引,worker通过索引来进行预选成功节点的存储
    toProcess := make(chan int, pieces)
    for i := 0; i < pieces; i++ {
        toProcess <- i
    }
    close(toProcess)

    if pieces < workers {
        workers = pieces
    }

    wg := sync.WaitGroup{}
    wg.Add(workers)
    for i := 0; i < workers; i++ {
        // 启动多个goroutine
        go func() {
            defer utilruntime.HandleCrash()
            defer wg.Done()
            for piece := range toProcess {
                select {
                case <-stop:
                    return
                default:
                    //获取索引,后续会通过该索引来进行结果的存储
                    doWorkPiece(piece)
                }
            }
        }()
    }
    // 等待退出
    wg.Wait()
}

2.5 取样并行函数

        checkNode := func(i int) {
            // 获取一个节点
            nodeName := g.cache.NodeTree().Next()

            // 取样核心流程是通过podFitsOnNode来确定
            fits, failedPredicates, status, err := g.podFitsOnNode(
                pluginContext,
                pod,
                meta,
                g.nodeInfoSnapshot.NodeInfoMap[nodeName],
                g.predicates, // 传递预选算法
                g.schedulingQueue,
                g.alwaysCheckAllPredicates,
            )
            if err != nil {
                predicateResultLock.Lock()
                errs[err.Error()]++
                predicateResultLock.Unlock()
                return
            }
            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 {
                // 进行错误状态的保存
                predicateResultLock.Lock()
                if !status.IsSuccess() {
                    filteredNodesStatuses[nodeName] = status
                }
                if len(failedPredicates) != 0 {
                    failedPredicateMap[nodeName] = failedPredicates
                }
                predicateResultLock.Unlock()
            }
        }

2.6 面向未来的筛选


在kubernetes中经过调度器调度后的pod结果会放入到SchedulingQueue中进行暂存,这些pod未来可能会经过后续调度流程运行在提议的node上,也可能因为某些原因导致最终没有运行,而预选流程为了减少后续因为调度冲突(比如pod之间的亲和性等问题,并且当前pod不能抢占这些pod),则会在进行预选的时候,将这部分pod考虑进去

如果在这些pod存在的情况下,node可以满足当前pod的筛选条件,则可以去除被提议的pod再进行筛选(如果这些提议的pod最终没有调度到node,则当前node也需要满足各种亲和性的需求)

2.6 取样核心设计


结合上面说的面向未来的筛选,通过两轮筛选在无论那些优先级高的pod是否被调度到当前node上,都可以满足pod的调度需求,在调度的流程中只需要获取之前注册的调度算法,完成预选检测,如果发现有条件不通过则不会进行第二轮筛选,继续选择下一个节点

func (g *genericScheduler) podFitsOnNode(
    pluginContext *framework.PluginContext,
    pod *v1.Pod,
    meta predicates.PredicateMetadata,
    info *schedulernodeinfo.NodeInfo,
    predicateFuncs map[string]predicates.FitPredicate,
    queue internalqueue.SchedulingQueue,
    alwaysCheckAllPredicates bool,
) (bool, []predicates.PredicateFailureReason, *framework.Status, error) {
    var failedPredicates []predicates.PredicateFailureReason
    var status *framework.Status

    // podsAdded主要用于标识当前是否有提议的pod如果没有提议的pod则就不需要再进行一轮筛选了
    podsAdded := false

    for i := 0; i < 2; i++ {
        metaToUse := meta
        nodeInfoToUse := info
        if i == 0 {
            // 首先获取那些提议的pod进行第一轮筛选, 如果第一轮筛选出错,则不会进行第二轮筛选
            podsAdded, metaToUse, nodeInfoToUse = addNominatedPods(pod, meta, info, queue)
        } else if !podsAdded || len(failedPredicates) != 0 {
            // 如果
            break
        }
        for _, predicateKey := range predicates.Ordering() {
            var (
                fit     bool
                reasons []predicates.PredicateFailureReason
                err     error
            )
            //TODO (yastij) : compute average predicate restrictiveness to export it as Prometheus metric
            if predicate, exist := predicateFuncs[predicateKey]; exist {
                // 预选算法计算
                fit, reasons, err = predicate(pod, metaToUse, nodeInfoToUse)
                if err != nil {
                    return false, []predicates.PredicateFailureReason{}, nil, err
                }

                if !fit {
                    // eCache is available and valid, and predicates result is unfit, record the fail reasons
                    failedPredicates = append(failedPredicates, reasons...)
                    // if alwaysCheckAllPredicates is false, short circuit all predicates when one predicate fails.
                    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
                    }
                }
            }
        }

        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
}

微信号:baxiaoshi2020
关注公告号阅读更多源码分析文章
更多文章关注 www.sreguide.com
本文由博客一文多发平台 OpenWrite 发布

原文地址:https://www.cnblogs.com/buyicoding/p/12202575.html

时间: 2024-07-30 15:16:04

图解kubernetes调度器预选设计实现学习的相关文章

图解kubernetes调度器ScheduleAlgorithm核心实现学习框架设计

ScheduleAlgorithm是一个接口负责为pod选择一个合适的node节点,本节主要解析如何实现一个可扩展.可配置的通用算法框架来实现通用调度,如何进行算法的统一注册和构建,如何进行metadata和调度流程上下文数据的传递 1. 设计思考 1.1 调度设计 1.1.1 调度与抢占 当接收到pod需要被调度后,默认首先调用schedule来进行正常的业务调度尝试从当前集群中选择一个合适的node 如果调度失败则尝试抢占调度,根据优先级抢占低优先级的pod运行高优先级pod 1.1.2 调

# IT明星不是梦 #图解kubernetes调度器SchedulingQueue核心源码实现

chedulingQueue是kubernetes scheduler中负责进行等待调度pod存储的对,Scheduler通过SchedulingQueue来获取当前系统中等待调度的Pod,本文主要讨论SchedulingQueue的设计与实现的各种实现, 了解探究其内部实现与底层源码,本系列代码基于kubernets1.1.6分析而来 SchedulingQueue设计 队列与优先级 队列与场景 类型 描述 通常实现 队列 普通队列是一个FIFO的数据结构,根据元素入队的次序依次出队 数组或者

# IT明星不是梦 # kubernetes调度器学习基础概览

scheudler是kubernetes中的核心组件,负责为用户声明的pod资源选择合适的node,同时保证集群资源的最大化利用,这里先介绍下资源调度系统设计里面的一些基础概念 基础任务资源调度 基础的任务资源调度通常包括三部分: 角色类型 功能 node node负责具体任务的执行,同时对包汇报自己拥有的资源 resource manager 汇总当前集群中所有node提供的资源,供上层的scheduler的调用获取,同时根据node汇报的任务信息来进行当前集群资源的更新 scheduler

kubernetes调度器

在Kubernetes上,我们很少会直接创建一个Pod,在大多数情况下,会通过RC.Deployment.DaemonSet.Job等控制器完成对一组Pod副本的创建.调度和整个生命周期的自动化控制. 在早期的Kubernetes版本上,是没有这么多Pod副本控制器的,只有一个Pod副本控制器RC(Replication Controller).RC独立于所控制的Pod,并通过label标签关联控制目标Pod的创建和销毁等动作.随着Kubernetes的发展,RC也出现了新的继承者——Deplo

Kubernetes 第十七章 调度器

来自: https://www.jianshu.com/p/acb34a1d1b6e Kubenernetes 调度器介绍 Kubernetes 调度器介绍 kube-scheduler是 kubernetes 系统的核心组件之一,主要负责整个集群资源的调度功能,根据特定的调度算法和策略,将 Pod 调度到最优的工作节点上面去,从而更加合理.更加充分的利用集群的资源,这也是我们选择使用 kubernetes 一个非常重要的理由.如果一门新的技术不能帮助企业节约成本.提供效率,我相信是很难推进的.

kubernetes调度及调度器性能调优

kubernetes调度器在kubernetes中,调度指的是将新生成的pod调度到合适的Node节点上,然后Node上对应的kubelet才能运行pod. 1.调度概述调度器通过kubernetes的watch机制来发现新生成的且未调度到Node上的pod.调度器会将发现的每一个未调度的pod调度到合适的Node上运行,调度器会使用以下所述的调度原则来做出调度选择. 2.kube-schedulerkube-sceduler时kubernetes集群中默认调度器,并且是集群控制面的一部分,ku

Linux进程调度器的设计--Linux进程的管理与调度(十七)

日期 内核版本 架构 作者 GitHub CSDN 2016-06-14 Linux-4.6 X86 & arm gatieme LinuxDeviceDrivers Linux进程管理与调度 前景回顾 进程调度 内存中保存了对每个进程的唯一描述, 并通过若干结构与其他进程连接起来. 调度器面对的情形就是这样, 其任务是在程序之间共享CPU时间, 创造并行执行的错觉, 该任务分为两个不同的部分, 其中一个涉及调度策略, 另外一个涉及上下文切换. 内核必须提供一种方法, 在各个进程之间尽可能公平地

k8s的调度器架构和策略

调度器功能 默认调度器的主要职责,就是为一个新创建出来的Pod寻找一个最合适的节点(Node) 调度器对一个 Pod 调度成功,实际上就是将它的 spec.nodeName 字段填上调度结果的节点名字 预选节点 从集群所有的节点中,根据调度算法挑选出所有可以运行该 Pod 的节点默认调度器会首先调用一组叫作 Predicate 的调度算法,来检查每个Node 优选节点 从预选的结果中,再根据调度算法挑选一个最符合条件的节点作为最终结果.再调用一组叫作 Priority 的调度算法,来给上一步得到

linux调度器源码研究 - 概述(一)

本文为原创,转载请注明:http://www.cnblogs.com/tolimit/ 引言 调度器作为操作系统的核心部件,具有非常重要的意义,其随着linux内核的更新也不断进行着更新.本系列文章通过linux-3.18.3源码进行调度器的学习和分析,一步一步将linux现有的调度器原原本本的展现出来.此篇文章作为开篇,主要介绍调度器的原理及重要数据结构. 调度器介绍 随着时代的发展,linux也从其初始版本稳步发展到今天,从2.4的非抢占内核发展到今天的可抢占内核,调度器无论从代码结构还是设