Kubernetes调度

Kubernetes调度

一、调度系统集群模式

Kubernetes调度系统本身的集群模式是主从。由Master节点负责调度,将任务调度分配到Node节点中。

二、在集群模式下,调度系统本身如何调度资源,例如如果要调度任务 A,那么应该是在调度系统进群的哪个节点上调度

Kubernetes的调度模式如下:

首先调度器会寻找nodeName为空的Pod进行作业调度,调度流程分为两部。
第一步Predicate:过滤不符合条件的节点

第二部Priority:优先级打分排序,选择优先级最高的节点。

三、任务分类

Kubernetes支持 LRS(Long Running Service)和 Batch Jobs 两种作业形态,对它们进行“分别管理”和“混合调度”。这里针对于任务分类,我们主要说一下Job和CronJob这两个对象。

  1. 首先我先介绍一下Job对象中负责并行控制的两个参数:
    • spec.completions:控制任务完成的数量。它定义的是 Job 至少要完成的 Pod 数目,即 Job 的最小完成数。
    • spec.parallelism:控制任务并行数。它定义的是一个 Job 在任意时间最多可以启动多少个 Pod 同时运行
  2. CronJob 他是一个专门用来管理 Job 对象的控制器。只不过,它创建和删除 Job 的依据,是 schedule 字段定义的、一个标准的Unix Cron格式的表达式。

所以对于我们对于任务的分类可以使用如下方式来实现:

  • 重复任务:可以将completions设置为重复完成的数量,parallelism根据需求设定
  • 定时任务:可以使用cronJob来实现定时任务。
  • 一次性任务:可以将completions设置为1。
  • 实时任务:可以使用job来实现。

四、任务管理

Kubernetes是通过apiserver来查询任务的运行情况。当任务的STATUS字段会记录用户任务执行的情况。

任务日志可以输出到stdout和stderr中然后通过在宿主机上部署 logging-agent 的方式来集中处理日志。

Kubernetes整个项目是声明式API,他只规定他的期望的目的。然后剩下的交给系统来处理。直到达到期望值。一个 Kubernetes 的控制器,实际上就是一个“死循环”,它不断地获取“实际状态”,然后与“期望状态”作对比,并以此为依据决定下一步的操作。

因此他允许删除任务。删除时,将任务,没有调度的任务直接从队列里丢弃。允许修改任务配置。修改调度配置以后会按照新的调度配置重新执行一遍。当删除任务时,任务先接收term信号, 再接收kill信号。

五、任务编排

Kubernetes的核心功能就是任务编排。kube-controller-manager这个组件,就是一系列控制器的集合。每一个控制器,都以独有的方式负责某种编排功能。这里的控制器,都遵循 Kubernetes 项目中的一个通用编排模式,即:控制循环(control loop)

比如,现在有一种待编排的对象 X,它有一个对应的控制器。那么,我就可以用一段 Go 语言风格的伪代码,为你描述这个控制循环:

1
2
3
4
5
6
7
8
9
for {
实际状态 := 获取集群中对象X的实际状态(Actual State)
期望状态 := 获取集群中对象X的期望状态(Desired State)
if 实际状态 == 期望状态{
什么都不做
} else {
执行编排动作,将实际状态调整为期望状态
}
}

举例:
以Deployment 对象中 Replicas 字段的值为例。很明显,这些信息往往都保存在 Etcd 中。

接下来,以 Deployment 为例,我和你简单描述一下它对控制器模型的实现:

  1. Deployment 控制器从 Etcd 中获取到所有携带了“app: nginx”标签的 Pod,然后统计它们的数量,这就是实际状态;
  2. Deployment 对象的 Replicas 字段的值就是期望状态;
  3. Deployment 控制器将两个状态做比较,然后根据比较结果,确定是创建 Pod,还是删除已有的 Pod;

可以看到,一个 Kubernetes 对象的主要编排逻辑,实际上是在第三步的“对比”阶段完成的。这个操作,通常被叫作调谐(Reconcile)。这个调谐的过程,则被称作“Reconcile Loop”(调谐循环)或者“Sync Loop”(同步循环)。

Deployment控制器,控制ReplicaSet。ReplicaSet支持Pod的“水平扩展 / 收缩”和“滚动更新”

Deployment 同样通过“控制器模式”,来操作 ReplicaSet 的个数和属性,进而实现“水平扩展 / 收缩”和“滚动更新”这两个编排动作。

