Crane-scheduler:一款基于真实工作负载感知的 Kubernetes 调度插件

公众号关注 「奇妙的 Linux 世界」

设为「星标」,每天带你玩转 Linux !

e59045f43ea6518f49c371e960c14ae4.png

原生 kubernetes 调度器只能基于资源的 resource request 进行调度,然而 Pod 的真实资源使用率,往往与其所申请资源的 request/limit 差异很大,导致集群负载不均的问题。

crane-scheduler基于集群的真实负载数据构造了一个简单却有效的模型,作用于调度过程中的 Filter 与 Score 阶段,并提供了一种灵活的调度策略配置方式,从而有效缓解集群中资源负载不均问题,真正实现将本增效。

作者:匡澄,中国移动云能力中心助理软件开发工程师,专注于云原生、微服务等领域。

背景

将服务部署在Kubernetes集群上是当今许多企业的首选方案,其能帮助企业自动化部署、弹性伸缩以及容错处理等工作,减少了人工操作和维护工作量,提高了服务的可靠性和稳定性,有效实现了降本增效。但kubernetes 的原生调度器只能通过资源请求来调度 pod,这很容易造成一系列负载不均的问题:

  1. 集群中的部分节点,资源的真实使用率远低于 resource request,却没有被调度更多的 Pod,这造成了比较大的资源浪费。

  2. 而集群中的另外一些节点,其资源的真实使用率事实上已经过载,却无法为调度器所感知到,这极大可能影响到业务的稳定性。

这些无疑都与企业上云的最初目的相悖,为业务投入了足够的资源,却没有达到理想的效果。crane-scheduler打破了资源 resource request 与真实使用率之间的鸿沟,着力于调度层面,让调度器直接基于真实使用率进行调度,使用最大化的同时排除了稳定性的后顾之忧,真正实现降本增效。

Kubernetes 调度框架

Kubernetes官方提供了可插拔架构的调度框架,能够进一步扩展Kubernetes调度器,下图展示了调度框架中的调度上下文及其中的扩展点,一个扩展可以注册多个扩展点,以便可以执行更复杂的有状态的任务。

cf7f9ace994d0cf1f95a91c2bb772c4c.png
图1 Pod 调度流程
  1. Sort - 用于对 Pod 的待调度队列进行排序,以决定先调度哪个 Pod

  2. Pre-filter - 用于对 Pod 的信息进行预处理

  3. Filter - 用于排除那些不能运行该 Pod 的节点

  4. Post-filter - 一个通知类型的扩展点,更新内部状态,或者产生日志

  5. Scoring - 用于为所有可选节点进行打分

  6. Normalize scoring - 在调度器对节点进行最终排序之前修改每个节点的评分结果

  7. Reserve - 使用该扩展点获得节点上为 Pod 预留的资源,该事件发生在调度器将 Pod 绑定到节点前

  8. Permit - 用于阻止或者延迟 Pod 与节点的绑定

  9. Pre-bind - 用于在 Pod 绑定之前执行某些逻辑

  10. Bind - 用于将 Pod 绑定到节点上

  11. Post-bind - 是一个通知性质的扩展

  12. Unreserve - 如果为 Pod 预留资源,又在被绑定过程中被拒绝绑定,则将被调用

对于调度框架插件的启用或者禁用,我们同样可以使用上面的 KubeSchedulerConfiguration 资源对象来进行配置。下面的例子中的配置启用了一个实现了 filter 和 scoring 扩展点的插件,并且禁用了另外一个插件,同时为插件 foo 提供了一些配置信息:

apiVersion: kubescheduler.config.k8s.io/v1alpha1
kind: KubeSchedulerConfiguration
...
plugins:
  filter:
    enabled:
    - name: foo
    - name: bar
    disabled:
    - name: baz
  scoring:
    enabled:
    - name: foo
    disabled:
    - name: baz

pluginConfig:
- name: foo
  args: >
    foo插件可以解析的任意内容

