关于kubernetes:kubescheduler深度剖析与开发一

7次阅读

共计 6286 个字符,预计需要花费 16 分钟才能阅读完成。

kube-scheduler 作为 k8s 的调度器,就好比人的大脑,将口头指定传递到手脚等器官,进而执行对应的动作,对于 kube-scheduler 则是将 Pod 调配(调度)到集群内的各个节点,进而创立容器运行过程,对于 k8s 来说至关重要。

为了深刻学习 kube-scheduler,本系从源码和实战角度深度学 习 kube-scheduler,该系列一共分 6 篇文章,如下:

  • kube-scheduler 整体架构
  • 初始化一个 scheduler
  • 一个 Pod 是如何被调度的
  • 如何开发一个属于本人的 scheduler 插件
  • 开发一个 prefilter 扩大点的插件
  • 开发一个 socre 扩大点的插件

本篇先相熟 kube-scheduler 的整体架构设计,看清全局,做到心里有数,在前面的篇章再庖丁解牛,一步步开掘细节。


咱们先看看官网是怎么形容 scheduler 的

The Kubernetes scheduler is a control plane process which assigns Pods to Nodes. The scheduler determines which Nodes are valid placements for each Pod in the scheduling queue according to constraints and available resources. The scheduler then ranks each valid Node and binds the Pod to a suitable Node. Multiple different schedulers may be used within a cluster; kube-scheduler is the reference implementation.

k8s scheduler 是一个管制面过程,它调配 Pod 到 Nodes。依据限度和可用资源,scheduler 确定哪些节点合乎调度队列里的 Pod。而后对这些合乎的节点进行打分,而后把 Pod 绑定到适合的节点上。一个集群内能够存在多个 scheduler,而 kube-scheduler 是一个参考实现。

这段话简略概括下就是:当有 pod 须要 scheduler 调度的时候,scheduler 会依据一些列规定挑选出最合乎的节点,而后将 Pod 绑定到这个 Node。

所以 scheduler 次要要做的事就是依据 Nodes 以后状态和 pod 对资源的需要,依照程序运行一系列指定的算法来挑选出一个 Node。

咱们能够通过下图,对上述说的列算法有一个初步的意识,前面咱们在开展具体说

如图中所示,图中每一个绿色箭头在 k8s 中叫 扩大点(extension point),从图中能够看到一共有 10 个扩大点,咱们能够分个类,如下图

<img src=”https://pic.peo.pw/a/2023/04/13/64381c7704b2b.png” width = “360” height = “300” alt=” 图片名称 ” align=center />

每一个扩大点能够运行一个或多个算法,在 k8s 中把这种算法叫做 插件(Plugin)。顾名思义,扩大点就是能够扩大的,所以用户能够开发本人的插件嵌入扩大点中,咱们既能够将本人开发的插件和零碎默认插件同时运行,也能够关闭系统自带的插件只运行本人的插件,这部分在前面开发实际阶段会具体介绍。

上面咱们介绍下各个类型扩大点

  • sort

sort 类型的扩大点只有一个:sort,而且这个扩大点上面只能有一个插件能够运行,如果同时 enable 多个 sort 插件,scheduler 会退出。

在 k8s 中,待调度的 Pod 会放在一个叫 activeQ 队列中,这个队列是一个基于堆实现的优先队列(priority queue),为什么是优先队列呢?

因为你能够对 Pod 设置优先级,将你认为须要优先调度的 Pod 优先级调大,如果队列里有多个 Pod 须要调度,就会呈现抢占景象,优先级高的 Pod 会挪动到队列头部,scheduler 会优先取出这个 Pod 进行调度。那么这个优先级怎么设置呢?有两种办法:

  1. 如应用 k8s 默认 sort 插件,则能够给 Pod (deployment 等形式) 设置 PriorityClass(创立 PriorityClass 资源并配置 deployment);如果你的所有 Pod 都没有设置 PriorityClass,那么会依据 Pod 创立的工夫先后顺序进行调度。PriorityClass 和 Pod 创立工夫是零碎默认的排序根据。
  2. 实现本人的 sort 插件定制排序算法,依据该排序算法实现抢占,例如你能够将蕴含特定标签的 Pod 移到队头。前面会具体讲述如何实现本人的插件来扭转零碎默认行为。
  • filter

filter 类型扩大点有 3 个:prefilter,filter,postfilter。各个扩大点有多个插件组成的插件汇合依据 Pod 的配置独特过滤 Node,如下图:

<img src=”https://pic.peo.pw/a/2023/04/13/64378cdb1d523.png” width = “500” height = “700” alt=” 图片名称 ” align=center />

