Kubernetes 调度器

Kubernetes 是一个基于容器的分布式调度器,实现了自己的调度模块。
在Kubernetes集群中,调度器作为一个独立模块通过pod运行。从几个方面介绍Kubernetes调度器。

调度器工作方式

Kubernetes中的调度器,是作为单独组件运行,一般运行在Master中,和Master数量保持一致。通过Raft协议选出一个实例作为Leader工作,其他实例Backup。 当Master故障,其他实例之间继续通过Raft协议选出新的Master工作。
其工作模式如下:

  • 调度器内部维护一个调度的pods队列podQueue, 并监听APIServer。
  • 当我们创建Pod时,首先通过APIServer 往ETCD写入pod元数据。
  • 调度器通过Informer监听pods状态,当有新增pod时,将pod加入到podQueue中。
  • 调度器中的主进程,会不断的从podQueue取出的pod,并将pod进入调度分配节点环节
  • 调度环节分为两个步奏, Filter过滤满足条件的节点 、 Prioritize根据pod配置,例如资源使用率,亲和性等指标,给这些节点打分,最终选出分数最高的节点。
  • 分配节点成功, 调用apiServer的binding pod 接口, 将pod.Spec.NodeName设置为所分配的那个节点。
  • 节点上的kubelet同样监听ApiServer,如果发现有新的pod被调度到所在节点,调用本地的dockerDaemon 运行容器。
  • 假如调度器尝试调度Pod不成功,如果开启了优先级和抢占功能,会尝试做一次抢占,将节点中优先级较低的pod删掉,并将待调度的pod调度到节点上。 如果未开启,或者抢占失败,会记录日志,并将pod加入podQueue队尾。

实现细节

kube-scheduling 是一个独立运行的组件,主要工作内容在 Run 函数 。

这里面主要做几件事情:

  • 初始化一个Scheduler 实例 sched,传入各种Informer,为关心的资源变化建立监听并注册handler,例如维护podQuene
  • 注册events组件,设置日志
  • 注册http/https 监听,提供健康检查和metrics 请求
  • 运行主要的调度内容入口 sched.run() 。 如果设置 --leader-elect=true ,代表启动多个实例,通过Raft选主,实例只有当被选为master后运行主要工作函数sched.run

调度核心内容在 sched.run() 函数,它会启动一个go routine不断运行sched.scheduleOne, 每次运行代表一个调度周期。

func (sched *Scheduler) Run() {
if !sched.config.WaitForCacheSync() {
return
}
go wait.Until(sched.scheduleOne, 0, sched.config.StopEverything)
}

我们看下 sched.scheduleOne 主要做什么

func (sched *Scheduler) scheduleOne() {
pod := sched.config.NextPod()
.... // do some pre check
scheduleResult, err := sched.schedule(pod)
if err != nil {
if fitError, ok := err.(*core.FitError); ok {
if !util.PodPriorityEnabled() || sched.config.DisablePreemption {
..... // do some log
} else {
sched.preempt(pod, fitError)
}
}
}
...
// Assume volumes first before assuming the pod.
allBound, err := sched.assumeVolumes(assumedPod, scheduleResult.SuggestedHost)
...
fo func() {
// Bind volumes first before Pod
if !allBound {
err := sched.bindVolumes(assumedPod)
if err != nil {
klog.Errorf("error binding volumes: %v", err)
metrics.PodScheduleErrors.Inc()
return
}
}
err := sched.bind(assumedPod, &v1.Binding{
ObjectMeta: metav1.ObjectMeta{Namespace: assumedPod.Namespace, Name: assumedPod.Name, UID: assumedPod.UID},
Target: v1.ObjectReference{
Kind: "Node",
Name: scheduleResult.SuggestedHost,
},
})
}
}

sched.scheduleOne 中,主要会做几件事情

  • 通过sched.config.NextPod(), 从podQuene中取出pod
  • 运行sched.schedule,尝试进行一次调度。
  • 假如调度失败,如果开启了抢占功能,会调用sched.preempt 尝试进行抢占,驱逐一些pod,为被调度的pod预留空间,在下一次调度中生效。
  • 如果调度成功,执行bind接口。在执行bind之前会为pod volume中声明的的PVC 做provision。

