「调度器」让 k8s 能够根据 node 节点 cpu/memory 负载情况而优化调度 pod 的插件

7,259 阅读9分钟

我是 LEE,老李,一个在 IT 行业摸爬滚打 16 年的技术老兵。

事件背景

随着公司的 k8s 中 Node 节点越来越多,Pod 数量奔着单 cluster 50K 的量级冲了。单 cluster 中这个数量虽然并不是非常惊人,但是很多日常的忽略的小问题,现在逐渐都是让人头疼的问题。目前因为 k8s 中存在业务种类多种多样,各种应用的负载压力和资源使用情况非常不一致,原本我们以为这么多 Pod 之间资源消耗可以起到“削峰填谷”的样子,而事实情况是很多 k8s node 节点会时不时 CPU 接近 90,内存容量只剩下 5G 不到。此时如果还有 pod 调度到这样的 k8s node 上,后面的故事,我想很多小伙伴都能自己脑补出来那种惨状,这个非常吃力的 k8s node 节点终于是因为承受不住这样的压力,已经运行的 pod 就会出现服务质量下降或者 OOM 后被驱逐。

面对我们需要一个在 pod 调度的时候不光只看 request values 的能力,还需要将 k8s node 节点 cpu/memory 的负载也考虑进来,让 pod 在一个最合理的环境中运行。最后实现 k8s node 之间 cpu/memory 使用负载趋近于“相同”。

在这样美好的愿望下,我参与了团队内部的 k8s scheduler 调研、评估和少量测试。 今天我就提一个我觉得整体架构比较喜欢的,也比较合理的 scheduler。

我的想法

调研别人的 scheduler,还是自研 scheduler 都是我的考虑选项,目前自研 scheduler 门槛被 k8s 小组压到了很低,基本一个会 golang 的小伙伴看几个小时后,就能写一个不错的 scheduler,但是这个并不是我在意的点。我在意的问题反而是如何让定制 scheduler 有通用性,让这个 scheduler 能够在 k8s 版本之间升级中,长期使用,最小代价。其次整体风险可控,能有一个“兜底”策略能够保证 scheduler 失效后使用原有的 k8s 的调度策略。

pod 运行起来,实现应用交付,与 pod 运行状态是不是最优是“两件事”,前者才是最重要的,后者是锦上添花。而后者如果做好了,能够很大的程度优化资源配置,提高资源的生产效率,必经 k8s node 节点都是成本。

根据上面的思路,我调研多加云厂商的 scheduler,也研究了他们的架构。有一家的 scheduler 让我为之眼亮,并不是他们的代码质量多好,而是 如下几点 吸引了我:

  1. 足够轻。这个非常重要,以为这个没有太多入侵,可以随来随走。好根据自己情况 diy。
  2. 官方标准。用的 scheduler framework, 这个意味是 k8s 框架原生技术(1.19 进入了 stable),风险小。(时间久了知道一个有规范的代码框架有多重要)
  3. 插件化。能够深入研究 k8s 架构的小伙伴必经不多,对于我来说投入人力去构建一个完成 scheduler 和周边生态是完全不可控的(违背第 2 点)。功能够“微服务化”,快进快出的开发方式,每个 plugin 的实现自己固定小功能。既保证输出效果,同时可以根据 cluster 实际运行情况,编排自己要的调度功能。
  4. 易淘汰。意味着这个模块要能装得上系统,也要能轻而易举地的卸载掉。因为任何代码都有它的历史使命,一旦存在原因消失,那么它也失去了运行的意义。小伙伴可以想想,自己产线上有多少“祖传”代码下不掉,而大量时间在翻系统。

说了这么多,那么这个神秘的 scheduler 到时是谁?

好,它是 crane-scheduler,出自腾讯云。

github: github.com/gocrane/cra…

架构解析

scheduler架构

唉,不是讲解 scheduler 架构吗?怎么贴上来了一大堆乌七八糟的东西,这些都有关系吗?我想说:“有,都有关系!!”。如果没有这边的系统那么调度意义就不存在了。

世界观

说到这里,我们先用怀疑态度一起学习下 crane-scheduler 整个系统的世界观。

Crane-scheduler

角色:【智者】

调度核心,调度功能的实现,直接影响 pod 调度结果。算法很轻量,主要是实现过滤不符合条件的 node(cpu/memory 超过阈值)。随后把计算结果交给 k8s scheduler 内其他模块继续处理。

Node-annotator

角色:【搬砖的】

从 tsdb 中读取的数据,根据设置的公式,每隔一段时间把这些值写到 Node CRD 中的 metadata/annotations。 它也只干这样一件事情,没有状态。 大白话说:读取 metrics --> 根据公式计算--> 写入 Node(CRD 资源) metadata/annotations。

Prometheus/telegraf

角色:【监工】

监控系统,监控 k8s node 节点上的 cpu/memory 状态值,并记录到 tsdb 中。不一定要这个方案,我这边使用的是:node_exporter + thanos。因为 Node-annotator 那边的计算公式是可以根据自己实际情况修改。