preFilter 扩大点次要有两个作用,一是为前面的扩大点计算 Pod 的一些信息,例如 preFilter 阶段的 NodeResourcesFit 算法不会去判断节点适合与否,而是计算这个 Pod 须要多少资源,而后存储这个信息,在 filter 扩大点的 NodeResourcesFit 插件中会把之前算进去的资源拿进去做判断;另外一个作用就是过滤一些显著不符合要求的节点,这样能够缩小后续扩大点插件一些无意义的计算。

filter 扩大点次要的作用就是依据各个插件定义的程序顺次执行,筛选出合乎 Pod 的节点,这些插件会在 preFilter 后留下的每个 Node 上运行,如果可能通过所有插件的”考验“,那么这个节点就留下来了。如果某个插件判断这个节点不合乎,那么残余的所有插件都不会对该节点做计算。

postFilter 扩大点只会在 filter 完结后没有任何 Node 合乎 Pod 的状况下才会运行,否则这个扩大点会被跳过。咱们能够看到,这个扩大点在零碎只有一个默认的插件,
这个默认插件的作用遍历这个 Pod 所在的命名空间上面的所有 Pod,查找是否有能够被抢占的 Pod,如果有的话选出一个最合适的 Pod 而后 delete 掉这个 Pod,并在待调度的 Pod 的 status 字段上面配置 nominateNode 为这个被抢占的 Pod。

  • score
    这个类型的扩大点的作用就是为下面 filter 扩大点筛选进去的所有 Node 进行打分,挑选出一个得分最高(最合适的),这个 Node 就是 Pod 要被调度下来的节点。这个这个类型的扩大有 preScore 和 score 两个,前者是为后者打分做前置筹备的,preScore 的各个插件会计算一些信息供 score 应用,这个和 prefilter 比拟相似。
  • reserve
    reserve 类型扩大点零碎默认只实现了一个插件:VolumeBinding,更新 Pod 申明的 PVC 和对应的 PV 缓存信息,示意该 PV 曾经被 Pod 占用。
  • permit

该类型扩大点,零碎没有实现默认的插件,咱们就不说了

  • bind

该类型扩大点有三个扩大点:preBind、bind 和 postBind。

preBind 扩大点有一个内置插件 VolumeBinding,这个插件会调用 pv controller 实现绑定操作,在后面的 reserve 也有同名插件,这个插件只是更新了本地缓存中的信息,没有理论做绑定。

bind 扩大点也只有一个默认的内置插件:DefaultBinder,这个插件只做了一件很简略的事,将 Pod.Spec.nodeName 更新为选出来的那个 node。前面的“故事”就是 kubelet 监听到了 nodeName=Kubelet 所在 nodename,而后开始创立 Pod(容器)。到了这里,整个调度流程就完结了。

从文章结尾的那张图中咱们可能看到 scheduler 分两个 cycle:scheduling cycle 和 binding cycle。辨别这两个 cycle 的起因是为了晋升调度效率。从下面的形容中咱们可能看到,在 bind cycle 中,会有两次内部 api 调用:调用 pv controller 绑定 pv 和调用 kube-apiserver 绑定 Node,api 调用是耗时的,所以将 bind 扩大点拆分进去,另起一个 go 协程进行 bind。而在 scheduling cycle 中为了晋升效率的一个重要准则就是 Pod、Node 等信息从本地缓存中获取,而具体的实现原理就是先应用 list 获取所有 Node、Pod 的信息,而后再 watch 他们的变动更新本地缓存。

下面咱们次要从 扩大点和插件 方面阐明了 scheduler 的架构。上面咱们从源码架构说说 scheduler 是怎么工作。

下图是 kube-scheduler 代码的次要框架

咱们先来看看 kube-scheduler 中的几个要害组件

  • schedulerCache

schedulerCache 缓存 Pod,Node 等信息,各个扩大点的插件在计算时所须要的 Node 和 Pod 信息都是从 schedulerCache 获取。schedulerCache 具体在外部是一个实现了 Cache 接口的 构造体 cacheImpl,咱们看下这个构造体:

type cacheImpl struct {stop   <-chan struct{}
    ttl    time.Duration
    period time.Duration

    // This mutex guards all fields within this cache struct.
    mu sync.RWMutex
    // a set of assumed pod keys.
    // The key could further be used to get an entry in podStates.
    assumedPods sets.String
    // a map from pod key to podState.
    podStates map[string]*podState
    nodes     map[string]*nodeInfoListItem
    // headNode points to the most recently updated NodeInfo in "nodes". It is the
    // head of the linked list.
    headNode *nodeInfoListItem
    nodeTree *nodeTree
    // A map from image name to its imageState.
    imageStates map[string]*imageState
}

