> 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-er.md).

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

### 背景

之前我们写过一篇介绍我们基于 Kubernetes 扩展调度机制研发的基于实时负载的调度插件，里面介绍了 Kubernetes 调度器流程以及调度器扩展方案，这个版本基本解决了我们刚开始大批量上容器时遇到的节点负载不均衡问题。上线前集群 CPU 使用率 40% 左右，业务高峰期节点 CPU 使用率最高可以达到 80%，最低的只有20%左右，导致高负载节点上的应用延迟明显增加，我们值班人员需要经常手动驱逐高负载节点上的 Pod，给业务和容器平台增加了很多运维成本。扩展调度器上线后我们又同步上线了重调度服务用于做高负载节点 Pod 自动驱逐，使得业务高峰期集群内节点 CPU 使用率差距不超过 30%，很少出现人工驱逐 Pod 的情况。

老版本扩展调度器在经过多个生产环境集群一年多时间线上使用以后也暴漏出了一些问题，比如调度热点、没有考虑节点历史资源水位、无法灵活扩展其他指标等问题。线上环境也出过几次因为这些原因导致的调度问题：

* 场景一：新上线的节点因为资源使用率非常低，开启调度以后该节点短时间调度了很多 Pod 出现调度热点问题，导致该节点上的部分 Pod 拉镜像很慢以及节点负载迅速升高，最终导致业务应用出现了明显抖动。
* 场景二：不同集群硬件资源配置、部署的应用不同导致集群对应的集群各维度资源画像差异较大，扩展调度器需要灵活的根据集群资源画像数据进行调度配置调整。大部分公用集群 CPU 资源波动较大调度配置需要调整为以 CPU 资源作为主要参考因子；某些 PAAS 服务集群需要 CPU + Memory 同时参与作为调度因子；不同的集群各种因子的权重设置也不同。这些需求老版本扩展调度器都很难满足。

基于遇到的各种问题我们做了很多调研和思考，最终决定要重构现有的扩展调度器插件架构。

### 落地过程

