Kubernetes之Scheduler源码分析1.8.3

K8S的scheduler的主要作用是将用户申请的pods调度到合适的node节点上。具体的来说,就是它通过监听API server提供的watch等接口,获取到未调度的pods和node的相关信息,通过对node的筛选,选择出最合适的也就是优先级最高的node节点,将其与pods进行绑定,并将绑定的结果固化到etcd中去。下面就通过走读源码来详细解析一下scheduler的流程:

scheduler的脑图:

 

源码解析:

 

main函数的入口:

 

func main() {
    //新建schedulerserver,它主要是读取master地址和kubeconf文件
    s := options.NewSchedulerServer()
 
    .......
    //启动参数结构体新建以后,开始执行主函数RUN
    if err := app.Run(s); err != nil {
        glog.Fatalf("scheduler app failed to run: %v", err)
    }

 

 

Run函数的入口(Run函数一单启动起来之后就不会在退出,一直以一个进程的身份运行在后台,当检测到有pods未进行调度的时候直接启动):

func Run(s *options.SchedulerServer) error {
 
    ........
    //新建scheduler它主要是分为两步:1.创建Scheduler需求的各种结构体,如监听器,node,pods的信息等 2.根据获取的信息新建一个scheduler对象,利用这个对象来开启调度。
    sched, err := CreateScheduler(
        s,
        kubecli,
        informerFactory.Core().V1().Nodes(),
        podInformer,
        informerFactory.Core().V1().PersistentVolumes(),
        informerFactory.Core().V1().PersistentVolumeClaims(),
        informerFactory.Core().V1().ReplicationControllers(),
        informerFactory.Extensions().V1beta1().ReplicaSets(),
        informerFactory.Apps().V1beta1().StatefulSets(),
        informerFactory.Core().V1().Services(),
        recorder,
    )
 
    ........
    //根据上面创建的scheduler的对象来调用Run函数来启动调度过程。
    run := func(stopCh <-chan struct{}) {
        sched.Run()
        <-stopCh
    }
 
    ........
}


创建scheduler对象:

 

 

 

func CreateScheduler(...../*传递进来的参数*/.....) (*scheduler.Scheduler, error) {
     
   //调用NewConfigFactory()函数来构建各种结构体
   configurator := factory.NewConfigFactory(
        s.SchedulerName,
        kubecli,
        nodeInformer,
        podInformer,
        pvInformer,
        pvcInformer,
        replicationControllerInformer,
        replicaSetInformer,
        statefulSetInformer,
        serviceInformer,
        s.HardPodAffinitySymmetricWeight,
        utilfeature.DefaultFeatureGate.Enabled(features.EnableEquivalenceClassCache),
    )
 
    .......
 
    //根据新建的scheduler结构体创建scheduler对象
    return scheduler.NewFromConfigurator(configurator, func(cfg *scheduler.Config) {
        cfg.Recorder = recorder
    })
}


scheduler各种结构体的构建:

 

 

 

 

func NewConfigFactory(...../*传递进来的参数*/.....) scheduler.Configurator {
 
 
    ......
    //所有的pods和node的信息都会先记录在schedulerCache中,这样做的目的是,可以一次性的先获取所有的pods和node的信息不用再反复的从etcd中读取,这样会节约很多的时间提高一定的效率,这个New()方法是用来将已经绑定好的pods和node信息固化到etcd中的,它是一个多线程的操作,30s启动一次,默认最多可以起16个线程同时去处理这件事情。
    schedulerCache := schedulercache.New(30*time.Second, stopEverything)
 
    c := &ConfigFactory{
        client:                         client,
        podLister:                      schedulerCache,
        podQueue:                       cache.NewFIFO(cache.MetaNamespaceKeyFunc),
        
        //注意这些lister,他们用来获取各种资源列表,它们会和 apiserver 保持实时同步
        nodeLister                      nodeInformer.Lister(),
        pVLister:                       pvInformer.Lister(),
        pVCLister:                      pvcInformer.Lister(),
        serviceLister:                  serviceInformer.Lister(),
        controllerLister:               replicationControllerInformer.Lister(),
        replicaSetLister:               replicaSetInformer.Lister(),
        statefulSetLister:              statefulSetInformer.Lister(),
  
 
        schedulerCache:                 schedulerCache,
        StopEverything:                 stopEverything,
        schedulerName:                  schedulerName,
        hardPodAffinitySymmetricWeight: hardPodAffinitySymmetricWeight,
        enableEquivalenceClassCache:    enableEquivalenceClassCache,
    }
 
     
    //获取已经调度的pods的信息
    podInformer.Informer().AddEventHandler(
        cache.FilteringResourceEventHandler{
            FilterFunc: func(obj interface{}) bool {
                switch t := obj.(type) {
                case *v1.Pod:
                    return assignedNonTerminatedPod(t)
                default:
                    runtime.HandleError(fmt.Errorf("unable to handle object in %T: %T", c, obj))
                    return false
                }
            },
            Handler: cache.ResourceEventHandlerFuncs{
                AddFunc:    c.addPodToCache,
                UpdateFunc: c.updatePodInCache,
                DeleteFunc: c.deletePodFromCache,
            },
        },
    )
    // 获取还未调度的pods的信息并将其放入调度的队列中,并定义其增删改的方法函数
    podInformer.Informer().AddEventHandler(
        cache.FilteringResourceEventHandler{
            FilterFunc: func(obj interface{}) bool {
                switch t := obj.(type) {
                case *v1.Pod:
                    return unassignedNonTerminatedPod(t)
                default:
                    runtime.HandleError(fmt.Errorf("unable to handle object in %T: %T", c, obj))
                    return false
                }
            },
            Handler: cache.ResourceEventHandlerFuncs{
                AddFunc: func(obj interface{}) {
                    if err := c.podQueue.Add(obj); err != nil {
                        runtime.HandleError(fmt.Errorf("unable to queue %T: %v", obj, err))
                    }
                },
                UpdateFunc: func(oldObj, newObj interface{}) {
                    if err := c.podQueue.Update(newObj); err != nil {
                        runtime.HandleError(fmt.Errorf("unable to update %T: %v", newObj, err))
                    }
                },
                DeleteFunc: func(obj interface{}) {
                    if err := c.podQueue.Delete(obj); err != nil {
                        runtime.HandleError(fmt.Errorf("unable to dequeue %T: %v", obj, err))
                    }
                },
            },
        },
    )
    c.scheduledPodLister = assignedPodLister{podInformer.Lister()}
 
    // 获取node的信息
    nodeInformer.Informer().AddEventHandlerWithResyncPeriod(
        cache.ResourceEventHandlerFuncs{
            AddFunc:    c.addNodeToCache,
            UpdateFunc: c.updateNodeInCache,
            DeleteFunc: c.deleteNodeFromCache,
        },
        0,
    )
    c.nodeLister = nodeInformer.Lister()
 
    //获取所监听的所有信息
    ..........
 
    return c
}


scheduler的对象的Run方法:

 

 

 

 

func (sched *Scheduler) Run() {
    if !sched.config.WaitForCacheSync() {
        return
    }
 
    //很明显是启动了一个goroutine一直循环调度schedulerOne这个方法
    go wait.Until(sched.scheduleOne, 0, sched.config.StopEverything)
}


schedulerOne方法函数:

 

 

 

 

func (sched *Scheduler) scheduleOne() {
    //调用NextPod()方法,来获取下一个还未调度的pods的信息
    pod := sched.config.NextPod()
    //判断pods是否已经被删除,如果已经被删除,那么直接返回
    if pod.DeletionTimestamp != nil {
        sched.config.Recorder.Eventf(pod, v1.EventTypeWarning, "FailedScheduling", "skip schedule deleting pod: %v/%v", pod.Namespace, pod.Name)
        glog.V(3).Infof("Skip schedule deleting pod: %v/%v", pod.Namespace, pod.Name)
        return
    }
 
    glog.V(3).Infof("Attempting to schedule pod: %v/%v", pod.Namespace, pod.Name)
 
    start := time.Now()
    //调用schedule这个方法,来获取合适的node
    suggestedHost, err := sched.schedule(pod)
    metrics.SchedulingAlgorithmLatency.Observe(metrics.SinceInMicroseconds(start))
    if err != nil {
        //这里是调度失败的结果,如果调度失败了,那么他就会在一定的时间内将这个pods在此加载回调度的pods队列中。
        if fitError, ok := err.(*core.FitError); ok {
            sched.preempt(pod, fitError)
        }
        return
    }
  
    //在选择出来合适的node节点的时候,先将这个node 的资源分配给它,但是暂时并不将其绑定,固化到etcd中,而是先将其存起来,但是这里注意,
    //node的资源已经假设分给pods了,node的资源信息就发生了变化,这是为了可以多线程的往etcd中固化信息,可以提高效率。
    assumedPod := *pod
    err = sched.assume(&assumedPod, suggestedHost)
    if err != nil {
        return
    }
 
    // 下面就是多线程的绑定信息并固化到etcd 中去,如果绑定失败的话,那么就会返回绑定失败的结果,并且调用ForgetPod()方法
    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,
            },
        })
        metrics.E2eSchedulingLatency.Observe(metrics.SinceInMicroseconds(start))
        if err != nil {
            glog.Errorf("Internal error binding pod: (%v)", err)
        }
    }()
}


