从一个pod的创建开始

  1. 由kubectl解析创建pod的yaml,发送创建pod请求到APIServer。
  2. APIServer首先做权限认证,然后检查信息并把数据存储到ETCD里,创建deployment资源初始化。
  3. kube-controller通过list-watch机制,检查发现新的deployment,将资源加入到内部工作队列,检查到资源没有关联pod和replicaset,然后创建rs资源,rs controller监听到rs创建事件后再创建pod资源。
  4. scheduler 监听到pod创建事件,执行调度算法,将pod绑定到合适节点,然后告知APIServer更新pod的spec.nodeName
  5. kubelet 每隔一段时间通过其所在节点的NodeName向APIServer拉取绑定到它的pod清单,并更新本地缓存。
  6. kubelet发现新的pod属于自己,调用容器API来创建容器,并向APIService上报pod状态。
  7. Kub-proxy为新创建的pod注册动态DNS到CoreOS。为Service添加iptables/ipvs规则,用于服务发现和负载均衡。
  8. deploy controller对比pod的当前状态和期望来修正状态。

调度器介绍

从上述流程中,我们能大概清楚kube-scheduler的主要工作,负责整个k8s中pod选择和绑定node的工作,这个选择的过程就是应用调度策略,包括NodeAffinity、PodAffinity、节点资源筛选、调度优先级、公平调度等等,而绑定便就是将pod资源定义里的nodeName进行更新。

设计

kube-scheduler的设计有两个历史阶段版本:

  1. 基于谓词(predicate)和优先级(priority)的筛选。
  2. 基于调度框架的调度器,新版本已经把所有的旧的设计都改造成扩展点插件形式(1.19+)。

所谓的谓词和优先级都是对调度算法的分类,在scheduler里,谓词调度算法是来选择出一组能够绑定pod的node,而优先级算法则是在这群node中进行打分,得出一个最高分的node。

而调度框架的设计相比之前则更复杂一点,但确更加灵活和便于扩展,关于调度框架的设计细节可以查看官方文档——624-scheduling-framework,当然我也有一遍文章对其做了翻译还加了一些便于理解的补充——KEP: 624-scheduling-framework。总结来说调度框架的出现是为了解决以前webhooks扩展器的局限性,一个是扩展点只有:筛选、打分、抢占、绑定,而调度框架则在这之上又细分了11个扩展点;另一个则是通过http调用扩展进程的方式其实效率不高,调度框架的设计用的是静态编译的方式将扩展的程序代码和scheduler源码一起编译成新的scheduler,然后通过scheduler配置文件启用需要的插件,在进程内就能通过函数调用的方式执行插件。

调度流程

现在网上大部分的kube-scheduler调度流程文章都不是基于新的调度框架所写的,还是谓词和优先级的流程。基于调度框架实现的调度流程总的来说就是执行一个个插件的过程,如下图:

整个过程可以分为两个周期:调度周期(scheduling cycle)、绑定周期(Binding Cycle),这两个周期的区别不仅仅是包含插件,还有每个周期的上下文(Cycle Context),这个上下文将贯穿各自的周期使周期内的每个插件之间能够进行数据的交流。Sort插件是不属于两个周期任何一个,它的职责就是对调度队列中的Pod进行排序。

一个pod的调度过程在调度插件里是线性执行下去的,但是绑定周期的执行是异步的,也就是说scheduler在执行A Pod的绑定周期时,其实也同时开始了B Pod的调度周期。这也是比较合理的,毕竟Bind插件是需要和APIServer进行通信来更新调度pod的nodeName,这个网络IO过程存在着不可确定性。

调度周期:

Filter插件的功能类似之前的谓词调度,这个过程就是根据调度策略函数(在调度框架里就是多个Filter插件函数)进行node筛选,筛选的原理就是将被筛选的node和待调度的pod以及周期上下文等作为参数一并传入这些函数,最后收集通过了所有筛选函数的node进入下一阶段,在这个阶段将会以node为单位进行并发处理。

PostFilter插件虽说是发生在Filter之后,但是确只能在Filter插件没有返回合适的node才执行。在scheduler里默认的PostFilter插件只有一个功能,进行抢占调度。抢占调度的原理:首先会将node上低于待调度pod的优先级的Pod全部剔除,当然这个只是模拟过程并不是真正将Pod从干掉,然后再次执行Filter插件,如果失败了那就是抢占调度失败,成功了则将前面剔除的pod一个一个加回来,每一次都执行Filter插件从而找出调度该Pod所需要剔除的最少的低优先级Pod。

Score插件的功能类比以前的优先级调度,这个过程是对前一阶段得出的node列表进行再筛选,得出最终要调度的node。NormalizeScore再调度框架里也不能算是一个单独扩展点,它往往是配合着score插件一起出现,为了将统一插件打分的分数。在调度框架里是作为Score插件可选的实现接口,同样的Score插件的也是会并发的在每个node上执行。