扩展的调用顺序如下:

  • 如果某个扩展点没有配置对应的扩展,调度框架将使用默认插件中的扩展

  • 如果为某个扩展点配置且激活了扩展,则调度框架将先调用默认插件的扩展,再调用配置中的扩展

  • 默认插件的扩展始终被最先调用,然后按照 KubeSchedulerConfiguration 中扩展的激活 enabled 顺序逐个调用扩展点的扩展

  • 可以先禁用默认插件的扩展,然后在 enabled 列表中的某个位置激活默认插件的扩展,这种做法可以改变默认插件的扩展被调用时的顺序

Kubernetes 调度插件demo: https://github.com/cnych/sample-scheduler-framework

crane-scheduler 设计与实现

总体架构

6a32b22618afcb599102f2d80d683bc7.jpeg
图2 Crane-scheduler 总体架构

动态调度器总体架构如上图所示,主要有两个组件组成:

  1. Node-annotator定期从 Prometheus 拉取数据,并以注释的形式在节点上用时间戳标记它们。

  2. Dynamic plugin直接从节点的注释中读取负载数据,过滤并基于简单的算法对候选节点进行评分。

同时动态调度器提供了一个默认值调度策略并支持用户自定义策略。默认策略依赖于以下指标:

cpu_usage_avg_5m
cpu_usage_max_avg_1h
cpu_usage_max_avg_1d
mem_usage_avg_5m
mem_usage_max_avg_1h
mem_usage_max_avg_1d

在调度的Filter阶段,如果该节点的实际使用率大于上述任一指标的阈值,则该节点将被过滤。而在Score阶段,最终得分是这些指标值的加权和。

在生产集群中,可能会频繁出现调度热点,因为创建 Pod 后节点的负载不能立即增加。因此定义了一个额外的指标,名为Hot Value,表示节点最近几次的调度频率。并且节点的最终优先级是最终得分减去Hot Value

关键代码实现

  • Node-annotation:定期从 Prometheus 拉取数据,并以注释的形式在节点上用时间戳标记它们

// /pkg/controller/annotator/node.go
func (n *nodeController) syncNode(key string) (bool, error) {
  startTime := time.Now()
  defer func() {
    klog.Infof("Finished syncing node event %q (%v)", key, time.Since(startTime))
  }()

  // 获取 nodeName, metricName
  nodeName, metricName, err := splitMetaKeyWithMetricName(key)

  // 通过 nodeName 获取 node 的具体信息
  node, err := n.nodeLister.Get(nodeName)

  // 通过 nodeIP 或者 nodeName 获取并更新 node 的监控指标
  err = annotateNodeLoad(n.promClient, n.kubeClient, node, metricName)

  // 获取 node hotVaule,并更新
  err = annotateNodeHotValue(n.kubeClient, n.bindingRecords, node, n.policy)

  return true, nil
}

func annotateNodeLoad(promClient prom.PromClient, kubeClient clientset.Interface, node *v1.Node, key string) error {
  // 通过 nodeIp 查询
  value, err := promClient.QueryByNodeIP(key, getNodeInternalIP(node))
  if err == nil && len(value) > 0 {
    return patchNodeAnnotation(kubeClient, node, key, value)
  }

  // 通过 nodeName 查询
  value, err = promClient.QueryByNodeName(key, getNodeName(node))
  if err == nil && len(value) > 0 {
    return patchNodeAnnotation(kubeClient, node, key, value)
  }
  return fmt.Errorf("failed to get data %s{%s=%s}: %v", key, node.Name, value, err)
}
  • Dynamic plugin:Dynamic plugin 修改 filter 和 score 阶段