调用的scheduler方法:

 

 

 

 

func (g *genericScheduler) Schedule(pod *v1.Pod, nodeLister algorithm.NodeLister) (string, error) {
 
 
    ......
  
    //获取node的列表
    nodes, err := nodeLister.List()
    if err != nil {
        return "", err
    }
    if len(nodes) == 0 {
        return "", ErrNoNodesAvailable
    }
 
     
    ........
 
    //先对node节点进行过滤,选择出来资源够的node节点,这里调用的方法就是findNodesThatFit方法
    filteredNodes, failedPredicateMap, err := findNodesThatFit(pod, g.cachedNodeInfoMap, nodes, g.predicates, g.extenders, g.predicateMetaProducer, g.equivalenceCache)
    if err != nil {
        return "", err
    }
 
    if len(filteredNodes) == 0 {
        return "", &FitError{
            Pod:              pod,
            FailedPredicates: failedPredicateMap,
        }
    }
 
    //在选择出来可以调度的node节点的时候,然后对选择出来的node节点进行优先级打分
    metaPrioritiesInterface := g.priorityMetaProducer(pod, g.cachedNodeInfoMap)
    priorityList, err := PrioritizeNodes(pod, g.cachedNodeInfoMap, metaPrioritiesInterface, g.prioritizers, filteredNodes, g.extenders)
    if err != nil {
        return "", err
    }
 
    //选择出来最合适的node节点
    return g.selectHost(priorityList)
}

 

 

 