说他是缓存,从这个构造体能够看到,实际上就是 map,用来存储 Pod 和 Node 的信息。那么这些数据是怎么来的呢?咱们来看下一个组件 informer

  • informer

informer 是 client-go 提供的能力,他的作用是监听指标资源的变动,同步到本地缓存。简直,在 k8s 的所有组件包含 controller-manager,kube-proxy,kubelet 等都应用了 informer 来监听 kube-apiserver 来获取资源的变动。举个例子,比方你执行了 kubectl edit 命令扭转了一个 deployment 的镜像版本,k8s 是怎么感知到这个变动,进一步做 Pod 的重建的工作的呢?就是 kube-scheduler 应用了 informer 来监听 Pod 的变动实现的。

具体来说,kube-scheduler 应用 informer 监听了:Node, Pod, CSINode, CSIDriver, CSIStorageCapacity, PersistentVolume, PersistentVolumeClaim, StorageClass。监听 Node,Pod 咱们能够了解,那么为什么要监听前面那些资源呢?前面的那些资源都是跟存储无关,在 preFilter 和 filter 扩大点的插件外面有 Volumebinding 这么一个插件,是查看零碎以后是否可能满足 Pod 申明的 PVC,如果不能满足,那么只能把 Pod 放入 unscheduleableQ 里。然而,后续如果零碎如果能够满足 Pod 对存储的须要了,这个 Pod 须要第一工夫可能被创立进去,所以零碎必须要可能实时感知到零碎 PVC 等资源的变动及时将 unscheduleableQ 外面调度失败的 Pod 进行从新调度。这就是 informer 存在的意义了。具体的 informer 的实现原理能够参考这篇文章。

  • schedulerQueue

schedulerQueue 蕴含三个队列:activeQ, podBackoffQ,unschedulablePods。

activeQ 是一个优先队列,基于堆实现,用于寄存待调度的 Pod,优先级高的会放在队列头部,优先被调度。该队列寄存的 Pod 可能的状况有:刚创立未被调度的 Pod;backOffPod 队列中转移过去的 Pod;unschedule 队列里转移过去的 Pod。

podBackoffQ 也是一个优先队列,用于寄存那些异样的 Pod,这种 Pod 须要期待肯定的工夫才可能被再次调度,会有协程定期去读取这个队列,而后退出到 activeQ 队列而后从新调度。

unschedulablePods 严格上来说不属于队列,用于寄存调度失败的 Pod。这个队列也会有协程定期(默认 30s)去读取,而后判断以后工夫间隔上次调度工夫的差是否超过 5Min,如果超过这个工夫则把 Pod 挪动到 activeQ 从新调度。

func (p *PriorityQueue) Run() {go wait.Until(p.flushBackoffQCompleted, 1.0*time.Second, p.stop)
    go wait.Until(p.flushUnschedulablePodsLeftover, 30*time.Second, p.stop)
}

说完这几个组件,咱们再来看看,当一个新的 Pod 创立进去后,这个流程是怎么走的

  1. informer 监听到了有新建 Pod,依据 Pod 的优先级把 Pod 退出到 activeQ 中适当地位(即执行 sort 插件);
  2. scheduler 从 activeQ 队头取一个 Pod(如果队列没有 Pod 可取,则会始终阻塞;此时假如就是上述说的新建的 Pod),开始调度;
  3. 执行 filter 类型扩大点(包含 preFilter,filter,postFilter)插件,选出所有合乎 Pod 的 Node,如果无奈找到合乎的 Node,则把 Pod 退出 unscheduleableQ 中,此次调度完结;
  4. 执行 score 扩大点插件,找出最合乎 Pod 的 那个 Node;
  5. assume Pod。这一步就是乐观假如 Pod 曾经调度胜利,更新缓存中 Node 和 PodStats 信息,到了这里 scheduling cycle 就曾经完结了,而后会开启新的一轮调度。至于真正的绑定,则会新起一个协程。
  6. 执行 reserve 插件;
  7. 启动协程绑定 Pod 到 Node 上。实际上就是批改 Pod.spec.nodeName: 选定的 node 名字,而后调用 kube-apiserver 接口写入 etcd。如果绑定失败了,那么移除缓存中此前退出的信息,而后把 Pod 放入 activeQ 中,后续从新调度。
  8. 执行 postBinding,该步没有实现的插件没所以没有做任何事。

以上就是 kube-scheduler 的基本原理。

在前面的文章中,咱们会持续聊聊 kube-scheduler 是怎么初始化进去的,要想开发一个本人的插件要做哪些事。

本文由博客一文多发平台 OpenWrite 公布!

正文完
 0