sched.schedule 是主要的pod调度逻辑

func (g *genericScheduler) Schedule(pod *v1.Pod, nodeLister algorithm.NodeLister) (result ScheduleResult, err error) {
// Get node list
nodes, err := nodeLister.List()
// Filter
filteredNodes, failedPredicateMap, err := g.findNodesThatFit(pod, nodes)
if err != nil {
return result, err
}
// Priority
priorityList, err := PrioritizeNodes(pod, g.cachedNodeInfoMap, metaPrioritiesInterface, g.prioritizers, filteredNodes, g.extenders)
if err != nil {
return result, err
} // SelectHost
host, err := g.selectHost(priorityList)
return ScheduleResult{
SuggestedHost: host,
EvaluatedNodes: len(filteredNodes) + len(failedPredicateMap),
FeasibleNodes: len(filteredNodes),
}, err
}

调度主要分为三个步奏:

  • Filters: 过滤条件不满足的节点
  • PrioritizeNodes: 在条件满足的节点中做Scoring,获取一个最终打分列表priorityList
  • selectHost: 在priorityList中选取分数最高的一组节点,从中根据round-robin 方式选取一个节点。

接下来我们继续拆解, 分别看下这三个步奏会怎么做

Filters

Filters 相对比较容易,调度器默认注册了一系列的predicates方法, 调度过程为并发调用每个节点的predicates 方法。最终得到一个node list,包含符合条件的节点对象。

func (g *genericScheduler) findNodesThatFit(pod *v1.Pod, nodes []*v1.Node) ([]*v1.Node, FailedPredicateMap, error) {
if len(g.predicates) == 0 {
filtered = nodes
} else {
allNodes := int32(g.cache.NodeTree().NumNodes())
numNodesToFind := g.numFeasibleNodesToFind(allNodes) checkNode := func(i int) {
nodeName := g.cache.NodeTree().Next()
// 此处会调用这个节点的所有predicates 方法
fits, failedPredicates, err := podFitsOnNode(
pod,
meta,
g.cachedNodeInfoMap[nodeName],
g.predicates,
g.schedulingQueue,
g.alwaysCheckAllPredicates,
) if fits {
length := atomic.AddInt32(&filteredLen, 1)
if length > numNodesToFind {
// 如果当前符合条件的节点数已经足够,会停止计算。
cancel()
atomic.AddInt32(&filteredLen, -1)
} else {
filtered[length-1] = g.cachedNodeInfoMap[nodeName].Node()
}
}
}
// 并发调用checkNode 方法
workqueue.ParallelizeUntil(ctx, 16, int(allNodes), checkNode)
filtered = filtered[:filteredLen]
}
return filtered, failedPredicateMap, nil
}

值得注意的是, 1.13中引入了FeasibleNodes 机制,为了提高大规模集群的调度性能。允许我们通过bad-percentage-of-nodes-to-score 参数, 设置filter的计算比例(默认50%), 当节点数大于100个, 在 filters的过程,只要满足条件的节点数超过这个比例,就会停止filter过程,而不是计算全部节点。
举个例子,当节点数为1000, 我们设置的计算比例为30%,那么调度器认为filter过程只需要找到满足条件的300个节点,filter过程中当满足条件的节点数达到300个,filter过程结束。 这样filter不用计算全部的节点,同样也会降低Prioritize 的计算数量。 但是带来的影响是pod有可能没有被调度到最合适的节点。

Prioritize

Prioritize 的目的是帮助pod,为每个符合条件的节点打分,帮助pod找到最合适的节点。同样调度器默认注册了一系列Prioritize方法。这是Prioritize 对象的数据结构

