使用一组自定义的调度规则创建一个额外的 kube-scheduler

使用一组自定义的调度规则创建一个额外的 kube-scheduler

Kube-scheduler是Kubernetes的一个组成部分,负责按照指定的策略跨节点调度pod。 通常,在 Kubernetes 集群的运行过程中,我们不必考虑究竟使用什么策略来调度 pod,因为默认的 kube-scheduler 策略集适用于大多数日常任务。 然而,在某些情况下,微调 pod 的分布对我们来说很重要,有两种方法可以完成此任务:

  1. 使用自定义规则集创建 kube-scheduler
  2. 编写您自己的调度程序并教它处理 API 服务器请求

在这篇文章中,我将描述解决我们其中一个项目上的 pod 调度不均匀问题的第一点的实现。

简单介绍一下kube-scheduler'a的工作

值得注意的是,kube-scheduler 并不直接负责调度 pod——它只负责确定放置 pod 的节点。 换句话说,kube-scheduler工作的结果就是它为调度请求返回给API服务器的节点名,它的工作到此结束。

首先,kube-scheduler 根据谓词策略列出 Pod 可以调度到的节点。 此外,该列表中的每个节点根据优先级策略接收一定数量的点。 结果,选择得分最高的节点。 如果存在具有相同最大分数的节点,则随机选择一个。 可以在中找到谓词(过滤)和优先级(评分)策略的列表和描述 文件资料.

问题体的描述

尽管在 Nixys 中维护着大量不同的 Kubernetes 集群,但直到最近我们才第一次遇到调度 pod 的问题,当时我们的一个项目需要运行大量周期性任务(约 100 个 CronJob 实体)。 为了尽可能简化问题的描述,我们以一个微服务为例,其中每分钟启动一次cron任务,对CPU造成一些负载。 对于 cron 任务的工作,分配了三个完全相同的节点(每个节点有 24 个 vCPU)。

同时,不可能准确地说出 CronJob 将运行多长时间,因为输入数据量在不断变化。 平均而言,在正常的 kube-scheduler 操作期间,每个节点运行 3-4 个作业实例,这会在每个节点的 CPU 上产生约 20-30% 的负载:

使用一组自定义的调度规则创建一个额外的 kube-scheduler

问题本身是有时 cron 任务 pod 停止为三个节点之一安排。 也就是说,在某个时间点,没有为其中一个节点计划一个 pod,而在其他两个节点上运行 6-8 个任务实例,在 CPU 上产生了大约 40-60% 的负载:

使用一组自定义的调度规则创建一个额外的 kube-scheduler

这个问题以绝对随机的频率重复出现,偶尔与推出新版本代码的时刻相关。

通过将 kube-scheduler 的日志级别提高到 10 级(-v=10),我们开始记录每个节点在评估过程中的得分。 正常调度时,在日志中可以看到如下信息:

resource_allocation.go:78] cronjob-1574828880-mn7m4 -> Node03: BalancedResourceAllocation, capacity 23900 millicores 67167186944 memory bytes, total request 1387 millicores 4161694720 memory bytes, score 9
resource_allocation.go:78] cronjob-1574828880-mn7m4 -> Node02: BalancedResourceAllocation, capacity 23900 millicores 67167186944 memory bytes, total request 1347 millicores 4444810240 memory bytes, score 9
resource_allocation.go:78] cronjob-1574828880-mn7m4 -> Node03: LeastResourceAllocation, capacity 23900 millicores 67167186944 memory bytes, total request 1387 millicores 4161694720 memory bytes, score 9
resource_allocation.go:78] cronjob-1574828880-mn7m4 -> Node01: BalancedResourceAllocation, capacity 23900 millicores 67167186944 memory bytes, total request 1687 millicores 4790840320 memory bytes, score 9
resource_allocation.go:78] cronjob-1574828880-mn7m4 -> Node02: LeastResourceAllocation, capacity 23900 millicores 67167186944 memory bytes, total request 1347 millicores 4444810240 memory bytes, score 9
resource_allocation.go:78] cronjob-1574828880-mn7m4 -> Node01: LeastResourceAllocation, capacity 23900 millicores 67167186944 memory bytes, total request 1687 millicores 4790840320 memory bytes, score 9
generic_scheduler.go:726] cronjob-1574828880-mn7m4_project-stage -> Node01: NodeAffinityPriority, Score: (0)                                                                                       
generic_scheduler.go:726] cronjob-1574828880-mn7m4_project-stage -> Node02: NodeAffinityPriority, Score: (0)                                                                                       
generic_scheduler.go:726] cronjob-1574828880-mn7m4_project-stage -> Node03: NodeAffinityPriority, Score: (0)                                                                                       
interpod_affinity.go:237] cronjob-1574828880-mn7m4 -> Node01: InterPodAffinityPriority, Score: (0)                                                                                                        
generic_scheduler.go:726] cronjob-1574828880-mn7m4_project-stage -> Node01: TaintTolerationPriority, Score: (10)                                                                                   
interpod_affinity.go:237] cronjob-1574828880-mn7m4 -> Node02: InterPodAffinityPriority, Score: (0)                                                                                                        
generic_scheduler.go:726] cronjob-1574828880-mn7m4_project-stage -> Node02: TaintTolerationPriority, Score: (10)                                                                                   
selector_spreading.go:146] cronjob-1574828880-mn7m4 -> Node01: SelectorSpreadPriority, Score: (10)                                                                                                        
interpod_affinity.go:237] cronjob-1574828880-mn7m4 -> Node03: InterPodAffinityPriority, Score: (0)                                                                                                        
generic_scheduler.go:726] cronjob-1574828880-mn7m4_project-stage -> Node03: TaintTolerationPriority, Score: (10)                                                                                   
selector_spreading.go:146] cronjob-1574828880-mn7m4 -> Node02: SelectorSpreadPriority, Score: (10)                                                                                                        
selector_spreading.go:146] cronjob-1574828880-mn7m4 -> Node03: SelectorSpreadPriority, Score: (10)                                                                                                        
generic_scheduler.go:726] cronjob-1574828880-mn7m4_project-stage -> Node01: SelectorSpreadPriority, Score: (10)                                                                                    
generic_scheduler.go:726] cronjob-1574828880-mn7m4_project-stage -> Node02: SelectorSpreadPriority, Score: (10)                                                                                    
generic_scheduler.go:726] cronjob-1574828880-mn7m4_project-stage -> Node03: SelectorSpreadPriority, Score: (10)                                                                                    
generic_scheduler.go:781] Host Node01 => Score 100043                                                                                                                                                                        
generic_scheduler.go:781] Host Node02 => Score 100043                                                                                                                                                                        
generic_scheduler.go:781] Host Node03 => Score 100043

那些。 根据从日志中获得的信息判断,每个节点获得相同数量的最终点,并随机选择一个点进行规划。 在问题规划时,日志如下所示:

resource_allocation.go:78] cronjob-1574211360-bzfkr -> Node02: BalancedResourceAllocation, capacity 23900 millicores 67167186944 memory bytes, total request 1587 millicores 4581125120 memory bytes, score 9
resource_allocation.go:78] cronjob-1574211360-bzfkr -> Node03: BalancedResourceAllocation, capacity 23900 millicores 67167186944 memory bytes, total request 1087 millicores 3532549120 memory bytes, score 9
resource_allocation.go:78] cronjob-1574211360-bzfkr -> Node02: LeastResourceAllocation, capacity 23900 millicores 67167186944 memory bytes, total request 1587 millicores 4581125120 memory bytes, score 9
resource_allocation.go:78] cronjob-1574211360-bzfkr -> Node01: BalancedResourceAllocation, capacity 23900 millicores 67167186944 memory bytes, total request 987 millicores 3322833920 memory bytes, score 9
resource_allocation.go:78] cronjob-1574211360-bzfkr -> Node01: LeastResourceAllocation, capacity 23900 millicores 67167186944 memory bytes, total request 987 millicores 3322833920 memory bytes, score 9 
resource_allocation.go:78] cronjob-1574211360-bzfkr -> Node03: LeastResourceAllocation, capacity 23900 millicores 67167186944 memory bytes, total request 1087 millicores 3532549120 memory bytes, score 9
interpod_affinity.go:237] cronjob-1574211360-bzfkr -> Node03: InterPodAffinityPriority, Score: (0)                                                                                                        
interpod_affinity.go:237] cronjob-1574211360-bzfkr -> Node02: InterPodAffinityPriority, Score: (0)                                                                                                        
interpod_affinity.go:237] cronjob-1574211360-bzfkr -> Node01: InterPodAffinityPriority, Score: (0)                                                                                                        
generic_scheduler.go:726] cronjob-1574211360-bzfkr_project-stage -> Node03: TaintTolerationPriority, Score: (10)                                                                                   
selector_spreading.go:146] cronjob-1574211360-bzfkr -> Node03: SelectorSpreadPriority, Score: (10)                                                                                                        
selector_spreading.go:146] cronjob-1574211360-bzfkr -> Node02: SelectorSpreadPriority, Score: (10)                                                                                                        
generic_scheduler.go:726] cronjob-1574211360-bzfkr_project-stage -> Node02: TaintTolerationPriority, Score: (10)                                                                                   
selector_spreading.go:146] cronjob-1574211360-bzfkr -> Node01: SelectorSpreadPriority, Score: (10)                                                                                                        
generic_scheduler.go:726] cronjob-1574211360-bzfkr_project-stage -> Node03: NodeAffinityPriority, Score: (0)                                                                                       
generic_scheduler.go:726] cronjob-1574211360-bzfkr_project-stage -> Node03: SelectorSpreadPriority, Score: (10)                                                                                    
generic_scheduler.go:726] cronjob-1574211360-bzfkr_project-stage -> Node02: SelectorSpreadPriority, Score: (10)                                                                                    
generic_scheduler.go:726] cronjob-1574211360-bzfkr_project-stage -> Node01: TaintTolerationPriority, Score: (10)                                                                                   
generic_scheduler.go:726] cronjob-1574211360-bzfkr_project-stage -> Node02: NodeAffinityPriority, Score: (0)                                                                                       
generic_scheduler.go:726] cronjob-1574211360-bzfkr_project-stage -> Node01: NodeAffinityPriority, Score: (0)                                                                                       
generic_scheduler.go:726] cronjob-1574211360-bzfkr_project-stage -> Node01: SelectorSpreadPriority, Score: (10)                                                                                    
generic_scheduler.go:781] Host Node03 => Score 100041                                                                                                                                                                        
generic_scheduler.go:781] Host Node02 => Score 100041                                                                                                                                                                        
generic_scheduler.go:781] Host Node01 => Score 100038

从中可以看出,其中一个节点的总得分低于其他节点,因此仅对得分最高的两个节点进行了规划。 因此,我们确信问题恰恰出在 pod 的规划上。

解决问题的进一步算法对我们来说是显而易见的 - 分析日志,了解节点没有获得分数的优先级,并在必要时调整默认 kube-scheduler 的策略。 然而,在这里我们面临两个重大困难:

  1. 最大日志记录级别 (10) 仅反映某些优先级的点集。 在上面的日志摘录中可以看到,对于日志中反映的所有优先级,节点在正常调度和问题调度中的得分是相同的,但在问题调度情况下最终的结果是不同的。 因此,我们可以得出结论,对于某些优先级,评分是在“幕后”进行的,我们无法了解节点没有获得分数的优先级。 我们已经详细描述了这个问题 问题 Github 上的 Kubernetes 存储库。 在撰写本文时,已收到开发人员的回应,称将在 Kubernetes v1.15,1.16、1.17 和 XNUMX 更新中添加日志记录支持。
  2. 没有简单的方法来了解 kube-scheduler 当前正在使用哪一组特定的策略。 是的,在 文件资料 该清单已列出,但不包含关于为每项优先政策设定的具体权重的信息。 您只能在中查看默认 kube-scheduler 的权重或编辑策略 来源.