比如,把replicas这个值从 3 改成 4,那么 Deployment 所对应的 ReplicaSet,就会根据修改后的值自动创建一个新的 Pod。这就是“水平扩展”了;“水平收缩”则反之。

六、任务注册与任务发现

Kubernetes有后台管理界面可以注册任务。
以Deployment为例,注册时填写的字段有:应用名称、容器镜像、pod的数量、定义的Service。还有一些高级选项如下图所示。

https://blob.hixforever.com/20221223175901.png

Kubernetes还可以以编程的形式创建任务。可以使用client-go来进行接入。

七、任务形态

Kubernetes可以通过grpc和http两种形式创建任务。

  1. 使用管理控制台来创建任务时调用的是https处理任务。
  2. 使用命令行在创建任务时是调用的是grpc处理任务。

八、调度模型

kube-scheduler监听kube-apiserver,查询还未分配Node的Pod(即NodeName为空的Pod)。

kubernetes的线程模型是goroutine模型,goroutine调度器的流程本质上是一个生产-消费的流程。

它的调度模型与算法也是不断优化演变的,从最初的 G-M 模型、到 G-M-P 模型,从不支持抢占,到支持协作式抢占,再到支持基于信号的异步抢占。下面简单的介绍一下GMP模型:

G:我们可以看做是goroutine

P:逻辑处理器,他上面最重要的三个结构是runNext、local run queue、global run queue。

M:系统线程(真正处理任务的CPU)

首先G我们可以看做生产者,当一个goroutine被创建时,会包装成一个G,

  1. 此时生成的G会先看一下P的runnext上有没有G。
  2. 如果没有的话,刚创建的G则直接放在runnext上,
  3. 如果runNext上有G,看一下P上的local run queue有没有满
    1. 如果没有满,则将runnext上的G放到local run queue(本地队列中)
    2. 如果满了,则将runnext上的G+local run queue中一半的G拿出来组成一个链表放到global run queue(全局队列)中。

M我们可以看做消费者,

  1. 首先runtime.schedle检测schedtick%61==0?
  2. 如果schedtick%61==0,则直接从全局队列里获取队里第一个G执行。
  3. schedtick%61≠0,先看P中runnext是否有值
  4. 如果runnext中有值,M直接拿runnext中的G执行。
  5. 如果runnext中没有值,先去看local run queue中是否有G,
  6. local run queue有G则获取本地队列里的第一个G放到M上执行
  7. local run queue没有G,则去看global run queue中是否有G
  8. global run queue有G,则从global run queue中取(全局队列g总数 / gomaxprocs)+ 1个,但是不能超过128个G。将从全局队列中拿出来的G的第一个放到M上执行,剩下的放入到本地队列中。
  9. global run queue没有G,则去看其他P中是否有G,如果有的话,从其他P的local run queue中的队头开始,偷一半。取偷过来的最后一个放到M上执行,剩下的放入当前P的本地队列中。

下面我们在看一下goroutine 调度器是如何进行抢占式调度的。Go 程序启动时,运行时会去启动一个名为 sysmon 的 M(一般称为监控线程),这个 M 的特殊之处在于它不需要绑定 P 就可以运行(以 g0 这个 G 的形式)

sysmon 每 20us~10ms 启动一次,sysmon 主要完成了这些工作:

  1. 释放闲置超过 5 分钟的 span 内存;
  2. 如果超过 2 分钟没有垃圾回收,强制执行;
  3. 将长时间未处理的 netpoll 结果添加到任务队列;
  4. 向长时间运行的 G 任务发出抢占调度;
  5. 收回因 syscall 长时间阻塞的 P;

如果一个 G 任务运行 10ms,sysmon 就会认为它的运行时间太久而发出抢占式调度的请求。一旦 G 的抢占标志位被设为 true,那么等到这个 G 下一次调用函数或方法时,运行时就可以将 G 抢占并移出运行状态,放入队列中,等待下一次被调度。

九、调度策略