总体流程

node (cpu/memory) --> tsdb --> node-annotator --> node (CRD) --> crane-scheduler

看到这里,是不是觉得 crane-scheduler 的功能实际没有那么复杂,都是非常简单的,并不是高不可攀。

虽然我是这么说,我相信有小伙伴质疑:“为什么要这么项功能来做这一件事情,我有 xxx 种方法来解决”。我想说,我们耐心的看,crane-scheduler 还是有“亮点”来闭坑的,我想腾讯的工程师们不会那么“笨”,想不到。

原理分析

要说 crane-scheduler 这个调度器原理,我想还是先看看 k8s scheduler 的逻辑。因为 crane-scheduler 遵守的 scheduler framework 规范。

k8s scheduler

k8s scheduler 流

这里我们将目光放在图中 Scheduling Cycle 这部分内容内。 在关注内容之前,我先要声明一个概念

官方英文:Scheduling cycles are run serially, while binding cycles may run concurrently. 翻译中文:调度周期是串行,而绑定周期可以并行

() 划重点Scheduling cycles are run serially串行,串行,串行 重要的概念说三遍。 既然串行就会有一个 Queue。

看上图 Sort 就是让需要被调度的 pod 进入到一个 Queue 中。 官方英文:QueueSort These plugins are used to sort Pods in the scheduling queue. A queue sort plugin essentially provides a Less(Pod1, Pod2) function. Only one queue sort plugin may be enabled at a time.

我们且先记下这里是串行的,因为这里有一个“”是要认真思考和注意的。

crane-scheduler 实现的图中的 Filter 模块,官方英文:These plugins are used to filter out nodes that cannot run the Pod. For each node, the scheduler will call filter plugins in their configured order. If any filter plugin marks the node as infeasible, the remaining plugins will not be called for that node. Nodes may be evaluated concurrently. 大致意思是说: 这些插件用于过滤掉无法运行 pod 的节点。对于每个节点,调度程序将按配置的顺序调用过滤器插件。如果任何过滤器插件将该节点标记为不可用的,则不会为该节点调用其余插件。节点可以并发求值。

() 划重点

  1. 在 Filter 模块中,所有 plugins 根据配置顺序(也可以理解成编排),串行过滤,构成一个 chain。
  2. 在 Filter 模块中,在 chain 中有一个 plugin 返回 true(被过滤),chain 中其他 plugin 将不被执行。
  3. 在 Filter 模块中,如果 chain 中没有 plugin 返回 true(被过滤),那么维持原有的过滤结果。

QueueSort 的坑

既然所有的 pod 在进入 Scheduling Cycle 的时候都老老实实的排队了,那么我们做这么一个假设:这个 Queue 中如果一个模块出现拥塞,会怎么样?不敢想,后续的需要被调度的 pod 只能老老实实的等待,因为大家都在一条单行道上。

结合到业务上,如果自定义的 scheduler plugin 处理速度很慢或者拥塞了,那么对于拥有庞大 pod 数量的大 cluster 可谓是一个灾难性的情况。不但新发布业务的 pod 不能及时被调度运行,同时那也因为异常重启或者 HPA 触发扩容的 pod 都将被卡住,导致无法满足业务的实际要求。

crane-scheduler

结合上面 k8s scheduler 结构逻辑以及 QueueSort 中可能会碰到的“”,我们再回过头去看 crane-scheduler 的架构图,是不是会发现,假如:crane-scheduler 在调度 pod 时候,去查询 tsdb 并计算负载值,再执行 filter。你觉得这个效率高吗? 发现这里会存在卡点的人,恐怕不是我一个人。我还没有说,pod 在调度的时候 tsdb 异常无法查询数据呢?是不是好多都不敢去想。

说到这里,细品,就发现了这个架构有意思的地方

  • Node-annotator/Prometheus 声明者。监控自己 Node 的 cpu/memory 负载情况,并声明这个值。因为自己最了解自己情况,自己对自己言行负责。
  • Crane-scheduler 决定者。根据声明的值来做判断是否要执行行动。

各司其职,功能解构和边界划分非常清晰。

隐藏的抗

既然声明者对自己负责,声明自己的状态值。确实存在声明者的程序异常或者无法连接到 tsdb 去读取数据,那该如何解决?

解决这个问题实际比较简单,就是给声明者声明的状态值加上一个 timestamp。决定者看到这个 timestamp 就会跟现在时间做比对,求差值。如果差值在一个周期内,那么就采用声明者所声明的状态值,反之则忽略这个值,执行默认策略。

因为决定者是工作在 k8s scheduler 内,采用上面的逻辑,就不会将 Queue 卡住。声明的值写在 Node CRD 中 metadata/annotations,不需要做查询 tsdb 和计算等待,而是直接调用声明结果做判断。

最终效果

左边是原生方案,右边采用了 Crane-scheduler 增强的方案

差异对比

解读:负载越往中间集中越好。越往中间集中的部分(如图),之间差异越小越好。

参考资料