Reserve 插件有两种函数,reserve函数在绑定前为Pod做准备动作,Unreserve函数则在绑定周期间发生错误的时候做恢复。默认的Reserve插件使用情况是处理pod关联里pvc与pv的绑定和解绑。

绑定周期:

整个绑定周期都是在一个异步的协程中,在执行进入绑定周期前会执行Pod的assume(假定)过程,这个过程做的主要是假设Pod已经绑定到目标node上,所以会更新scheduler的node缓存信息,这样当调度下一个pod到前一个pod真正在node上创建的过程中,能够用真正的node信息进行调度。

Scheduler的启动流程

现在我们了解了scheduler是如何执行调度算法、pod绑定过程的,但是对于什么时候执行调度和调度的pod怎么获得其实还并不清楚,所以我们需要深入到scheduler的代码来了解这一切。

上面是一个简略版的调度器处理pod流程:

首先scheduler会启动一个client-go的Informer来监听Pod事件(不只Pod其实还有Node等资源变更事件),这时候注册的Informer回调事件会区分Pod是否已经被调度(spec.nodeName),已经调度过的Pod则只是更新调度器缓存,而未被调度的Pod会加入到调度队列,然后经过调度框架执行注册的插件,在绑定周期前会进行Pod的假定动作,从而更新调度器缓存中该Pod状态,最后在绑定周期执行完向ApiServer发起BindAPI,从而完成了一次调度过程。

先找到在/cmd/kube-scheduler/scheduler.go的入口函数

func main() {
command := app.NewSchedulerCommand()
code := cli.Run(command)
os.Exit(code)
}

k8中组件通用的启动模版,我们需要找到这个command定义的

func NewSchedulerCommand(registryOptions ...Option) *cobra.Command {
...
cmd := &cobra.Command{ // 定义了一个cobra的Comand结构体, cmd.Execute(),会执行定义的Run函数。
Run: func(cmd *cobra.Command, args []string) {
if err := runCommand(cmd, opts, registryOptions...); err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
os.Exit(1)
}
}
...
}
}

查看runCommand定义

func runCommand(cmd *cobra.Command, opts *options.Options, registryOptions ...Option) error {
...
cc, sched, err := Setup(ctx, opts, registryOptions...) // 初始化配置、Scheduler
...
return Run(ctx, cc, sched)
}

查看Run定义

func Run(ctx context.Context, cc *schedulerserverconfig.CompletedConfig, sched *scheduler.Scheduler) error {
// To help debugging, immediately log version
klog.V(1).Infof("Starting Kubernetes Scheduler version %+v", version.Get()) // 全局配置
if cz, err := configz.New("componentconfig"); err == nil {
cz.Set(cc.ComponentConfig)
} else {
return fmt.Errorf("unable to register configz: %s", err)
} // 事件管理器
cc.EventBroadcaster.StartRecordingToSink(ctx.Done()) // 选举检查
var checks []healthz.HealthChecker
if cc.ComponentConfig.LeaderElection.LeaderElect {
checks = append(checks, cc.LeaderElection.WatchDog)
} // http和metric服务
if cc.InsecureServing != nil {
...
}
if cc.InsecureMetricsServing != nil {
...
}
// https服务
if cc.SecureServing != nil {
...
} // 启动所有Informer
cc.InformerFactory.Start(ctx.Done()) // 等待informer缓存完毕
cc.InformerFactory.WaitForCacheSync(ctx.Done()) // 选举机制启动
if cc.LeaderElection != nil {
...
} // 非选举机制启动过, 无论是选举和非选举启动都会调用最后处理逻辑都会到sched.Run()
sched.Run(ctx)
return fmt.Errorf("finished without leader elect")
}

sched.Run在/pkg/scheduler/scheduler.go

func (sched *Scheduler) Run(ctx context.Context) {
...
sched.SchedulingQueue.Run()
wait.UntilWithContext(ctx, sched.scheduleOne, 0)
sched.SchedulingQueue.Close()
}

其中wait.UntilWithContext将会不间断的调用sched.scheduleOne函数,这么看schedulerOne就是处理Pod调度的工作函数了,到这里我们得回到上面New出sched的地方cc, sched, err := Setup(...)

func Setup(ctx context.Context, opts *options.Options, outOfTreeRegistryOptions ...Option) (*schedulerserverconfig.CompletedConfig, *scheduler.Scheduler, error) {
c, err := opts.Config() // 从Options(命令行收集)初始化schedler的配置 cc := c.Complete() // 补充配置 // Create the scheduler.
sched, err := scheduler.New(...), // 初始化Scheduler
)
return &cc, sched, nil
}

查看New方法