在第二大部分时,有介绍到对于Kubernetes的调度分为两个部分,第一部分Predicates,先对所有节点进行按照一定的策略进行过滤,第二部分Priority,对第一部分过滤出来的结果按照一定的策略进行打分,选择打分最高的节点进行调度。下面我来分别介绍一下这两部分的调度策略。

  1. Predicates中的调度策略有很多种,还可以自己编写自己的调度策略,这里我就简单介绍下面几种调度策略:
    • PodFitsHostPorts:检查是否有Host Ports冲突
    • PodFitsPorts:和PodFitsHostPorts相同
    • PodFitsResources:检查Node的资源是否充足,包括允许的Pod数量、CPU、内存、GPU个数以及其他的OpaqueIntResources。
    • HostName:检查pod.Spec.NodeName是否与候选节点一致。
    • MatchNodeSelector:检查候选节点的pod.Spec.NodeSelector是否匹配
    • NoVolumeZoneConflict:检查volume zone是否冲突
    • MatchinterPodAffinity:检查是否匹配Pod的亲和性要求
    • NoDiskConflict:检查是否存在Volume冲突,仅限于GCE PD、AWS EBS、Ceph RBD以及ISCSI。
    • PodToleratesNodeTaints:检查Pod是否容忍Node Taints。
    • CheckNodeMemoryPressure:检查Pod是否可以调度到MemoryPressure的节点上。
    • CheckNodeDiskPressure:检查Pod是否可以调度到DiskPressure的节点上。
    • NoVolumeNodeConflict:检查节点是否满足Pod所引用的Volume的条件。
  2. Priority中的调度策略有很多种,还可以自己编写自己的调度策略,这里我就简单介绍下面几种调度策略:
    • SelectorSpreadPriority:优先减少节点上属于同一个Service或Replication Controller的Pod数量
    • InterPodAffinityPriority:优先将Pod调度到相同的拓扑上(如同一个节点、Rack、Zone等)
    • LeastRequestedPriority:优先调度到请求资源少的节点上。
    • BalancedResourceAllocation:优先平衡各节点的资源使用。
    • NodePreferAvoidPodsPriority:alpha.kubernetes.io/preferAvoidPods 字段判断,权重为10000,避免其他优先级策略的影响。
    • NodeAffinityPriority:优先调度到匹配NodeAffinity的节点上。
    • TaintTolerationPriority:优先调度到匹配TaintToleration的节点上。
    • ServiceSpreadingPriority:尽量将同一个service的Pod分布到不同节点上,已经被SelectorSpreadPriority替代(默认未使用)
    • EqualPriority:将所有节点的优先级设置为1(默认未使用)
    • ImageLocalityPriority:尽量将使用大镜像的容器调度到已经下拉了该镜像的节点上(默认未使用)
    • MostRequestedPriority:尽量调度到已经使用过的Node上,特别适用于cluster-autoscaler(默认未使用)

Kubernetes是允许用户通过调度框架(Scheduling Framework)来自定义调度器。Scheduling Framework为我们定义了一些扩展点,用户能够实现扩展点定义的接口来定义本身的调度逻辑,并将扩展注册到扩展点上,调度器在执行调度流程时,会检查扩展plugin,如果有的话扩展plugin的话,则处理扩展plugin。

Scheduler 扩展点:

调度行为发生在一系列阶段中,这些阶段是通过以下扩展点公开的:

  • QueueSort:这些插件对调度队列中的悬决的 Pod 排序。 一次只能启用一个队列排序插件。
  • PreFilter:这些插件用于在过滤之前预处理或检查 Pod 或集群的信息。 它们可以将 Pod 标记为不可调度。
  • Filter:这些插件相当于调度策略中的断言(Predicates),用于过滤不能运行 Pod 的节点。 过滤器的调用顺序是可配置的。 如果没有一个节点通过所有过滤器的筛选,Pod 将会被标记为不可调度。
  • PreScore:这是一个信息扩展点,可用于预打分工作。
  • Score:这些插件给通过筛选阶段的节点打分。调度器会选择得分最高的节点。
  • Reserve:这是一个信息扩展点,当资源已经预留给 Pod 时,会通知插件。 这些插件还实现了 Unreserve 接口,在 Reserve 期间或之后出现故障时调用。
  • Permit:这些插件可以阻止或延迟 Pod 绑定。
  • PreBind:这些插件在 Pod 绑定节点之前执行。
  • Bind:这个插件将 Pod 与节点绑定。绑定插件是按顺序调用的,只要有一个插件完成了绑定,其余插件都会跳过。绑定插件至少需要一个。
  • PostBind:这是一个信息扩展点,在 Pod 绑定了节点之后调用。

十、监控与告警

Kubernetes 项目的监控体系是以 Prometheus 项目为核心的一套统一的方案。下面是Prometheus官网的架构图。

https://blob.hixforever.com/20221224094332.png