// PriorityConfig is a config used for a priority function.
type PriorityConfig struct {
Name string
Map PriorityMapFunction
Reduce PriorityReduceFunction
// TODO: Remove it after migrating all functions to
// Map-Reduce pattern.
Function PriorityFunction
Weight int
}

每个PriorityConfig 代表一个评分的指标,会考虑服务的均衡性,节点的资源分配等因素。 一个 PriorityConfig 的主要Scoring过程分为 Map和Reduce,

  • Map 过程计算每个节点的分数值
  • Reduce 过程会将当前PriorityConfig的所有节点的打分结果再做一次处理。

所有PriorityConfig 计算完毕后,将每个PriorityConfig的数值乘以对应的权重,并按照节点再做一次聚合。

    workqueue.ParallelizeUntil(context.TODO(), 16, len(nodes), func(index int) {
nodeInfo := nodeNameToInfo[nodes[index].Name]
for i := range priorityConfigs {
var err error
results[i][index], err = priorityConfigs[i].Map(pod, meta, nodeInfo)
}
}) for i := range priorityConfigs {
wg.Add(1)
go func(index int) {
defer wg.Done()
if err := priorityConfigs[index].Reduce(pod, meta, nodeNameToInfo, results[index]);
}(i)
}
wg.Wait() // Summarize all scores.
result := make(schedulerapi.HostPriorityList, 0, len(nodes)) 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
}
}

此外Filter和Prioritize 都支持extener scheduler 的调用,本文不做过多阐述。

现状

目前kubernetes调度器的调度方式是Pod-by-Pod,也是当前调度器不足的地方。主要瓶颈如下

  • kubernets目前调度的方式,每个pod会对所有节点都计算一遍,当集群规模非常大,节点数很多时,pod的调度时间会非常慢。 这也是percentage-of-nodes-to-score 尝试要解决的问题
  • pod-by-pod的调度方式不适合一些机器学习场景。 kubernetes早期设计主要为在线任务服务,在一些离线任务场景,比如分布式机器学习中,我们需要一种新的算法gang scheduler,pod也许对调度的即时性要求没有那么高,但是提交任务后,只有当一个批量计算任务的所有workers都运行起来时,才会开始计算任务。 pod-by-pod 方式在这个场景下,当资源不足时非常容易引起资源死锁。
  • 当前调度器的扩展性不是十分好,特定场景的调度流程都需要通过硬编码实现在主流程中,比如我们看到的bindVolume部分, 同样也导致Gang Scheduler 无法在当前调度器框架下通过原生方式实现。

Kubernetes调度器的发展

社区调度器的发展,也是为了解决这些问题

接下来,我们会分析一个具体的调度器方法实现,帮助理解拆解调度器的过程。 并且关注分析调度器的社区动态。

参考

https://medium.com/jorgeacetozi/kubernetes-master-components-etcd-api-server-controller-manager-and-scheduler-3a0179fc8186
https://jvns.ca/blog/2017/07/27/how-does-the-kubernetes-scheduler-work/


本文作者:萧元

原文链接

本文为云栖社区原创内容,未经允许不得转载。

