集群内负载调度方案调研
背景
资源调度是容器云平台中最核心的功能之一。为了满足各业务线的资源需求,调度系统需要选择合适的集群分配计算资源,支撑业务服务的运行。
Kubernetes的调度是指为新创建的pod,寻找一个最合适的宿主机。这个过程的成功与否、结果是否恰当,关键在于调度组件是怎么管理和分配集群节点的资源的。
Kubernetes的资源模型建立在pod对象上。每个pod的定义都会有CPU、内存资源的设置,默认是0.1核和200MB。实际上资源还要分成requests和limits两种情况。
这两者的区别其实非常简单:在调度的时候,kube-scheduler 只会按照 requests 的值进行计算。而在真正设置 Cgroups 限制的时候,kubelet 则会按照 limits 的值来进行设置。
kube-scheduler(调度器)就是Kubernetes负责调度的组件。简单地说,新创建的pod都会压入调度器的队列中,调度器依次判断集群内节点资源是否满足pod的需求,从而选择合适的节点。
资源调度关注的是集群资源的高效分配,例如机器的CPU、内存等资源的使用情况。除了资源的有效利用,集群稳定性也是重点。当所需的资源不足时,高利用率机器上的服务实例会出现性能上的下降。
Kubernetes调度器的调度流程是静态的资源请求调度,并没有办法解决上述问题。而在实际场景中,大多数实例实际使用到的资源其实远小于他所申请的request配额。这些情况加剧了空闲资源的浪费。
业界方案调研
2.1 腾讯 - node annotator
指标收集node-annotator:将节点资源使用指标更新到节点annotation上:
- 从指标数据源拉取节点指标数据
- 将节点指标数据更新至节点annotation 调度扩展scheduler extender:基于scheduler extender扩展调度算法,被kube scheduler调用执行调度策略:
- 依据历史周期内节点资源利用率初选,过滤高利用率节点
- 依据节点资源利用率指标优选节点,低利用率节点优先级越高
可以调整5分钟、1小时的均值和峰值的预选阈值 可以配置5分钟、1小时、1天的均值和峰值的优选权重
举例来说,当一个新pod被调度时,kube scheduler能够将pod调度到真实利用率低的节点上。
2.2 k8s社区(IBM, Paypal) - load watcher
指标收集load watcher: 将节点资源使用指标缓存到本地:
- 从监控源拉取节点指标数据
- 将指标缓存到本地
- 并服务于调度器插件查询 调度扩展scheduler plugin:基于Scheduler Framework Plugin实现优选阶段的Score插件,依据资源利用率对工作节点打分,选出资源利用率最低的节点。
这套方案期望的调度结果是:在总资源请求量和平均资源使用量之外,这个方案还会考虑历史周期内出现高峰值的情况,并倾向于将新pod调度到节点1上。
2.3 Intel - Schedule on metric
两个核心组件部署到同一个pod内协同工作: 指标收集和策略控制controller:
- 缓存指标数据:负责周期性拉取指标数据并本地缓存(mem cache)
- 缓存策略数据:监听TASPolicy CRD,依据规格更新本地缓存中的策略信息
- 提供本地缓存访问:向extender提供服务 调度扩展scheduler extender:extender从controller缓存中获取调度策略,执行相应的预选/优选算法。
该方案除了两个核心组件外还要求对pod设置label或nodeAffinity,才能在调度的时候执行相应策略。
大致的workflow如下:
- 创建用户策略实例:
apiVersion: telemetry.intel.com/v1alpha1
kind: TASPolicy
metadata:
name: free-memory
namespace: default
spec:
strategies:
scheduleonmetric: # 依据指标的调度策略
rules:
- metricname: memory_free
operator: GreaterThan
target: 1000
- 创建用户pod实例:
apiVersion: v1
kind: Pod
metadata:
name: pod-a
namespace: default
labels:
app: demo
telemetry-policy: free-memory # 通过label指定调度策略
spec:
containers:
- name: nginx
image: nginx:latest
imagePullPolicy: IfNotPresent
- 最终extender会将Pod A调度到可用内存量最高的Node A上。在优选阶段,调度器会依据extender返回节点列表选择优先级最高的节点。
2.4 负载调度扩展方案比较
k8s扩展调度方式
由上述方案可以看出,业界针对动态负载调度主要基于两类方式扩展默认调度器:scheduler extender,scheduler framework plugin。两者都属于非侵入式的方案,无需修改scheduler核心代码。
| 方式. | scheduler | extender |
| ----------- | ------------- | ------------- |
| v1.17 兼容性 | Stable in v1.17 | Beta in v1.17, Stable in v1.19 |
| 性能 | 走 http/https + 加解 JSON 包,开销较大 | 调用原生函数 |
| 灵活性 | extender 提供的扩展点较少且固定 | 调度扩展点更多,切分更细 |
| 异常处理 | scheduler 无法将调度异常传递给extender,以及终止调度请求。| 在 plugin 调度失败或者发生错误时都可能发生中断并被放入 scheduler 队列等待重新
调度 |
| 共享缓存 | extender 需要自行与 API Server 进行通信,建立重复缓存 |
共享 scheduler 的节点、pod 快照 |
3.1 Scheduler framework plugin技术调研
Scheduler framework 通过 KubeSchedulerConfiguration 来配置各 plugin 的开启和关闭。 Scheduler framework 将整个调度过程分为调度循环(Scheduling Cycle)和绑定循环 (Binding Cycle)
- 调度循环:通过预选和优选选择出一个节点以供 pod 运行。串行执行,即一个 pod 一个 pod 的调 度
- 绑定循环:依据调度循环的结果,将 pod 绑定节点上。并行执行,即并发执行多个 pod 的绑定操 作 如图,framework 在调度过程中暴露多个扩展点来定制化 pod 的调度。一个插件可以注册到一个 或多个扩展点来实现复杂或者有状态的调度任务。
- Sort: 用来对调度队列中的 pod 进行排序
- PreFilter: 对 pod 进行预处理或者检查集群或者 pod 必须满足的条件 - Filter: 在功能上等同于“预选”,在默认的预选算法之后执行
- PostFilter: 在 Filter 之后再执行过滤,在默认的预选算法、Filter plugin、extender 之后执行 - PreScore: 对过滤之后的节点预评分。v1.17 未支持
- Score: 在功能上等同于“优选”,在原有的优选策略之后执行
- NormalizeScore: 执行所有插件的 normalize scoring;每个插件对所有节点 score 进行 reduce,最终将分数限制在[MinNodeScore, MaxNodeScore]有效范围
- Reserve: 将 node 相关资源预留(assume)给 pod,更新 scheduler cache。这步操作发生在执 行绑定请求
- Permit: 阻止或推迟 pod 的绑定请求
- PreBind: 执行 bind 操作之前的准备工作
- Bind: 用于执行 pod 与 node 之间的绑定操作。先执行 Bind plugin,再发送绑定请求 - PostBind: 绑定循环的最后一个步骤,用于在 bind 操作执行成功后清理相关资源
我们可以在上述扩展点上添加自定义的调度行为来匹配需求。
总结
基于性能指标来动态调度实例在业界已经比较成熟,虽然各方案基于现有监控系统而有所不同,但是基于机器负载来修改默认调度器的思路是一致的。从业界上来看,已有的系统大多使用extender来扩张默认调度器。但是从社区的发展和性能考虑,plugin的方式是以后的趋势。提高机器利用率,均衡机器负载是一个长远的工作,除了扩展调度器外,还需要结合二次调度来平衡集群负载,降低高负载的风险。