如果pod.yaml文件中写的scheduler name是default的话,那么就会直接使用默认的调度策略。

由上面的源代码可以看出,他在选择node的时候采用的是分三步的策略:1.过滤出可以调度的节点 2. 优先级的判断 3.根据每个节点的优先级的大小选择出来一个最合适的节点。

下面就介绍一下默认的调度策略:

findNodeThatFit()方法:该方法主要是对node节点进行过滤的。

 

func findNodesThatFit(
    pod *v1.Pod,
    nodeNameToInfo map[string]*schedulercache.NodeInfo,
    nodes []*v1.Node,
    predicateFuncs map[string]algorithm.FitPredicate,
    extenders []algorithm.SchedulerExtender,
    metadataProducer algorithm.PredicateMetadataProducer,
    ecache *EquivalenceCache,
) ([]*v1.Node, FailedPredicateMap, error) {
    var filtered []*v1.Node
    failedPredicateMap := FailedPredicateMap{}
 
    if len(predicateFuncs) == 0 {
        filtered = nodes
    } else {
        filtered = make([]*v1.Node, len(nodes))
        errs := errors.MessageCountMap{}
        var predicateResultLock sync.Mutex
        var filteredLen int32
 
        // 这里利用metadataProducer函数来获取pods和node的信息
        meta := metadataProducer(pod, nodeNameToInfo)
        checkNode := func(i int) {
            nodeName := nodes[i].Name
            //调用podFitsOnNode方法来过滤node节点
            fits, failedPredicates, err := podFitsOnNode(pod, meta, nodeNameToInfo[nodeName], predicateFuncs, ecache)
            if err != nil {
                predicateResultLock.Lock()
                errs[err.Error()]++
                predicateResultLock.Unlock()
                return
            }
            if fits {
                filtered[atomic.AddInt32(&filteredLen, 1)-1] = nodes[i]
            } else {
                predicateResultLock.Lock()
                failedPredicateMap[nodeName] = failedPredicates
                predicateResultLock.Unlock()
            }
        }
        //为了加速,这里起了16个线程,来并行进行node节点的判断。
        workqueue.Parallelize(16, len(nodes), checkNode)
        filtered = filtered[:filteredLen]
 
        ..........
 
    return filtered, failedPredicateMap, nil
}

 

过滤的方法主要有:

  1. MaxEBSVolumeCount:请求的 volumes 是否超过 EBS(Elastic Block Store) 支持的最大值,默认是 39
  2. MatchInterPodAffinity:根据 inter-pod affinity 来决定 pod 是否能调度到节点上。这个过滤方法会看 pod 是否和当前节点的某个 pod 互斥。关于亲和性和互斥性
  3. NoDiskConflict:检查 pod 请求的 volume 是否就绪和冲突。如果主机上已经挂载了某个卷,则使用相同卷的 pod 不能调度到这个主机上。kubernetes 使用的 volume 类型不同,过滤逻辑也不同。比如不同云主机的 volume 使用限制不同:GCE 允许多个 pods 使用同时使用 volume,前提是它们是只读的;AWS 不允许 pods 使用同一个 volume;Ceph RBD 不允许 pods 共享同一个 monitor
  4. GeneralPredicates:普通过滤函数,主要考虑 kubernetes 资源是否能够满足,比如 CPU 和 Memory 是否足够,端口是否冲突、selector 是否匹配
    • PodFitsResources:检查主机上的资源是否满足 pod 的需求。资源的计算是根据主机上运行 pod 请求的资源作为参考的,而不是以实际运行的资源数量
    • PodFitsHost:如果 pod 指定了 spec.NodeName,看节点的名字是否何它匹配,只有匹配的节点才能运行 pod
    • PodFitsHostPorts:检查 pod 申请的主机端口是否已经被其他 pod 占用,如果是,则不能调度
    • PodSelectorMatches:检查主机的标签是否满足 pod 的 selector。包括 NodeAffinity 和 nodeSelector 中定义的标签。
  5. CheckNodeMemoryPressure:检查 pod 能否调度到内存有压力的节点上。如有节点有内存压力,那么智能调度内存标记为0的情况。
  6. CheckNodeDiskPressure:检查 pod 能否调度到磁盘有压力的节点上,目前所有的 pod 都不能调度到磁盘有压力的节点上

下面是资源判断的主要方法PodFitsResources():

func PodFitsResources(pod *v1.Pod, meta algorithm.PredicateMetadata, nodeInfo *schedulercache.NodeInfo) (bool, []algorithm.PredicateFailureReason, error) {
    node := nodeInfo.Node()
    if node == nil {
        return false, nil, fmt.Errorf("node not found")
    }
 
    var predicateFails []algorithm.PredicateFailureReason
  
    //判断node节点上的pods个数是否已经超出了允许分配的个数
    allowedPodNumber := nodeInfo.AllowedPodNumber()
    if len(nodeInfo.Pods())+1 > allowedPodNumber {
        predicateFails = append(predicateFails, NewInsufficientResourceError(v1.ResourcePods, 1, int64(len(nodeInfo.Pods())), int64(allowedPodNumber)))
    }
 
    //获取pods的需求的资源
    var podRequest *schedulercache.Resource
    if predicateMeta, ok := meta.(*predicateMetadata); ok {
        podRequest = predicateMeta.podRequest
    } else {
        // We couldn't parse metadata - fallback to computing it.
        podRequest = GetResourceRequest(pod)
    }
    if podRequest.MilliCPU == 0 &&
        podRequest.Memory == 0 &&
        podRequest.NvidiaGPU == 0 &&
        podRequest.EphemeralStorage == 0 &&
        len(podRequest.ExtendedResources) == 0 &&
        len(podRequest.HugePages) == 0 {
        return len(predicateFails) == 0, predicateFails, nil
    }
 
    //对四个方面的进行判断,内存、cpu、Gpu、磁盘空间,这里就是简单的比较大小。
    allocatable := nodeInfo.AllocatableResource()
    if allocatable.MilliCPU < podRequest.MilliCPU+nodeInfo.RequestedResource().MilliCPU {
        predicateFails = append(predicateFails, NewInsufficientResourceError(v1.ResourceCPU, podRequest.MilliCPU, nodeInfo.RequestedResource().MilliCPU, allocatable.MilliCPU))
    }
    if allocatable.Memory < podRequest.Memory+nodeInfo.RequestedResource().Memory {
        predicateFails = append(predicateFails, NewInsufficientResourceError(v1.ResourceMemory, podRequest.Memory, nodeInfo.RequestedResource().Memory, allocatable.Memory))
    }
    if allocatable.NvidiaGPU < podRequest.NvidiaGPU+nodeInfo.RequestedResource().NvidiaGPU {
        predicateFails = append(predicateFails, NewInsufficientResourceError(v1.ResourceNvidiaGPU, podRequest.NvidiaGPU, nodeInfo.RequestedResource().NvidiaGPU, allocatable.NvidiaGPU))
    }
 
    if allocatable.EphemeralStorage < podRequest.EphemeralStorage+nodeInfo.RequestedResource().EphemeralStorage {
        predicateFails = append(predicateFails, NewInsufficientResourceError(v1.ResourceEphemeralStorage, podRequest.EphemeralStorage, nodeInfo.RequestedResource().EphemeralStorage, allocatable.EphemeralStorage))
    }
 
    ..........
 
    return len(predicateFails) == 0, predicateFails, nil
}

 

在过滤node节点之后,下面就是对node节点的优先级进行打分:

 

 

 

 

func PrioritizeNodes(
    pod *v1.Pod,
    nodeNameToInfo map[string]*schedulercache.NodeInfo,
    meta interface{},
    priorityConfigs []algorithm.PriorityConfig,
    nodes []*v1.Node,
    extenders []algorithm.SchedulerExtender,
) (schedulerapi.HostPriorityList, error) {
 
    //如果没有选择优先级判断这一项,那么所有的节点的优先级是一样的,那就随机选择一个节点
    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
    }
 
    ..........
 
    //建立一个二维数组来记录每个优先级算法对节点的打分情况,分值为0-10,node节点的最终分值是所有优先级算法分值的相加
    results := make([]schedulerapi.HostPriorityList, len(priorityConfigs), len(priorityConfigs))
 
    for i, priorityConfig := range priorityConfigs {
        if priorityConfig.Function != nil {
            // DEPRECATED
            wg.Add(1)
            go func(index int, config algorithm.PriorityConfig) {
                defer wg.Done()
                var err error
                results[index], err = config.Function(pod, nodeNameToInfo, nodes)
                if err != nil {
                    appendError(err)
                }
            }(i, priorityConfig)
        } else {
            results[i] = make(schedulerapi.HostPriorityList, len(nodes))
        }
    }
    processNode := func(index int) {
        nodeInfo := nodeNameToInfo[nodes[index].Name]
        var err error
         
        //写一个方法,从priorityConfigs中遍历其中的优先级算法,并打分
        for i := range priorityConfigs {
            if priorityConfigs[i].Function != nil {
                continue
            }
            results[i][index], err = priorityConfigs[i].Map(pod, meta, nodeInfo)
            if err != nil {
                appendError(err)
                return
            }
        }
    }
  
    //启动多线程来并发的执行这个操作
    workqueue.Parallelize(16, len(nodes), processNode)
 
    .........
 
        // 等待打分完之后,进行分数的汇总操作
        wg.Wait()
        for i := range result {
            result[i].Score += combinedScores[result[i].Host]
        }
 
    if glog.V(10) {
        for i := range result {
            glog.V(10).Infof("Host %s => Score %d", result[i].Host, result[i].Score)
        }
    }
    return result, nil
}

 

 

 

