k8s_PaaS/原理及源码解析/Kubernetes调度机制.md
2020-04-19 12:35:19 +08:00

259 lines
11 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

## Kubernetes调度机制
### Kubernetes的资源模型与资源管理
所有跟调度和资源管理相关的属性都是属于 Pod 对象的字段,其中最重要的部分,就是 Pod 的 CPU 和内存配置,如下所示
~~~
apiVersion: v1
kind: Pod
metadata:
name: frontend
spec:
containers:
- name: db
image: mysql
env:
- name: MYSQL_ROOT_PASSWORD
value: "password"
resources:
requests:
memory: "64Mi"
cpu: "250m"
limits:
memory: "128Mi"
cpu: "500m"
- name: wp
image: wordpress
resources:
requests:
memory: "64Mi"
cpu: "250m"
limits:
memory: "128Mi"
cpu: "500m"
~~~
> 在Kubernetes 中
>
> - CPU称为 “可压缩资源” 可压缩资源不足时Pod只会“饥饿”不会退出
> - 内存称为“不可压缩资源”不可压缩资源不足时Pod会因为OOMOut-Of-Memory被内核杀掉
> - Pod 可以由多个 Container 组成,所以 CPU 和内存资源的限额,是要配置在每个 Container 的定义上的而Pod整体资源配置就由这些 Container 的配置值累加得到
**limits 和 requests** 的区别很简单在调度的时候kube-scheduler 只会按照 requests 的值进行计算。而在真正设置 Cgroups 限制的时候kubelet 则会按照 limits 的值来进行设置。
requests+limits 的做法其实是Borg的思想实际场景中大多数作业使用到的资源其实远小宇它所请求的资源限额基于这种假设用户可以声明一个相对小的requests值供调度器使用而Kubernetes 真正设置给容器 Cgroups 的,则是相对较大的 limits 值。
#### QoS 模型
> 三个级别,衔接上面
- **Guaranteed**Pod 里的每一个 Container 都同时设置了 requests 和 limits并且 requests 和 limits 值相等的时候,这个 Pod 就属于 Guaranteed 类别
~~~
apiVersion: v1
kind: Pod
metadata:
name: qos-demo
namespace: qos-example
spec:
containers:
- name: qos-demo-ctr
image: nginx
resources:
limits:
memory: "200Mi"
cpu: "700m"
requests:
memory: "200Mi"
cpu: "700m"
~~~
- **Burstable**:当 Pod 不满足 Guaranteed 的条件,但至少有一个 Container 设置了 requests。那么这个 Pod 就会被划分到 Burstable 类别
~~~
apiVersion: v1
kind: Pod
metadata:
name: qos-demo-2
namespace: qos-example
spec:
containers:
- name: qos-demo-2-ctr
image: nginx
resources:
limits
memory: "200Mi"
requests:
memory: "100Mi"
~~~
- **BestEffort**:如果 Pod 既没有设置 requests也没有设置 limits那么它的 QoS 类别就是 BestEffort
~~~
apiVersion: v1
kind: Pod
metadata:
name: qos-demo-3
namespace: qos-example
spec:
containers:
- name: qos-demo-3-ctr
image: nginx
~~~
**QoS 划分的主要应用场景是当宿主机资源紧张的时候kubelet 对 Pod 进行 Eviction即资源回收时需要用到的。**当Eviction发生时kubelet 具体会挑 Pod 进行删除操作,按如下级别
**BestEffort < Burstable < Guaranteed**
并且Kubernetes 会保证只有当 Guaranteed 类别的 Pod 的资源使用量超过了其 limits 的限制,或者宿主机本身正处于 Memory Pressure 状态时Guaranteed 的 Pod 才可能被选中进行 Eviction 操作。
#### cpuset 的设置
> 一个实际生产中非常有用的特性,衔接上面
>
> 在使用容器时,可以通过设置 cpuset 把容器绑定到某个 CPU 的核上,而不是像 cpushare 那样共享 CPU 的计算能力这样CPU之间进行上下文切换的次数大大减少容器里应用的性能会得到大幅提升
实现方法
- Pod 必须是 Guaranteed 的 QoS 类型
- 将 Pod 的 CPU 资源的 requests 和 limits 设置为同一个相等的整数值
如下例子
~~~
spec:
containers:
- name: nginx
image: nginx
resources:
limits:
memory: "200Mi"
cpu: "2"
requests:
memory: "200Mi"
cpu: "2"
~~~
这样该Pod就会绑定在2个独占的CPU核上具体是哪两个CPU由kubelet 分配
基于上面的情况建议将DaemonSet亦或者类似的的 Pod 都设置为 Guaranteed 的 QoS 类型,否则一旦被资源紧张被回收,又立即会在宿主机上重建出来,这样资源回收的动作就没有意义了
### Kubernetes默认的调度策略
![1587049585883](assets/1587049585883.png)
> 调度机制的工作原理示意图
**默认的几种调度策略**
1. **第一种类型GeneralPredicates**:这一组过滤规则,负责的是最基础的调度策略,计算的就是宿主机的 CPU 和内存资源等是否够用。
2. **第二种类型,与 Volume 相关的过滤规则**:这一组过滤规则,负责的是跟容器持久化 Volume 相关的调度策略。
3. **第三种类型,是宿主机相关的过滤规则**:这一组规则,主要考察待调度 Pod 是否满足 Node 本身的某些条件。比如PodToleratesNodeTaints负责检查的就是我们前面经常用到的 Node 的“污点”机制。
4. **第四种类型,是 Pod 相关的过滤规则**:这一组规则,跟 GeneralPredicates 大多数是重合的。而比较特殊的,是 PodAffinityPredicate。这个规则的作用是检查待调度 Pod 与 Node 上的已有 Pod 之间的亲密affinity和反亲密anti-affinity关系
**在具体执行的时候, 当开始调度一个 Pod 时Kubernetes 调度器会同时启动 16 个 Goroutine来并发地为集群里的所有 Node 计算 Predicates最后返回可以运行这个 Pod 的宿主机列表。**
> **Goroutine**go语言的“线程”比传统线程对资源的占用更合理
在 Predicates 阶段完成了节点的“过滤”之后Priorities 阶段的工作就是为这些节点打分。这里打分的范围是 0-10 分,得分最高的节点就是最后被 Pod 绑定的最佳节点。
Priorities 里最常用到的一个打分规则,是 LeastRequestedPriority。它的计算方法可以简单地总结为如下所示的公式
~~~
score = (cpu((capacity-sum(requested))10/capacity) + memory((capacity-sum(requested))10/capacity))/2
~~~
这个算法实际上就是在选择空闲资源CPU 和 Memory最多的宿主机。
与 LeastRequestedPriority 一起发挥作用的,还有 BalancedResourceAllocation。它的计算公式如下所示
~~~
score = 10 - variance(cpuFraction,memoryFraction,volumeFraction)*10
~~~
每种资源的 Fraction 的定义是 Pod 请求的资源 / 节点上的可用资源。而 variance 算法的作用,则是计算每两种资源 Fraction 之间的“距离”。而最后选择的,则是资源 Fraction 差距最小的节点。
**也就是调度完成后,所有节点里各种资源分配最均衡的那个节点,从而避免一个节点上 CPU 被大量分配、而 Memory 大量剩余的情况**
此外,还有 NodeAffinityPriority、TaintTolerationPriority 和 InterPodAffinityPriority 这三种 Priority。这里就不一一介绍了除了默认的调度策略还有很多默认不会开启的策略可以通过为 kube-scheduler 指定一个配置文件或者创建一个 ConfigMap ,来配置哪些规则需要开启、哪些规则需要关闭。并且,还可以通过为 Priorities 设置权重,来控制调度器的调度行为。
### 调度器的优先级与强制机制
工作中我们也需要一些Pod有优先级和抢占机制比如Pod调度失败后会被“搁置”知道Pod被更新或集群状态发生变化调度器才会对Pod进行重新调度我们希望高优先级的Pod调度失败后不被搁置而“挤走”某些低优先级的。
1.11版本后这个特性已经是Beta用法如下
先在Kubernetes里提交一个PriorityClass如下:
~~~
apiVersion: scheduling.k8s.io/v1beta1
kind: PriorityClass
metadata:
name: high-priority
value: 1000000
globalDefault: false
description: "This priority class should be used for high priority service pods only."
~~~
> 这个YAML文件名为high-priority
>
> **value**值越大代表优先级越高最大1000000000/10亿
>
> **globalDefault**声明使用该PriorityClass的Pod拥有值为1000000 的优先级没有声明则为0。 true 的话,那就意味着这个 PriorityClass 的值会成为系统的默认值
然后我们在Pod就可以声明使用如下
~~~
apiVersion: v1
kind: Pod
metadata:
name: nginx
labels:
env: test
spec:
containers:
- name: nginx
image: nginx
imagePullPolicy: IfNotPresent
priorityClassName: high-priority
~~~
> 提交后Kubernetes 的 PriorityAdmissionController 就会自动将这个 Pod 的 spec.priority 字段设置为 1000000
调度器里维护着一个调度队列,高优先级的 Pod 就可能会比低优先级的 Pod 提前出队,从而尽早完成调度过程。**这个过程,就是“优先级”这个概念在 Kubernetes 里的主要体现。**
而当一个高优先级的 Pod 调度失败的时候,调度器的抢占能力就会被触发。这时,调度器就会试图从当前集群里寻找一个节点,使得当这个节点上的一个或者多个低优先级 Pod 被删除后,待调度的高优先级 Pod 就可以被调度到这个节点上。**这个过程,就是“抢占”这个概念在 Kubernetes 里的主要体现。**
### kubelet
![img](https://static001.geekbang.org/resource/image/91/03/914e097aed10b9ff39b509759f8b1d03.png)
**kubelet 调用下层容器运行时的执行过程,并不会直接调用 Docker 的 API而是通过一组叫作 CRIContainer Runtime Interface容器运行时接口的 gRPC 接口来间接执行的。**为什么由CRI来间接执行是关乎历史原因搭配CRI之后Kubernetes 以及 kubelet 本身的架构,就可以用如下所示的一幅示意图来描述。
![img](https://static001.geekbang.org/resource/image/51/fe/5161bd6201942f7a1ed6d70d7d55acfe.png)
可以看到,当 Kubernetes 通过编排能力创建了一个 Pod 之后,调度器会为这个 Pod 选择一个具体的节点来运行比如创建一个Pod。此时kubelet 实际上就会调用一个叫作 GenericRuntime 的通用组件来发起创建 Pod 的 CRI 请求。如果使用容器项目是 Docker 的话,那么负责响应这个请求的就是一个叫作 dockershim 的组件。它会把 CRI 请求里的内容拿出来,然后组装成 Docker API 请求发给 Docker Daemon。
**CRI 这个接口的定义**如下图
![1587263685501](assets/1587263685501.png)
- 第一组,是 RuntimeService。它提供的接口主要是跟容器相关的操作。比如创建和启动容器、删除容器、执行 exec 命令等等。
- 而第二组,则是 ImageService。它提供的接口主要是容器镜像相关的操作比如拉取镜像、删除镜像等等。
**CRI 设计的一个重要原则,就是确保这个接口本身,只关注容器,不关注 Pod**
**第一**Pod 是 Kubernetes 的编排概念,而不是容器运行时的概念。所以,我们就不能假设所有下层容器项目,都能够暴露出可以直接映射为 Pod 的 API。
**第二**,如果 CRI 里引入了关于 Pod 的概念,那么接下来只要 Pod API 对象的字段发生变化,那么 CRI 就很有可能需要变更。而在 Kubernetes 开发的前期Pod 对象的变化还是比较频繁的,但对于 CRI 这样的标准接口来说,这个变更频率就有点麻烦了。
所以,在 CRI 的设计里,并没有一个直接创建 Pod 或者启动 Pod 的接口。