func New(...) (*Scheduler, error) {
options := defaultSchedulerOptions // 设置默认配置项
...
configurator := &Configurator{ // 创建配置器
...
} sched, err := configurator.create() // 通过配置起器创建scheduler
if err != nil {
return nil, fmt.Errorf("couldn't create scheduler: %v", err)
}
// 为informer设置监听事件,包括pod(已调度(字段NodeName)-添加到SchedulerCache, 为调度则添加到SchedulingQueue队列中。
// Node、PV、PVC、SC、CSINode、Service
addAllEventHandlers(sched, informerFactory, podInformer)
return sched, nil
}

查看配置起Configuratorcreate

func (c *Configurator) create() (*Scheduler, error) {
// 创建提名队列,用于存储发生抢占的Pod
nominator := internalqueue.NewPodNominator(c.informerFactory.Core().V1().Pods().Lister())
profiles, err := profile.NewMap(...) // 调度框架配置 podQueue := internalqueue.NewSchedulingQueue() // 创建调度框架 algo := NewGenericScheduler() // 创建调度算法,这里面主要是执行筛选和打分插件 return &Scheduler{
SchedulerCache: c.schedulerCache, // 调度缓存
Algorithm: algo, // 调度算法
Extenders: extenders, // webhook扩展
Profiles: profiles, // 调度框架配置
NextPod: internalqueue.MakeNextPodFunc(podQueue), // 获取调度Pod
Error: MakeDefaultErrorFunc(), // 调度失败处理
StopEverything: c.StopEverything, // 停止器
SchedulingQueue: podQueue, // 调度队列
}, nil
}

这里我们发现了SchedulingQueue是 由NewSchedulingQueue声明的一个对象。

/pkg/scheduler/internal/queue/scheduling_queue.go

func NewPriorityQueue(
lessFn framework.LessFunc,
opts ...Option,
) *PriorityQueue {
...
pq := &PriorityQueue{ // 定义了3种队列,activeQ、unschedulableQ、podBackoffQ
PodNominator: options.podNominator,
clock: options.clock,
stop: make(chan struct{}),
podInitialBackoffDuration: options.podInitialBackoffDuration,
podMaxBackoffDuration: options.podMaxBackoffDuration,
activeQ: heap.NewWithRecorder(),
unschedulableQ: newUnschedulablePodsMap(),
moveRequestCycle: -1,
}
pq.podBackoffQ = heap.NewWithRecorder()
return pq
}

SchedulingQueue的结构

type SchedulingQueue interface {
...
Pop() (*framework.QueuedPodInfo, error)
Update(oldPod, newPod *v1.Pod) error
Delete(pod *v1.Pod) error
MoveAllToActiveOrBackoffQueue(event string)
}

找到了sched的属性SchedulingQueue实际上是一个PriorityQueue对象,我们找到它的Run方法。

func (p *PriorityQueue) Run() {
// 每一秒从podBackoffQ拿出最近的pod检查是否可以加入到activeQ
go wait.Until(p.flushBackoffQCompleted, 1.0*time.Second, p.stop)
// 没30秒从无法调度pod的队列拿出pod检查是否可以加入到activeQ
go wait.Until(p.flushUnschedulableQLeftover, 30*time.Second, p.stop)
}

现在我们找到了整个sched的启动和调度队列管理的功能,接下来查看具体调度一个pod的详细经过。

sched.Run中我们找打了scheduleOne方法:/pkg/scheduler/scheduler.go

func (sched *Scheduler) scheduleOne(ctx context.Context) {
podInfo := sched.NextPod() // 获取activeQ的下一个pod
fwk, err := sched.frameworkForPod(pod) // 从Pod里获取设置调度框架,默认`default-schdeler`
...
scheduleResult, err := sched.Algorithm.Schedule() // 执行调度算法:Filter和Score等插件
...
err = sched.assume() // 假定pod
...
go func() { // 异步执行bind
...
err := sched.bind()
...
}
}

这个函数正是处理pod调度的主函数,而获取需要调度的pod是执行sched.NextPod(),然后就是执行调度框架里的各个注册插件,至此这就是所有的scheduler的工作代码了,如果要看详细的流程,可以查看我写的思维导图。

github思维导图地址:https://github.com/goofy-z/k8s-learning/blob/master/K8s源码学习/kube-scheduler/scheduler.xmind

在线思维导图:https://www.processon.com/view/link/6167925d5653bb1336dca0ca