值得注意的是,一旦我们设法修复节点未根据 ImageLocalityPriority 策略获得积分,如果节点已经具有运行应用程序所需的图像,则该策略会奖励积分给节点。 也就是说,在推出新版本应用程序时,cron 任务有时间在两个节点上运行,从 docker registry 下载新镜像给它们,因此两个节点相对于第三个。

正如我上面所写,在日志中我们看不到有关 ImageLocalityPriority 策略评估的信息,因此,为了检查我们的假设,我们将带有新版本应用程序的图像假脱机到第三个节点,之后规划工作正常。 正是由于 ImageLocalityPriority 策略,调度问题很少被观察到,更多时候它与其他事物相关联。 由于我们无法完全调试默认 kube-scheduler 的优先级列表中的每个策略,因此我们需要灵活管理 pod 调度策略。

制定问题

我们希望问题的解决方案尽可能具有针对性,即 Kubernetes 的主要实体(这里指的是默认的 kube-scheduler)应该保持不变。 我们不想在一个地方解决问题并在另一个地方创建它。 因此,我们得出了解决文章介绍中宣布的问题的两种选择 - 创建一个额外的调度程序或编写您自己的调度程序。 调度 cron 任务的主要要求是在三个节点之间平均分配负载。 这个要求可以通过现有的 kube-scheduler 策略来满足,所以为我们的任务编写我们自己的调度器是没有意义的。

创建和部署额外的 kube-scheduler 的说明在 文件资料. 但是,在我们看来,Deployment实体在kube-scheduler这种关键服务的运行中似乎不足以保证容错性,所以我们决定将新的kube-scheduler部署为Static Pod,由Kubelet直接监控. 因此,我们对新的 kube-scheduler 有以下要求:

  1. 该服务必须作为静态 Pod 部署在所有集群主节点上
  2. 如果带有 kube-scheduler 的活动 pod 不可用,则应提供故障转移
  3. 规划时的主要优先级应该是节点上可用资源的数量(LeastRequestedPriority)

实施方案

需要注意的是,我们将在 Kubernetes v1.14.7 中执行所有工作,因为项目中使用了这个版本。 让我们从为我们的新 kube-scheduler 编写清单开始。 让我们以默认清单(/etc/kubernetes/manifests/kube-scheduler.yaml)为基础,将其带入以下形式:

kind: Pod
metadata:
  labels:
    component: scheduler
    tier: control-plane
  name: kube-scheduler-cron
  namespace: kube-system
spec:
      containers:
      - command:
        - /usr/local/bin/kube-scheduler
        - --address=0.0.0.0
        - --port=10151
        - --secure-port=10159
        - --config=/etc/kubernetes/scheduler-custom.conf
        - --authentication-kubeconfig=/etc/kubernetes/scheduler.conf
        - --authorization-kubeconfig=/etc/kubernetes/scheduler.conf
        - --v=2
        image: gcr.io/google-containers/kube-scheduler:v1.14.7
        imagePullPolicy: IfNotPresent
        livenessProbe:
          failureThreshold: 8
          httpGet:
            host: 127.0.0.1
            path: /healthz
            port: 10151
            scheme: HTTP
          initialDelaySeconds: 15
          timeoutSeconds: 15
        name: kube-scheduler-cron-container
        resources:
          requests:
            cpu: '0.1'
        volumeMounts:
        - mountPath: /etc/kubernetes/scheduler.conf
          name: kube-config
          readOnly: true
        - mountPath: /etc/localtime
          name: localtime
          readOnly: true
        - mountPath: /etc/kubernetes/scheduler-custom.conf
          name: scheduler-config
          readOnly: true
        - mountPath: /etc/kubernetes/scheduler-custom-policy-config.json
          name: policy-config
          readOnly: true
      hostNetwork: true
      priorityClassName: system-cluster-critical
      volumes:
      - hostPath:
          path: /etc/kubernetes/scheduler.conf
          type: FileOrCreate
        name: kube-config
      - hostPath:
          path: /etc/localtime
        name: localtime
      - hostPath:
          path: /etc/kubernetes/scheduler-custom.conf
          type: FileOrCreate
        name: scheduler-config
      - hostPath:
          path: /etc/kubernetes/scheduler-custom-policy-config.json
          type: FileOrCreate
        name: policy-config