在调研过一段时间后发现，腾讯开源的 [crane-scheduler](https://github.com/gocrane/crane-scheduler) 项目比较满足我们的需求，这个项目使用的也是 Kubernetes 官方推荐的调度器 Framework 机制实现的基于实时负载的调度器插件、考虑了热点调度问题、通过自定义配置文件灵活更新资源指标。最终我们我们内部讨论后确定切换到 crane-scheduler 项目，后续的一些功能基于 crane-scheduler 做二次开发。

#### Crane-scheduler 源码分析

**架构图**

<figure><img src="/files/iwAIjAWQPszIeYm4S4y6" alt=""><figcaption><p>crane-scheduler架构图</p></figcaption></figure>

Crane-scheduler 主要包含两个组件: Crane-scheduler 和 Crane-scheduler-controller，Crane-scheduler-controller 负责从 kube-apiserver 和 Prometheus 获取调度事件以及节点监控指标并更新到节点注解；Crane-scheduler 会监听 kube-apiserver 当发现未调度的 pod 时会进入该 pod 调度流程，最终将计算出的节点更新到该 pod 的 nodeName 字段，这个 pod 就完成了调度。

**Crane-scheduler-controller**

Crane-scheduler-controller 代码流程图如下所示：

<figure><img src="/files/DFs6VpdC6vrdAhgag5dw" alt=""><figcaption><p>crane-scheduler-controller流程图</p></figcaption></figure>

Crane-scheduler-controller 中启动了两个 Controller ：event Controller 和 node Controller。event Controller 会通过 kube-apiserver 监听 event 资源事件，当收到的 event 事件为调度事件时会将事件中的重要信息推入 heap 中。node Controller 启动时会根据 policy.yaml 配置中 syncPolicy 段配置间歇值初始化多个定时器，每个定时器到期以后会遍历所有节点向工作队列中放入指标名和节点名；node Controller 会实时消费该工作队列，然后从 Prometheus 和 heap 中拿到该节点监控指标以及调度热点值（热点值根据 policy.yaml 中的hotValue配置计算），最后更新到节点注解上。

```
  ## 需要获取的指标名及间隔
  syncPolicy:
    ##cpu usage
    - name: cpu_usage_avg_5m
      period: 1m
    - name: cpu_usage_max_avg_1h
      period: 15m
    - name: cpu_usage_max_avg_1d
      period: 3h
    ##memory usage
    - name: mem_usage_avg_5m
      period: 3m
    - name: mem_usage_max_avg_1h
      period: 15m
    - name: mem_usage_max_avg_1d
      period: 3h
  
  ## 调度热点值计算配置
  hotValue:
    - timeRange: 5m
      count: 5
    - timeRange: 1m
      count: 2
```

**Crane-scheduler**

Crane-scheduler 中的默认启动的 dynamic 插件为基于实时负载的扩展调度器插件主要实现了调度流程中 Filter 和 Score 扩展点。

Filter 扩展点逻辑主要是根据 policy.yaml 配置中 predicate 段配置对所有参与调度的节点进行检查，如果该节点对应的指标超过阈值以后该节点会被过滤掉，无法参与下一阶段调度。

Score 扩展点会使用 priority 段配置根据 CPU 和内存使用率指标进行节点打分，打分公示为：sum(usage \* weight \* 100) / sum(weight) - (hotvalue \* 10)。该插件的打分值会加入到 kube-scheduler 默认所有的打分插件值之和，最终的打分值最高的节点就是 pod 调度节点。

```
  predicate:
    ##cpu usage
    - name: cpu_usage_avg_5m
      maxLimitPecent: 0.65
    - name: cpu_usage_max_avg_1h
      maxLimitPecent: 0.75
    ##memory usage
    - name: mem_usage_avg_5m
      maxLimitPecent: 0.65
    - name: mem_usage_max_avg_1h
      maxLimitPecent: 0.75

  priority:
    ##cpu usage
    - name: cpu_usage_avg_5m
      weight: 0.5
    - name: cpu_usage_max_avg_1h
      weight: 0.4
    - name: cpu_usage_max_avg_1d
      weight: 0.5
    ##memory usage
    - name: mem_usage_avg_5m
      weight: 0.1
    - name: mem_usage_max_avg_1h
      weight: 0.2
    - name: mem_usage_max_avg_1d
      weight: 0.3
```

#### Crane-scheduler 优化

**1、优化 Crane-scheduler 表达式处理逻辑兼容非使用率类型指标**

**问题**

我们生产环境中之前遇到过几次因为节点平均负载过高导致业务应用延迟增大的案例，所以我们在调研之初就有考虑将节点平均负载加入到调度指标中。虽然 Crane-scheduler 支持通过配置文件灵活的增加指标，但是在增加平均负载指标过程中我们还是遇到了一些问题。默认 Crane-scheduler 在部署时会在 Prometheus 增加一些自定义的指标表达式，这些表达式都会乘以100，程序里面获取表达式的时候会在拼接 promQL 的时候除以100。这种逻辑在一些使用率类型的指标场景下是没有问题的，但是平均负载这种非使用率类型的指标使用起来就会有些歧义，

**优化方案**

我对 Prometheus 表达式和程序中的 promQL 去处了百分数换算的逻辑，加上了平均负载的指标，后续其他非使用率指标也可以很方便的配置。

**注意**：修改完 prometheus 指标一天以后再更新 Crane-scheduler-controller 代码，否则会导致最近一天/一小时相关的指标大于1，从而导致大部分节点在调度过程会被过滤，可能造成没有节点可以调度。

**2、基于 K8S 事件处理机制优化调度热点值更新频率**

**问题**

本次扩展调度器架构优化一个最主要的目的就是解决调度热点问题，所以在测试环境中测试中我尤其关注调度热点的数据。Crane-scheduler-controller 中节点调度热点值是跟着监控指标一起更新的，所以热点值的更新频率主要取决于监控指标最小更新间隔，而监控指标最小间隔默认是 3m ，为了减少热点值的更新频率我刚开始把最小更新间隔改为了 1m 。但是在测试过程中发现还是会经常存在某个时间段一个节点连续调度 3 到 4 个 pod的现象，这种现象在生产环境上可能会导致一个节点负载突然升高。

**优化方案**

为了继续进一步优化调度热点数据，我最终 Crane-scheduler-controller 在收到节点调度事件时增加主动触发更新该节点调度热点值，以达到调度热点值实时更新的结果。最终测试过程中发现同一时间点一个节点连续调度 pod 数量不超过 2 个。

<figure><img src="/files/ISRVIivRo8t6DBOlBfx8" alt=""><figcaption></figcaption></figure>

### 落地效果

**K8S 原生调度器 VS 新版本扩展调度器**

以下是我们一个使用原生 kube-scheduler 组件的生产集群上线扩展调度器前后节点均衡度数据。

**集群CPU使用率均衡度**

上线前节点 CPU 使用率最高值为 80% 最低值为 10%，上线后节点 CPU 使用率最高值为 50% 最低值为 20%。

上线前均衡度方差数据平均值是 0.7 最高值1.5，上线后方差数据平均值为 0.2 最高值为 0.5，集群节点间 CPU 资源均衡度**提高了 70%**。

<figure><img src="/files/fMpFzJI7HbuBqgjvqufi" alt=""><figcaption></figcaption></figure>

**集群 CPU + Memory 使用率均衡度**

上线前节点 CPU + Memory 使用率最高值为 110% 最低值为 30%，上线后节点 CPU + Memory 使用率最高值为 70% 最低值为 50%。

上线前均衡度方差数据平均值是 1.3，最高值 2.7，上线后方差数据平均值为0.4，最高值 0.6，集群节点间 CPU + Memory 资源均衡度**提高了 69%**。

<figure><img src="/files/ocoogrHjaw8TkdC60GAw" alt=""><figcaption></figcaption></figure>

新版本扩展调度器上线后节点 CPU 使用率峰值最大降低了 30%，节点 CPU + Memory 使用率峰值最大降低了 40%，使得节点负载带来的应用延迟和平台运维成本大幅度降低。

**成本价值**

基于原生 K8S 调度器集群 CPU 使用率达到 30% 就需要进行扩容了，扩展调度器上线后集群 CPU 使用率可以达到 50% 。当前生产容器集群物理机大概 500 台左右，全部集群上线后在保证应用性能不受影响的情况下预计可以节省 80 台物理机。

### 存在的问题

Q1：当前调度器插件参考的监控指标主要都是节点维度的，没有参考被调度 pod 实际可能使用的资源，会导致调度计算出来的节点虽然是最优的，但是对于当前pod来说不一定是最优的。例如：当前集群平均节点CPU使用率都比较高，当某个CPU实际使用量非常大的pod调度时，当前调度器计算出来的节点不一定是CPU是调用率最低的节点而是一个综合各个指标以后比较优的节点，这样就可能会导致调度上去以后这个节点CPU使用率被打到非常高的情况出现。

Q2: 针对批量调度的任务类型 pod 还是会出现比较严重的热点调度的情况。