// /pkg/plugins/dynamic/plugins.go
// Filter - 检查一个节点的实际负载是否过高
func (ds *DynamicScheduler) Filter(ctx context.Context, state *framework.CycleState, pod *v1.Pod, nodeInfo *framework.NodeInfo) *framework.Status {
    node := nodeInfo.Node()
  // 读取 nodeAnnotation, nodeName
  nodeAnnotations, nodeName := nodeInfo.Node().Annotations, nodeInfo.Node().Name

  // filter - 过滤
  for _, policy := range ds.schedulerPolicy.Spec.Predicate {
    // 获取采样时间
    activeDuration, err := getActiveDuration(ds.schedulerPolicy.Spec.SyncPeriod, policy.Name)

    // 根据指标,判断 node 是否过载
    if isOverLoad(nodeName, nodeAnnotations, policy, activeDuration) {
      return framework.NewStatus(framework.Unschedulable, fmt.Sprintf("Load[%s] of node[%s] is too high", policy.Name, nodeName))
    }
  }
  return framework.NewStatus(framework.Success, "")
}

// Score - 它从节点注释中获取度量数据,并支持实际资源使用最少的节点
func (ds *DynamicScheduler) Score(ctx context.Context, state *framework.CycleState, p *v1.Pod, nodeName string) (int64, *framework.Status) {
  // 通过 nodeName,获取 node 具体信息
  nodeInfo, err := ds.handle.SnapshotSharedLister().NodeInfos().Get(nodeName)
  node := nodeInfo.Node()
  nodeAnnotations := node.Annotations

  // 计算得分和 hotValue
  score, hotValue := getNodeScore(node.Name, nodeAnnotations, ds.schedulerPolicy.Spec), getNodeHotValue(node)
  score = score - int(hotValue*10)

    // 计算总得分 finalScore
  finalScore := utils.NormalizeScore(int64(score), framework.MaxNodeScore, framework.MinNodeScore)
  return finalScore, nil
}

使用流程

配置 prometheus 监测规则
  • syncPolicy: 用户可以自定义负载数据的类型与拉取周期;

  • predicate:  Filter 策略,若候选节点的当前负载数据超过了任一所配置的指标阈值,则这个节点将会被过滤;

  • priority:在 Score 策略中配置相关指标的权重,候选节点的最终得分为不同指标得分的加权和;

  • hotValue:定义调度热点规则,最终节点的 Priority 为上一小节中的 Score 减去 Hot Value

apiVersion: scheduler.policy.crane.io/v1alpha1
kind: DynamicSchedulerPolicy
spec:
  syncPolicy:
    ##cpu usage
    - name: cpu_usage_avg_5m
      period: 3m
    - 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

  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.2
    - name: cpu_usage_max_avg_1h
      weight: 0.3
    - name: cpu_usage_max_avg_1d
      weight: 0.5
    ##memory usage
    - name: mem_usage_avg_5m
      weight: 0.2
    - name: mem_usage_max_avg_1h
      weight: 0.3
    - name: mem_usage_max_avg_1d
      weight: 0.5

  hotValue:
    - timeRange: 5m
      count: 5
    - timeRange: 1m
      count: 2
使用 crane-scheduler

这里有两种方式可供选择:

  • 作为k8s原生调度器之外的第二个调度器

  • 替代k8s原生调度器成为默认的调度器

作为k8s原生调度器之外的第二个调度器:在 pod spec.schedulerName 指定 crane scheduler

apiVersion: apps/v1
kind: Deployment
metadata:
  name: cpu-stress
spec:
  selector:
    matchLabels:
      app: cpu-stress
  replicas: 1
  template:
    metadata:
      labels:
        app: cpu-stress
    spec:
      schedulerName: crane-scheduler
      hostNetwork: true
      tolerations:
      - key: node.kubernetes.io/network-unavailable
        operator: Exists
        effect: NoSchedule
      containers:
      - name: stress
        image: docker.io/gocrane/stress:latest
        command: ["stress", "-c", "1"]
        resources:
          requests:
            memory: "1Gi"
            cpu: "1"
          limits:
            memory: "1Gi"
            cpu: "1"