Kubernetes 调度器实现初探的更多相关文章

  1. kubernetes 调度器

    调度器 kube-scheduler 是 kubernetes 的核心组件之一,主要负责整个集群资源的调度功能,根据特定的调度算法和策略,将 Pod 调度到最优的工作节点上面去,从而更加合理.更加充分 ...

  2. 第十五章 Kubernetes调度器

    一.简介 Scheduler 是 kubernetes 的调度器,主要的任务是把定义的 pod 分配到集群的节点上.听起来非常简单,但有很多要考虑的问题: ① 公平:如何保证每个节点都能被分配资源 ② ...

  3. 图解kubernetes调度器SchedulingQueue核心源码实现

    SchedulingQueue是kubernetes scheduler中负责进行等待调度pod存储的对,Scheduler通过SchedulingQueue来获取当前系统中等待调度的Pod,本文主要 ...

  4. 图解kubernetes调度器SchedulerCache核心源码实现

    SchedulerCache是kubernetes scheduler中负责本地数据缓存的核心数据结构, 其实现了Cache接口,负责存储从apiserver获取的数据,提供给Scheduler调度器 ...

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

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

  6. 图解kubernetes调度器抢占流程与算法设计

    抢占调度是分布式调度中一种常见的设计,其核心目标是当不能为高优先级的任务分配资源的时候,会通过抢占低优先级的任务来进行高优先级的调度,本文主要学习k8s的抢占调度以及里面的一些有趣的算法 1. 抢占调 ...

  7. 图解kubernetes调度器SchedulerExtender扩展

    在kubernetes的scheduler调度器的设计中为用户预留了两种扩展机制SchdulerExtender与Framework,本文主要浅谈一下SchdulerExtender的实现, 因为还有 ...

  8. 巧用Prometheus来扩展kubernetes调度器

    Overview 本文将深入讲解 如何扩展 Kubernetes scheduler 中各个扩展点如何使用,与扩展scheduler的原理,这些是作为扩展 scheduler 的所需的知识点.最后会完 ...

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

    ScheduleAlgorithm是一个接口负责为pod选择一个合适的node节点,本节主要解析如何实现一个可扩展.可配置的通用算法框架来实现通用调度,如何进行算法的统一注册和构建,如何进行metad ...

随机推荐

  1. Create STKNetDiskC Instance Error

    关于Create STKNetDiskC Instance Error错误的解决方法 这个错误可能出现在: AM8 附件直接存网盘的时候 报错 解决方法步骤如下: 在出现该错误的机器上,先将AM及 m ...

  2. [转]在C#代码中应用Log4Net系列教程(附源代码)

    Log4Net应该可以说是DotNet中最流行的开源日志组件了.以前需要苦逼写的日志类,在Log4Net中简单地配置一下就搞定了.没用过Log4Net,真心不知道原来日志组件也可以做得这么灵活,当然这 ...

  3. mac配置ls命令显示不同文件不同颜色

    使用Mac看到Linux的Ubuntu终端显示的颜色是不是觉得很酷炫,是否很想自己也拥有一样变色技巧?不怕,我们也是可以的! . 打开配置文件 sudo vim ~/.bash_profile 写入以 ...

  4. spark dataframe 将null 改为 nan

    由于我要叠加rdd某列的数据,如果加数中出现nan,结果也需要是nan,nan可以做到,但我要处理的数据源中的nan是以null的形式出现的,null不能叠加,而且我也不能删掉含null的行,于是我用 ...

  5. c语言学习笔记 关于double

    今天做了个简单的例子,由于没有使用正确的数据类型导致出错,下面是记录 #include <stdio.h> int main(void){ int i; double sum; doubl ...

  6. DFS-深度优先搜索与BFS-广度优先搜索

    1.DFS DFS是一个递归过程.(类似于二叉树的前序遍历) 参考:深度优先搜索(Depth-First-Search)精髓 2.BFS 可以理解为按层遍历,借助队列结构来实现.(类似于二叉树的层次遍 ...

  7. PAT甲级——A1049 Counting Ones

    The task is simple: given any positive integer N, you are supposed to count the total number of 1's ...

  8. Leetcode151. Reverse Words in a String翻转字符串里的单词

    给定一个字符串,逐个翻转字符串中的每个单词. 示例: 输入: "the sky is blue", 输出: "blue is sky the". 说明: 无空格 ...

  9. 只要三步!阿里云DLA帮你处理海量JSON数据

    概述 您可能有大量应用程序产生的JSON数据,您可能需要对这些JSON数据进行整理,去除不想要的字段,或者只保留想要的字段,或者仅仅是进行数据查询. 那么,利用阿里云Data Lake Analyti ...

  10. TZ_13_Hystix的服务降级_线程隔离

    1.微服务中,服务之间的调用关系复杂. 一个请求有可能需要多个微服务接口才能实现.如果一次请求出现问题就会直接堵塞,占用一次tomcat链接.如果访问这个出现问题的请求就会造成tomcat请求链接都被 ...