k8s调度器介绍(调度框架版本)的更多相关文章

  1. Go调度器介绍和容易忽视的问题

    本文记录了本人对Golang调度器的理解和跟踪调度器的方法,特别是一个容易忽略的goroutine执行顺序问题,看了很多篇Golang调度器的文章都没提到这个点,分享出来一起学习,欢迎交流指正. 什么 ...

  2. scrapy 基础组件专题(七):scrapy 调度器、调度器中间件、自定义调度器

    一.调度器 配置 SCHEDULER = 'scrapy.core.scheduler.Scheduler' #表示scrapy包下core文件夹scheduler文件Scheduler类# 可以通过 ...

  3. 重新梳理调度器——GMP 调度模型

    调度器--GMP 调度模型 Goroutine 调度器,它是负责在工作线程上分发准备运行的 goroutines. 首先在讲 GMP 调度模型之前,我们先了解为什么会有这个模型,之前的调度模型是什么样 ...

  4. Linux调度器 - deadline调度器

    一.概述 实时系统是这样的一种计算系统:当事件发生后,它必须在确定的时间范围内做出响应.在实时系统中,产生正确的结果不仅依赖于系统正确的逻辑动作,而且依赖于逻辑动作的时序.换句话说,当系统收到某个请求 ...

  5. Kubernetes之调度器和调度过程

    scheduler 当Scheduler通过API server 的watch接口监听到新建Pod副本的信息后,它会检查所有符合该Pod要求的Node列表,开始执行Pod调度逻辑.调度成功后将Pod绑 ...

  6. 泡面不好吃,我用了这篇k8s调度器,征服了他

    1.1 调度器简介 来个小刘一起 装逼吧 ,今天我们来学习 K8的调度器 Scheduler是 Kubernetes的调度器,主要的任务是把定义的 pod分配到集群的节点上,需要考虑以下问题: 公平: ...

  7. Kubernetes K8S之调度器kube-scheduler详解

    Kubernetes K8S之调度器kube-scheduler概述与详解 kube-scheduler调度概述 在 Kubernetes 中,调度是指将 Pod 放置到合适的 Node 节点上,然后 ...

  8. k8s之调度器、预选策略及优选函数

    1.调度器(scheduler) 调度器的功能是调度Pod在哪个Node上运行,这些调度信息存储在master上的etcd里面,能够和etcd打交道的只有apiserver; kubelet运行在no ...

  9. kubernetes 调度器

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

随机推荐

  1. rabbitMQ重复消费(结合死循环重发那一篇看)

    /** * 重复消费逻辑判断与处理 */ @Component public class RepeatMqConsumer { /** * 服务对象 */ private int count=1; @ ...

  2. ArrayPool 源码解读之 byte[] 也能池化?

    一:背景 1. 讲故事 最近在分析一个 dump 的过程中发现其在 gen2 和 LOH 上有不少size较大的free,仔细看了下,这些free生前大多都是模板引擎生成的html片段的byte[]数 ...

  3. 树莓派4B切换国内源-亲测有效

    参考:https://blog.csdn.net/qq_30290661/article/details/103386997 修改/etc/apt/sources.list,去掉自带的源,添加如下源: ...

  4. 基于Linux系统的网络服务——高速缓存DNS及企业级域名解析服务

    1.DNS域名系统 DNS(Domain Name System,域名系统),因特网上作为域名和IP地址相互映射的一个分布式数据库,能够使用户更方便的访问互联网,而不用去记住能够被机器直接读取的IP数 ...

  5. 你的 SQL 还在回表查询吗?快给它安排覆盖索引

    什么是回表查询 小伙伴们可以先看这篇文章了解下什么是聚集索引和辅助索引:Are You OK?主键.聚集索引.辅助索引,简单回顾下,聚集索引的叶子节点包含完整的行数据,而非聚集索引的叶子节点存储的是每 ...

  6. CentOS7系统搭建FTP服务器

    创建FTP服务器1.安装FTP服务 yum install -y vsftpd 默认的FTP服务的配置文件路径为/etc/vsftpd cd /etc/vsftpd[root@test924 vsft ...

  7. P1721 [NOI2016] 国王饮水记 题解

    蒟蒻的第一篇黑题题解,求过. 题目链接 题意描述 这道题用简洁的话来说,就是: 给你 \(n\) 个数字,你可以让取其中任意若干个数字,每次操作,都会使所有取的数字变为取的数字的平均数,并且你最多只能 ...

  8. Python习题集(七)

    每天一习题,提升Python不是问题!!有更简洁的写法请评论告知我! https://www.cnblogs.com/poloyy/category/1676599.html 题目 如果有一个列表a= ...

  9. ubantu下载源详细目录

    都说ubantu系统自带的下载源不给力,一般使用时体现不出来,也没有必要更换.我是在安装gnuradio时,安装了好久,没安装上,后来就去更改下载源(后来发现不是下载源的问题),不过还不错,最起码最下 ...

  10. DLL延时加载技术与资源释放

    DLL延时加载技术与资源释放 0x00 前言 诸如调用非Windows的第三方库,我们或许会使用到dll文件,而这个时候原本程序运行需要相应的dll文件才能加载启动.通过DLL延时加载技术,使用延时加 ...