可以看到,Prometheus 项目工作的核心,是使用 Pull (抓取)的方式去搜集被监控对象的 Metrics 数据(监控指标数据),然后,再把这些数据保存在一个 TSDB (时间序列数据库,比如 OpenTSDB、InfluxDB 等)当中,以便后续可以按照时间进行检索。

有了这套核心监控机制, Prometheus 剩下的组件就是用来配合这套机制的运行。比如 Pushgateway,可以允许被监控对象以 Push 的方式向 Prometheus 推送 Metrics 数据。而 Alertmanager,则可以根据 Metrics 信息灵活地设置报警。当然, Prometheus 最受用户欢迎的功能,还是通过 Grafana 对外暴露出的、可以灵活配置的监控数据可视化界面。

Kubernetes中Prometheus 拉取的Metrics 数据的来源大致分为三种:

  1. 宿主机的监控数据。这部分数据的提供,需要借助一个由 Prometheus 维护的Node Exporter 工具。一般来说,Node Exporter 会以 DaemonSet 的方式运行在宿主机上。其实,所谓的 Exporter,就是代替被监控对象来对 Prometheus 暴露出可以被“抓取”的 Metrics 信息的一个辅助进程。
  2. Kubernetes 的 API Server、kubelet 等组件的 /metrics API。除了常规的 CPU、内存的信息外,这部分信息还主要包括了各个组件的核心监控指标。比如,对于 API Server 来说,它就会在 /metrics API 里,暴露出各个 Controller 的工作队列(Work Queue)的长度、请求的 QPS 和延迟数据等等。这些信息,是检查 Kubernetes 本身工作情况的主要依据。
  3. Kubernetes 相关的监控数据。这部分数据,一般叫作 Kubernetes 核心监控数据(core metrics)。这其中包括了 Pod、Node、容器、Service 等主要 Kubernetes 核心概念的 Metrics。

十一、认证和权限

Kubernetes使用Namespace来实现资源的隔离。

用户使用 kubectl、客户端库或构造 REST 请求来访问 Kubernetes API。 Kubernetes 服务账户(ServiceAccount)可以被鉴权访问 API。 当请求到达 API 时,它会经历多个阶段,如下图所示:

https://blob.hixforever.com/20221223203432.png

具体的权限控制流程如下:

  • 用户携带令牌或者证书给Kubernetes的api-server发送请求要求查询修改集群资源
  • Kubernetes开始认证。认证通过
  • Kubernetes查询用户的授权(在 Kubernetes 项目中,负责完成授权(Authorization)工作的机制,就是 RBAC)
  • 用户执行操作。过程中的一些操作(cpu、内存、硬盘、网络等….),利用准入控制来判断是否可以允许操作

对于API Server来说,可以使用Webhook 模式来实现自己的认证、鉴权和准入控制功能。也可以集成第三方的认证、鉴权和准入控制。

十二、数据存储

Kubernetes是使用etcd来进行数据存储的。

在 Kubernetes 项目中,一个 API 对象在 Etcd 里的完整资源路径,是由:Group(API 组)、Version(API 版本)和 Resource(API 资源类型)三个部分组成的。通过这样的结构,整个 Kubernetes 里的所有 API 对象,实际上就可以用如下的树形结构表示出来:

https://blob.hixforever.com/20221226143254.png

我们来看一下Kubernetes 是如何对 Resource、Group 和 Version 进行解析,从而在 Kubernetes 项目里找到 CronJob 对象的定义。

  1. Kubernetes 会匹配 API 对象的组。
  2. Kubernetes 会进一步匹配到 API 对象的版本号。
  3. Kubernetes 会匹配 API 对象的资源类型。

在前面匹配到正确的版本之后,Kubernetes 就知道,我要创建的原来是一个 /apis/batch/v2alpha1 下的 CronJob 对象。

十三、 以一个简单的任务为例子,解释整个系统的运作过程

以Kubernetes使用默认调度器,为一个新创建出来的Pod,寻找一个最合适的节点(Node)为例。

默认调度器会首先调用一组叫作 Predicate 的调度算法,来检查每个 Node。然后,再调用一组叫作 Priority 的调度算法,来给上一步得到的结果里的每个 Node 打分。最终的调度结果,就是得分最高的那个 Node。然后调度器会在Pod的spec.nodeName 字段填上调度结果的节点名字。到这里调度器对一个 Pod 的调度就完成了。