替代k8s原生调度器成为默认的调度器

  1. 修改kube调度器的配置文件(scheduler config.yaml)以启用动态调度器插件并配置插件参数:

apiVersion: kubescheduler.config.k8s.io/v1beta2
kind: KubeSchedulerConfiguration
...
profiles:
- schedulerName: default-scheduler
  plugins:
    filter:
      enabled:
      - name: Dynamic
    score:
      enabled:
      - name: Dynamic
        weight: 3
  pluginConfig:
  - name: Dynamic
     args:
      policyConfigPath: /etc/kubernetes/policy.yaml
...

/etc/kubernetes/policy.yaml 就是 4.3.1 中的 DynamicSchedulerPolicy 资源对象

  1. 修改kube-scheduler.yaml,并将kube调度器映像替换为Crane schedule

...
 image: docker.io/gocrane/crane-scheduler:0.0.23
...
  1. 安装 crane-scheduler-controller

kubectl apply ./deploy/controller/rbac.yaml && kubectl apply -f ./deploy/controller/deployment.yaml

真实环境测试

crane-sheduler 会将监控指标数据写在 node annotation 上

bdfec749f3c832ecd866c92942df77ec.png
node annotation

内存型服务测试

测试服务单副本实际占用 2C 20G ,申请资源 5C 40G

dfba9f9ee991428f435fa0309ea67e1e.png
memory
  • k8s默认调度器结果(%)

7846cb81c1fa2a7278747bb4a171a7b7.png

默认调度器根据 资源申请值request 调度服务,且节点间分布不均衡

当副本数到达12 个时,默认调度器出现了资源分配严重不均的情况

80c2365f8326f81c12bf24480222a5f1.png
  • crane-schedule调度器结果(%)

608626ed920960a29ef38fd3db7a1625.png

当启动11个服务的时候,node03中的mem_usage_avg_5m指标过高,禁止调度:

494773a3947b23d06abd72ad8e8126f4.png

CPU型服务测试

测试服务单副本实际占用 8C 8G ,申请资源 12C 12G

769b9ddcfb7ea52a8c564931f818d836.png
  • k8s默认调度器结果(%)

d12be827e274c35caa31673feba38cfe.png

当启动9个服务的时候,出现 Insufficient cpu 的情况:

57e159350e8dd35b280676a733ba1f4c.png
  • crane-schedule调度器结果(%)

2e03f520a51206d69b5927718d5ba8c2.png

当启动8服务的时候,node03中的mem_usage_avg_5m指标过高,禁止调度:

40805008f81f9c3bae30326d8ed63e8b.png

参考文献

  1. http://kubernetes.p2hp.com/docs/

  2. https://www.qikqiak.com/post/custom-kube-scheduler/

  3. https://gocrane.io/zh-cn/docs/tutorials/dynamic-scheduler-plugin/

  4. https://github.com/gocrane/crane-scheduler

  5. https://blog.51cto.com/u_14120339/5363877

本文转载自:「k8s技术圈」,原文:https://tinyurl.com/2xanp6wf,版权归原作者所有。欢迎投稿,投稿邮箱: editor@hi-linux.com。

63ce9a8bfd1e08da22d4b3f647ba3fc7.gif

最近,我们建立了一个技术交流微信群。目前群里已加入了不少行业内的大神,有兴趣的同学可以加入和我们一起交流技术,在 「奇妙的 Linux 世界」 公众号直接回复 「加群」 邀请你入群。

9a960f4283582cfa30d35e3734a88196.png

你可能还喜欢

点击下方图片即可阅读

ab27352be704dee8792e85f588eddafc.jpeg

一款超赞极简开源文件共享系统,无需注册可直接下载文件

a7e6fb2c7eb549c57ce23db8299b4902.png
点击上方图片,『美团|饿了么』外卖红包天天免费领

8c73d8328dcc8610c8c0f3a1371a0034.png

更多有趣的互联网新鲜事,关注「奇妙的互联网」视频号全了解!

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值