k8s 作为云原生最重要的基石之一,她是怎么运作的呢?你是否了解过她是怎么从众多的 node 节点中筛选出符合 pod 的调度节点,这里会从 k8s 的调度原理和流程开始结合源码内容带你了解整个调度过程,并配合一个小的调度实验,让你亲手实现一个简单的k8s调度器。
PS:本文有些长,有兴趣的同学可以先收藏再阅读
k8s 调度器实现原理 k8s 中一个任务的创建流程 k8s 的 scheduler 和 controller manager,kubelet 这些是一样的,都是针对 apiserver 进行控制循环的操作。
当我们通过 kubectl 命令创建一个 job 的时候,kube-contoller 检测到资源的创建,并根据参数创建一个 pod 的实例发送给 apiserver。 kube-scheduler 调度器检测到一个新的未调度 pod,他会从已有的 node 节点选择出一个 node节点绑定这个pod,并向 apiserver 发送一个绑定指令。 部署在对应节点上的 kubelet 通过 watchAndList 从 apiserver 检测到这个绑定的指令后,会发送到节点上的 container api,让其节点上运行这个pod。 以上是一个简单job的创建流程为例。这里面的kube-scheduler
调度器就是我们今天带大家了解的k8s基础组件之一 —— k8s的调度器。
kube-scheduler调度器的内部流转流程 通过上面我们知道,kube-scheduler 主要是负责将 pod 绑定到适合的 node 上面,那么 kube-scheduler 是怎么选择适合的 node 节点的呢?
这里提供了一副 kube-scheduler 调度的全景图:
整个事件流程如下:
Scheduler 通过注册 client-go 的 informer 的 handler 方法监听 api-server 的 pod 和 node 变更事件,从而实现将 pod 的信息更新 scheduler 的 activeQ, podbackoffQ, unschedulableQ 三个队列中。 带调度的 pod 会进入到 activeQ 的调度队列中,activeQ 是一个维护着 pod 优先级的堆结构,调度器在调度循环中每次从堆中取出优先级最高的 pod 进行调度。 取出的待调度 pod 会经过调度器的一系列调度算法找到合适的 node 节点进行绑定。如果调度算法判定没有适合的节点,会将 pod 更新为不可调度状态,并扔进 unschedulable 的队列中。 调度器在执行绑定操作的时候是一个异步过程,调度器会先在缓存中创建一个和原来 pod 一样的 assume pod 对象用模拟完成节点的绑定,如将 assume pod 的 nodename 设置成绑定节点名称,同时通过异步执行绑定指令操作。 在 pod 和 node 绑定前,scheduler需要确保 volume 已经完成绑定操作,确认完所有绑定前准备工作,scheduler 会向 api-server 发送一个 bind 对象,对应节点的 kubelet 将待绑定的pod在节点运行起来。 kube-scheduler 源码解析 在源码解读这小节我会把 kube-scheduler分成三部分,第一部分是 scheduleOne,也就是调度器的主线逻辑,第二部分是 Algorithm,也就是调度阶段的核心流程。
本章节源码基于 kuberenesv1.19版本,commit id: 070ff5e3a98bc3ecd596ed62bc456079bcff0290
先对整个 kube-scheduler 的源码解析图和 scheduler 对象有个初步的认识,方便我们后续查阅:
上面是调度器的组件,下面我们再看看调度框架包涵哪些:
ScheduleOne 基于上面 kube-scheduler 的源码解析图,我们知道 scheduleOne 的流程如下:
sche.NextPod(),从 scheduleQueue 获取需要调度的 pod 通过 pod 的 SchedulerName 判断是否属于这个调度器处理,kube-scheduler 的名字是 default-scheduler,因此 pod 没有专门指定调度器的都会被k8s默认调度器处理。 确定属于自己处理后进入调度节点,通过 sched.Algorithm.Schedule 找到当前 pod 最适合的节点,如果没找到适合的节点,调度器会根据 pod 的优先级进行抢占操作。 在通过调度算法找到适合的待调度节点之后就是具体调度了,这里 schedule 设计了一个 assume pod 的对象,这个 assume pod 将原来的 pod 对象深度拷贝放入 scheduler cache 中,并设置 nodeName 表示这个节点已被调度,后续的检查和调度基于 assume pod 对象,这样就可以对 pod 进行异步绑定操作而不会有读写锁的问题了。 接着 assume pod 会对卷进行 AssumePodVolumes,这一步主要由 RunReservePluginsReserve 方法实现。如果预设操作失败,会进行回滚操作。 到 Permit 阶段,这个阶段是在真正调度前对 pod 绑定操作进行最后的批准、拒绝或者执行延时调度。 在 Permit 之后,资源的准备评估结束,正式进入第二阶段 pod 的真正绑定周期,整个绑定过程是异步的,放在 go func() 里面。 进入异步绑定阶段后,会先通过一个 WaitOnPermit 方法来检查是否延迟调度的,如果有会进行等待。 之后会进入 prebind,prebind 主要做 pvc 和 pv 的绑定。 完成 prebind 之后就正式进入 bind 操作,scheduler 会向 api-server 发送一个 bind 请求。完成绑定后会执行 postbind,现在这个 plugin 还是一个空的插入点,k8s暂时还没有默认插件。 下面是 ScheduleOne 的源码及注释:
这里面从 RunReservePluginsReserve,RunPermitPlugins,RunPreBindPlugins,RunPreBindPlugins 到 RunPostBindPlugins 都支持用户编写自己的插件扩展 scheduler 调度器。
对照 scheduler framework 官方图解:
详细可以参考kube-scheduler#624提案
我们看看 k8s 的提供的一些默认插件:
Algorithm Algorithm 是 scheduler 的调度的核心,包涵了一个过滤器和一个打分器,核心逻辑就是把所有适合的节点筛选出来,在再里面找出最优的节点,下面看下 Algorithm 的代码,kube-default 的 algorithm 对象是由一个叫 genericScheduler 的实例实现:
先会通过 podPassesBasicChecks 对 pod 做基本检查,比如检查 pod 使用的 pvc 是否存在在命名空间下 然后会将 scheduler cache 和 node info 做一次镜像,方便后续对相关数据的使用,这里为啥对 cache 也还要再做一层镜像缓存在后面 scheduler 优化那一小节会讲到. 通过 findNodesThatFitPod 方法找出所有适合的节点,再通过 prioritizeNodes 对所有节点进行打分 根据打分结果找出最高得分节点返回 找出适合节点 下面来看看findNodesThatFitPod
方法:
findNodesThatFitPod 比较简单,包涵三个方法,一个是 prefilter 插入点,一个是 filter 插入点,还有一个是 extender filter。
prefilter 主要是做一些过滤前的预处理,比如 node port信息, volumebinding 信息等。
filter 对节点做过滤,找出适合的框架,这里会检查节点的亲和性,资源是否充足,是否存在挂载卷等。
extender 这个是旧版调度器架构的扩展方式,这里就不累赘,有兴趣的可以自行学习。
计算适合节点的得分 在通过findNodesThatFitPod
方法获得适合分配的节点后,需要通过prioritizeNodes
方法来打分找到最适合的节点:
findNodesThatFitPod主要包括 PreScore 和 ScorePlugins两个插入点
RunScorePlugins 方法会先对节点进行打分,然后再对打分插件的打分进行修正,最后乘以各插件的权重系数就得到各插件打分的最终分数
最后再将各种插件打分的结果汇总得到节点的总分表
ScorePlugins打分插件包括:
小结 k8s调度器从1.15 开始由 extension 模式改成了 framework 的架构,kube-scheduler整个代码架构提供了更灵活性定制化能力,可以在原架构上满足了更灵活定制化的需求,而不需要重新 fork 一份源码来修改。
参考文献