简要介绍主要变化:

  1. 将 pod 和容器名称更改为 kube-scheduler-cron
  2. 指定使用端口 10151 和 10159 作为选项定义 hostNetwork: true 而且我们不能使用与默认 kube-scheduler 相同的端口(10251 和 10259)
  3. 使用 --config 参数,指定服务应该启动的配置文件
  4. 配置从主机挂载配置文件(scheduler-custom.conf)和调度策略文件(scheduler-custom-policy-config.json)

不要忘记我们的 kube-scheduler 需要类似于默认权限的权限。 编辑其集群角色:

kubectl edit clusterrole system:kube-scheduler

...
   resourceNames:
    - kube-scheduler
    - kube-scheduler-cron
...

下面说说配置文件和调度策略文件应该包含什么:

  • 配置文件(scheduler-custom.conf)
    获取默认kube-scheduler的配置,需要使用参数 --write-config-to из 文件资料. 我们将生成的配置放在 /etc/kubernetes/scheduler-custom.conf 文件中,并将其变为以下形式:

apiVersion: kubescheduler.config.k8s.io/v1alpha1
kind: KubeSchedulerConfiguration
schedulerName: kube-scheduler-cron
bindTimeoutSeconds: 600
clientConnection:
  acceptContentTypes: ""
  burst: 100
  contentType: application/vnd.kubernetes.protobuf
  kubeconfig: /etc/kubernetes/scheduler.conf
  qps: 50
disablePreemption: false
enableContentionProfiling: false
enableProfiling: false
failureDomains: kubernetes.io/hostname,failure-domain.beta.kubernetes.io/zone,failure-domain.beta.kubernetes.io/region
hardPodAffinitySymmetricWeight: 1
healthzBindAddress: 0.0.0.0:10151
leaderElection:
  leaderElect: true
  leaseDuration: 15s
  lockObjectName: kube-scheduler-cron
  lockObjectNamespace: kube-system
  renewDeadline: 10s
  resourceLock: endpoints
  retryPeriod: 2s
metricsBindAddress: 0.0.0.0:10151
percentageOfNodesToScore: 0
algorithmSource:
   policy:
     file:
       path: "/etc/kubernetes/scheduler-custom-policy-config.json"

简要介绍主要变化:

  1. 将 schedulerName 设置为我们的服务 kube-scheduler-cron 的名称。
  2. 在参数中 lockObjectName 我们还需要设置服务的名称并确保参数 leaderElect 设置为真(如果你有一个主节点,你可以将值设置为假)。
  3. 指定参数中带有调度策略描述的文件路径 algorithmSource.

值得更详细地讨论第二段,我们在其中编辑密钥的参数 leaderElection. 为了容错,我们启用了(leaderElect)通过为它们使用单​​个端点在我们的 kube-scheduler 的 pod 之间选择领导者(主)的过程(resourceLock) 命名为 kube-scheduler-cron (lockObjectName) 在 kube-system 命名空间 (lockObjectNamespace). Kubernetes 如何保证主要组件(包括 kube-scheduler)的高可用可以参见 文章.

  • 调度策略文件(scheduler-custom-policy-config.json)
    正如我之前所写,我们只能通过分析其代码来了解默认的 kube-scheduler 使用的具体策略。 即我们无法获取到默认kube-scheduler调度策略的文件,类比配置文件。 让我们在 /etc/kubernetes/scheduler-custom-policy-config.json 文件中描述我们感兴趣的调度策略,如下所示:

