> For the complete documentation index, see [llms.txt](https://marswang.gitbook.io/blog/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://marswang.gitbook.io/blog/k8s/k8s-shi-jian-kuo-zhan-kubernetes-diao-du-qi-yi.md).

# k8s实践：扩展 Kubernetes 调度器(一)

### 背景

在我们公司内部使用基于 Kubernetes 容器编排项目进行业务容器化落地过程中，发现生产容器集群中经常会出现节点之前调度严重不均衡的情况，当业务高峰期时集群维度 CPU 使用率超过 30% 以后，CPU 使用率高的节点可以达到 80% 以上，而 CPU 使用率低的节点只能达到 20% 左右，落在 CPU 使用率高的节点上的应用延迟会有明显增高，导致高峰期经常需要大量手动干预调度行为，增加了容器环境运维成本，导致这个问题原因一个是我们快速容器化过程中，根据应用类型粗略的设置了对应的 Pod 规格，不同的 Pod request配置是一致的，但是资源实际使用量差距比较大；另一个原因是因为调度器无法感知到节点的实际负载导致的。优化 request 配置这个方案我们也讨论过，但是由于修改应用的 request 会导致应用重启对在线业务影响较大并且不同时间段业务资源使用不相同，所以通过修改 request 对我们收益并不大，最终我们还是决定通过扩展调度器的方式来解决这个问题。

kube-scheduler是 kubernetes 核心组件之一，主要负责集群 pod 的调度功能，调度过程主要是根据特定的调度算法和策略将 pod 调度到最优的工作节点上，从而更加合理、更加充分的利用集群的资源，这也是我们使用 Kubernetes 最重要的原因之一。

### 调度流程

kube-scheduler 是 kubernetes 的调度器，它的内部处理逻辑就是启动之后一直监听 kube-apiserver 获取PodSpec.NodeName 为空的 Pod，然后根据一系列的调度算法和调度策略为 Pod 分配合适的 Node 节点。

调度主要分为以下几个部分：

* 预选阶段（ Predicates ）：根据所有预选算法过滤掉不满足当前 Pod 的节点；
* 优选阶段（ Priorities ）：根据所有优选算法对节点进行打分并计算出最终优先级；
* 绑定阶段（ Bind ）：根据优选阶段的节点优先级选定最终节点，并将节点更新到 Pod 资源；

调度流程的示意图如下所示：

<figure><img src="https://3164098718-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F5MQ8MEJH8N9Wi6h66P8a%2Fuploads%2FD4MsOy8o25lBAkqvLXzv%2Fkube-scheduler%E8%B0%83%E5%BA%A6%E6%B5%81%E7%A8%8B.png?alt=media&amp;token=4dc25f0a-2d25-4397-bbd6-f0ef6f904a4b" alt=""><figcaption><p>kube-scheduler调度流程</p></figcaption></figure>

`Predicates` 阶段常用算法：

* Volume 相关： 云厂商 volume 检查、pod volume冲突检查；
* 节点压力相关： 内存、磁盘、PID等；
* 通用项检查： request资源、hostname、hostPort、nodeSelector等；
* 节点污点及容忍检查；
* volumeBinding：检查 PV 对应节点亲和性；
* Pod 之间亲和性、反亲和性检查；

`Priorities` 阶段常用算法：

* SelectorSpreadPriority：所属同一个 Service、RS、StatefulSet 在当前节点上分布pod数，数量越少分数越高；
* InterPodAffinityPriority：Pod 亲和性选择；
* LeastRequestedPriority：空闲 request 资源越多优先级越高；
* resource request 均衡度：request 申请的 cpu、memory 均衡度越高优先级越高；
* 节点亲和性、污点容忍优先级；
* ImageLocalityPriority：本地存在镜像时，镜像越大优先级越高；

`Predicates` 阶段首先遍历全部节点，强制过滤掉不满足条件的节点， 这一阶段输出的所有满足要求的 Node 作为 `Priorities` 阶段输入，如果所有节点都不满足的话当前 Pod 将会一直处于 Pending 状态，直到有节点满足条件，在这期间调度器会不断重试。

`Priorities` 阶段是对 `Predicates` 阶段通过的节点列表进行优先级打分，所有算法计算完成以后，最终选出优先级最高的节点进行调度。

### 调度器扩展

kubernetes 社区常见的 kube-scheduler 扩展方法有：

* 调度扩展（scheduler-extender）：这个方案可以和原生调度器兼容，就是通过 Webhook 方式调用一个实现了 `filter` 和 `score` 阶段的 web 程序，然后对调度过程进行扩展，但是该方案已经计划在 kubernetes v1.24 版本被移除了。
* 调度框架（scheduling Framework）：该方案是在 kubernetes v1.15 版本引入了一个可插拔的调度框架，是的扩展调度器更加容易，调度框架向原生调度器中添加了一组插件化的 API，使得现有调度程序更加易维护的基础上，其它大部分调度函数也能够通过该方式更加方便的进行扩展，后续该方式也是社区推荐的扩展调度器方式。

这里主要讲一下调度框架的实现细节。

#### 调度框架

调度框架定义了一组扩展点，用户可以实现对应扩展点的接口来增加自己的调度逻辑，调度框架会自动将插件注册到扩展点上，调度器在调度过程中遇到对应的扩展点时，将调用用户注册的扩展。调度框架在预留扩展点时，都是有特定的目的，有些扩展点上的扩展可以改变调度程序的决策方法，有些扩展点上的扩展只是发送一个通知。

**扩展点**

下图展示了调度框架中的调度上下文及其中的扩展点，一个插件可以注册多个扩展点，以便执行更复杂的有状态任务。

<figure><img src="https://3164098718-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F5MQ8MEJH8N9Wi6h66P8a%2Fuploads%2FDsjvjfse9Z7RTSYi6SQf%2Fscheduling-framework-extensions.png?alt=media&amp;token=ee40f07d-0826-472a-a0fc-c5de3016718b" alt=""><figcaption><p>kube-scheduler扩展点</p></figcaption></figure>

1. `QueueSort`：用于对调度队列中的 Pod 进行排序，以决定先调度哪个 Pod，`QueueSort` 扩展点本质上只需要实现一个方法 `Less(Pod1, Pod2)` 用于比较两个 Pod 谁先调度，同一时间点只能有一个 `QueueSort` 插件生效。
2. `Pre-filter`：用于对 Pod 的信息进行预处理，或者检查一些集群或 Pod 必须满足的前提条件，如果 `pre-filter` 返回了 error，则调度过程终止。
3. `Filter` ：用于排除那些不能运行该 Pod 的节点，对于每一个节点，调度器将按顺序执行 `filter` 扩展；如果任何一个 `filter` 将节点标记为不可选，则余下的 `filter` 扩展将不会被执行。调度器可以同时对多个节点执行 `filter` 过程。
4. `Post-filter` ：在 Filter 阶段后调用，但仅在该 Pod 没有可行的节点时调用。 调度器按顺序执行`Post-filter`扩展；如果任何 `PostFilter` 插件标记节点为可调度， 则其余的插件不会调用。典型的 `PostFilter` 实现是抢占，试图通过抢占其他 Pod 的资源使该 Pod 可以调度。
5. `PreScore`：用于执行一些打分阶段的前置工作，即生成一个共享状态供 `Score` 阶段使用。如果 `PreScore`阶段返回错误，则调度周期终止。
6. `Score`：用于为所有通过 filter 阶段的节点进行打分，调度器将针对每一个节点调用 `Sore` 扩展，评分结果是一个范围内的整数。在 `normalizeScore` 阶段，调度器将会把每个 `Score` 扩展对具体某个节点的评分结果和该插件的权重合并起来，作为最终评分结果。
7. `NormalizeScore` ：在调度器对节点进行最终排序之前修改每个节点的评分结果，注册到该扩展点的扩展在被调用时，将获得同一个插件中的 `Score` 扩展的评分结果作为参数，调度框架每执行一次调度，都将调用所有插件中的一个 `NormalizeScore` 扩展一次。
8. `Reserve` ：是一个通知性质的扩展点，有状态的插件可以使用该扩展点来获得节点上为 Pod 预留的资源，该事件发生在调度器将 Pod 绑定到节点之前，目的是避免调度器在等待 Pod 与节点绑定的过程中调度新的 Pod 到节点上时，发生实际使用资源超出可用资源的情况。（因为绑定 Pod 到节点上是异步发生的）。这是调度过程的最后一个步骤，Pod 进入 `Reserved` 阶段以后，要么在绑定失败时触发 Unreserve 扩展，要么在绑定成功时，由 `Post-bind` 扩展结束绑定过程。
9. `Permit` ：用于阻止或者延迟 Pod 与节点的绑定。`Permit` 插件可以做下面三件事中的一项：
   * approve（批准）：当所有的 `permit` 插件都 approve 了 Pod 与节点的绑定，调度器将继续执行绑定过程
   * deny（拒绝）：如果任何一个 `permit` 扩展 deny 了 Pod 与节点的绑定，Pod 将被放回到待调度队列，此时将触发 `Unreserve` 扩展
   * wait（等待）：如果一个 `permit` 扩展返回了 wait，则 Pod 将保持在 permit 阶段，直到被其他扩展 approve，如果超时事件发生，wait 状态变成 deny，Pod 将被放回到待调度队列，此时将触发 Unreserve 扩展
10. `Pre-bind` ：用于在 Pod 绑定之前执行某些逻辑。例如，`pre-bind` 扩展可以将一个基于网络的数据卷挂载到节点上，以便 Pod 可以使用。如果任何一个 `pre-bind` 扩展返回错误，Pod 将被放回到待调度队列，此时将触发 Unreserve 扩展。
11. `Bind` ：用于将 Pod 绑定到节点上：
    * 只有所有的 pre-bind 扩展都成功执行了，bind 扩展才会执行
    * 调度框架按照 bind 扩展注册的顺序逐个调用 bind 扩展
    * 具体某个 bind 扩展可以选择处理或者不处理该 Pod
    * 如果某个 bind 扩展处理了该 Pod 与节点的绑定，余下的 bind 扩展将被忽略
12. `Post-bind` ：是一个通知性质的扩展点：
    * Post-bind 扩展在 Pod 成功绑定到节点上之后被动调用
    * Post-bind 扩展是绑定过程的最后一个步骤，可以用来执行资源清理的动作
13. `Unreserve` ：是一个通知性质的扩展点，如果为 Pod 预留了资源，Pod 又在被绑定过程中被拒绝绑定，则 unreserve 扩展将被调用。Unreserve 扩展应该释放已经为 Pod 预留的节点上的计算资源。在一个插件中，reserve 扩展和 unreserve 扩展应该成对出现。

### 落地思路

我们在扩展调度器过程中是使用调度框架实现的，通过扩展 `Filter` 扩展点对高负载节点进行了过滤以及扩展 `Socre` 扩展点根据节点实际内存和 CPU 使用率进行了打分，使得集群节点之间负载更加均衡。通过生产环境落地该扩展调度器使得我们集群层面 CPU 使用率从 30% 提高到 50% 并且手动干预调度频率减少了 80% 左右。

由于篇幅限制，本篇文章主要向大家介绍调度器流程及扩展方案，后续再从代码和架构方面分享扩展调度器的实现细节。