几个关键的打分算法:

  • LeastRequestedPriority:最低请求优先级。根据 CPU 和内存的使用率来决定优先级,使用率越低优先级越高,也就是说优先调度到资源利用率低的节点,这个优先级函数能起到把负载尽量平均分到集群的节点上。默认权重为 1
  • BalancedResourceAllocation:资源平衡分配。这个优先级函数会把 pod 分配到 CPU 和 memory 利用率差不多的节点(计算的时候会考虑当前 pod 一旦分配到节点的情况)。默认权重为 1
  • SelectorSpreadPriority:尽量把同一个 service、replication controller、replica set 的 pod 分配到不同的节点,这些资源都是通过 selector 来选择 pod 的,所以名字才是这样的。默认权重为 1
  • CalculateAntiAffinityPriority:尽量把同一个 service 下面某个 label 相同的 pod 分配到不同的节点
  • ImageLocalityPriority:根据镜像是否已经存在的节点上来决定优先级,节点上存在要使用的镜像,而且镜像越大,优先级越高。这个函数会尽量把 pod 分配到下载镜像花销最少的节点
  • InterPodAffinityPriority:根据 pod 之间的亲和性决定 node 的优先级,默认权重为 1

 

以上分析如有不对的地方,还请各位大神指正,谢谢

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值