{
  "kind": "Policy",
  "apiVersion": "v1",
  "predicates": [
    {
      "name": "GeneralPredicates"
    }
  ],
  "priorities": [
    {
      "name": "ServiceSpreadingPriority",
      "weight": 1
    },
    {
      "name": "EqualPriority",
      "weight": 1
    },
    {
      "name": "LeastRequestedPriority",
      "weight": 1
    },
    {
      "name": "NodePreferAvoidPodsPriority",
      "weight": 10000
    },
    {
      "name": "NodeAffinityPriority",
      "weight": 1
    }
  ],
  "hardPodAffinitySymmetricWeight" : 10,
  "alwaysCheckAllPredicates" : false
}

因此 kube-scheduler 首先根据 GeneralPredicates 策略(包括 PodFitsResources、PodFitsHostPorts、HostName 和 MatchNodeSelector 策略集)列出 Pod 可以调度到的节点。 然后根据优先级数组中的策略集对每个节点进行评估。 为了满足我们的任务条件,我们认为这样一套政策将是最好的解决方案。 让我提醒您,一组政策及其详细说明可在 文件资料. 要完成您的任务,您可以简单地更改使用的策略集并为它们分配适当的权重。

我们在本章开头创建的新 kube-scheduler 清单将称为 kube-scheduler-custom.yaml 并放置在三个主节点上的 /etc/kubernetes/manifests 中。 如果一切都正确完成,Kubelet 将在每个节点上启动 pod,并且在我们新的 kube-scheduler 的日志中,我们将看到我们的策略文件已成功应用的信息:

Creating scheduler from configuration: {{ } [{GeneralPredicates <nil>}] [{ServiceSpreadingPriority 1 <nil>} {EqualPriority 1 <nil>} {LeastRequestedPriority 1 <nil>} {NodePreferAvoidPodsPriority 10000 <nil>} {NodeAffinityPriority 1 <nil>}] [] 10 false}
Registering predicate: GeneralPredicates
Predicate type GeneralPredicates already registered, reusing.
Registering priority: ServiceSpreadingPriority
Priority type ServiceSpreadingPriority already registered, reusing.
Registering priority: EqualPriority
Priority type EqualPriority already registered, reusing.
Registering priority: LeastRequestedPriority
Priority type LeastRequestedPriority already registered, reusing.
Registering priority: NodePreferAvoidPodsPriority
Priority type NodePreferAvoidPodsPriority already registered, reusing.
Registering priority: NodeAffinityPriority
Priority type NodeAffinityPriority already registered, reusing.
Creating scheduler with fit predicates 'map[GeneralPredicates:{}]' and priority functions 'map[EqualPriority:{} LeastRequestedPriority:{} NodeAffinityPriority:{} NodePreferAvoidPodsPriority:{} ServiceSpreadingPriority:{}]'

现在只需要在我们的 CronJob 规范中指出所有调度其 pod 的请求都应由我们的新 kube-scheduler 处理:

...
 jobTemplate:
    spec:
      template:
        spec:
          schedulerName: kube-scheduler-cron
...

结论

最后,我们得到了一个额外的 kube-scheduler,它有一套独特的调度策略,由 kubelet 直接监控。 此外,我们在 kube-scheduler 的 pod 之间设置了新领导者的选举,以防旧领导者由于某种原因不可用。

正常的应用程序和服务继续通过默认的 kube-scheduler 进行调度,所有的 cron 任务已经完全转移到新的。 cron 任务创建的负载现在均匀分布在所有节点上。 考虑到大多数 cron 任务与项目的主要应用程序在相同的节点上执行,这大大降低了 pod 由于资源不足而移动的风险。 引入额外的 kube-scheduler 后,不再有 cron 作业调度不均匀的问题。

另请阅读我们博客上的其他文章:

来源: habr.com

添加评论