kube-scheduler 源码解析

 opsdev 王希刚 360云计算

女主宣言

本篇文章带大家了解部署在我们 HULK 容器服务 master 节点上的重要组件之一,kube-scheduler 的运行机制解读和核心代码分析,给想要阅读学习 Kubernetes 源码的同学一个参考。本文最先发布于 opsdev,转载已获取作者授权。 

PS:丰富的一线技术、多元化的表现形式,尽在“HULK一线技术杂谈”,点关注哦!

前言

本文所涉及的源码为 Kubernetes 1.9,git commit id 为 925c127ec。本文前半部分讲解 scheduler 的原理,后半部分对 scheduler 源码进行分析。

1

kubernetes scheduler 基本原理

kubernetes scheduler 作为一个单独的进程部署在 master 节点上,它会 watch kube-apiserver 进程去发现 PodSpec.NodeName 为空的 Pod,然后根据指定的算法将 Pod 调度到合适的 Node 上,这一过程也叫绑定(Bind)。scheduler 的输入是需要被调度的 Pod 和 Node 的信息,输出是经过调度算法筛选出条件最优的 Node,并将该 Pod 绑定到这个 Node 上。如下图所示:

kube-scheduler 源码解析_Java


scheduler 调度算法分为两个阶段:


  • 预选 (Predicates)

根据Predicates策略去滤掉不符合 Policies 的 Node.


  • 优选 (Priorities)

经过 Predicates 剩下的 Node,需要经过Priorities 策略选出一个最优的 Node,并将 Pod 绑定到该 Node 上。根据下面这张调度图详细描述下:


1.  首先 scheduler 根据 predicates 集合过滤掉不符合的 Node。例如,如果 PodSpec 指定的请求资源 (resource requests),那么 scheduler 会过滤掉没有足够资源的 Node。


2.  其次 scheduler 会根据 priority functions 集合从 predicates 中过滤出来的 Node 中,选出一个最优的 Node。


算法实现:

对每一个 Node, priority functions 会计算出一个 0-10 之间的数字,表示 Pod 放到该 Node 的合适程度,其中 10 表示非常合适,0 表示不合适,priority functions 集合中的每一个函数都有一个权重 (weight),最终的值为 weight 和 priority functions 的乘积,而一个节点的 weight 就是所有 priority functions 结果的加和。例如,有两个 priority functions: priorityFunc1 和 priorityFunc2,对应的 weight 分别为 weight1 和 weight2,那么 NodeA 的最终得分是:


kube-scheduler 源码解析_Java_02

3.  最终,得分最高的 Node 胜出(如果有多个得分相同的 Node,会随机的选取一个 Node 作为最终胜出的 Node)。


kube-scheduler 源码解析_Java_03

2

kubernetes scheduler 源码分析

 

scheduler 的代码结构


kube-scheduler 源码解析_Java_04

  • k8s.io/kubernetes/plugin/cmd/scheduler.go 为程序入口文件 (main.go)

  • k8s.io/kubernetes/plugin/cmd/kube-scheduler/app/server.go 包含 scheduler 的基础配置项


除了入口函数,scheduler 的具体逻辑实现均在 k8s.io/kubernetes/plugin/pkg/scheduler 目录下,这里就不一一介绍了。


 

scheduler的具体实现

kube-scheduler 源码解析_Java_05

上面的时序图就是整个 scheduler 的具体实现逻辑。下面根据这个时序图来对 scheduler 的源码进行解析。


  • NewSchedulerCommand 创建一个 scheduler命令行实例,用来对 scheduler 的命令行参数进行解析校验并且包含 scheduler 程序的入口函数 Run 的函数定义。

  • command.Execute() 会执行命令行实例中的 options.Run 方法。


在 options.Run 中主要进行了如下的操作:


  • loadConfigFromFile 加载 scheduler 配置文件信息。

  • NewSchedulerServer 创建 scheduler server 实例,使用 scheduler 配置参数对 scheduler server 进行初始化,如:


  1. createClients 创建一系列 client,如连接 k8s 的 client,进行 scheduler 选主的 client 及 event client.

  2. makeLeaderElectionConfig 生成 Leader Election 配置信息 (scheduler做了 HA,可以同时运行多个实例进程,但只有一个能正常工作,如果主的 scheduler 挂了,会重新进行选举)。

  3. makeHealthzServer 初始化 healthz server,用于健康检查。

  4. makeMetricsServer 初始化 metrics server,用于 prometheus 性能监控。


  • SchedulerServer.Run 启动 SchedulerServer,用于监控还是否有 Pod 待调度,并且进行相应的调度工作。具体的代码实现:

kube-scheduler 源码解析_Java_06

  • SchedulerConfig() 创建Scheduler Config,其中关键性函数是 NewConfigFactory 和 CreateFromProvider。


  1. NewConfigFactory 定义了 podQueue 用来存储需要被调度的 Pod,每当新的 Pod 建立后,就会将 Pod 添加到该 queue 中。

  2. CreateFromProvider 根据 algorithm provider 名称创建一个 scheduler 配置信息。其中 GetAlgorithmProvider 则根据 provider 名称去获取指定的 provider。

kube-scheduler 源码解析_Java_07

scheduler 默认使用的 provider 是 DefaultProvider。它主要实现了如下数据结构:


kube-scheduler 源码解析_Java_08

AlgorithmProviderConfig 这个数据结构包含预选和优选相关算法 key 的集合 (一个算法对应一个key,key 是算法的名字,value 是算法的具体实现 funtion),而这些算法注册是在 scheduler/algorithmprovider/defaults/defaults.go 文件的 init() 方法中进行注册 (实现使用的是工厂模式)。 如果想了解预选和优选算法的详细信息,请参看官方文档:

https://github.com/kubernetes/community/blob/master/contributors/devel/scheduler_algorithm.md


接着往下说: 通过 GetAlgorithmProvider 得到了 provider 关联的预选和优选算法集合的 Key。然后通过调用 CreateFromKeys (预选和优选的 Key作为参数) 来获取预选和优选算法的具体实现 (funtion),并对 NewGenericScheduler 实例进行初始化,返回最终的 scheduler 配置信息。


  • NewFromConfig 由 Scheduler Config 创建一个 schduler。

  • Start up the healthz server 启动健康检查服务

  • Start up the metrics server 启动 metrics 服务,供 Prometheus 进行性能监控数据的抓取。

  • LeaderElection 如果指定选举的方式来启动scheduler,则使用这种方式来执行scheduler。(使用 CallBack 的方式执行 Run 方法。如果主的 scheduler 出现问题,还会指定优雅处理函数对其进行处理)。


下面就到了 scheduler 真正干活的逻辑了,每次调度一个 Pod 都会执行下面的 Scheduler.Run(),具体的代码如下:


kube-scheduler 源码解析_Java_09

  • WaitForCacheSync() 将最新的数据同步到 SchedulerCache 缓存中。

  • scheduleOne() 调度 Pod 的整体逻辑。具体的实现可以看下面代码:

  • kube-scheduler 源码解析_Java_10
  • NextPod() 从 PodQueue 中获取一个未绑定的Pod。

  • schedule(pod) 执行对应 Algorithm 的 Schedule,进行预选和优选。接口定义如下:

  • kube-scheduler 源码解析_Java_11
  • Schedule 主要包含如下几个重要的方法:


  1. nodeLister.List() 获取可用的 Node 列表。

  2. findNodesThatFit() 进行预选。

  3. PrioritizeNodes() 进行优选。

  4. selectHost() 如果优选出的多个得分相同的 Node,则随机选取一个 Node。


  • assume() 更新 SchedulerCache 中 Pod 的状态,标志该 Pod 为 scheduled,并更新到 NodeInfo 中。

  • bind() 调用 kube-apiserver API,将 Pod 绑定到选出的 Node,之后 Kube-apiserver 会将元数据写入 etcd 中。接口定义如下:

  • kube-scheduler 源码解析_Java_12

这样一个 Pod 绑定到 Node 的流程就完成了。


3

总结

kube-scheduler 作为 Kubernetes master上一个单独的进程提供调度服务,通过 master 指定 kube-api-server 的地址,用来 watch Pod 和 watch Node 并调用 api server bind 接口完成 Node 和 Pod 的 Bind 操作。


kube-scheduler 中维护了一个 FIFO 类型的 PodQueue cache (其实还有一种 PriorityQueue 用于指定 Pod 的优先级,需要指定参数开启,默认是 FIFO 队列),新创建的 Pod 都会被 ConfigFactory watch 到,被添加到该 PodQueue 中,每次调度都从该 PodQueue 中 NextPod() 一个即将调度的 Pod。


获取到待调度的 Pod 后,就执行 AlgorithmProvider 配置 Algorithm 的 Schedule 方法进行调度,整个调度过程分两个关键步骤:Predicates 和 Priorities,最终选出一个最适合该 Pod 的 Node。


更新 SchedulerCache 中 Pod 的状态 (AssumePod),标志该 Pod 为 scheduled,并更新到 NodeInfo 中。


调用 api server 的 Bind 接口,完成 Node 和 Pod 的 Bind 操作,如果 Bind 失败,从 SchedulerCache 中删除上一步中已经 Assumed 的 Pod