K8S自定义调度器之调度器扩展程序

K8S自定义调度器之调度器扩展程序
默认状况下, kube-scheduler 提供的默认调度器可以知足咱们绝大多数的要求,咱们前面和你们接触的示例也基本上用的默认的策略,均可以保证咱们的 Pod 能够被分配到资源充足的节点上运行。可是在实际的线上项目中,可能咱们本身会比 kubernetes 更加了解咱们本身的应用,好比咱们但愿一个 Pod 只能运行在特定的几个节点上,或者这几个节点只能用来运行特定类型的应用,这就须要咱们的调度器可以可控。node

kube-scheduler 的主要做用就是根据特定的调度算法和调度策略将 Pod 调度到合适的 Node 节点上去,是一个独立的二进制程序,启动以后会一直监听 API Server,获取到 PodSpec.NodeName 为空的 Pod,对每一个 Pod 都会建立一个 binding。linux

K8S自定义调度器之调度器扩展程序

这个过程在咱们看来好像比较简单,但在实际的生产环境中,须要考虑的问题就有不少了:git

  • 如何保证所有的节点调度的公平性?要知道并非全部节点资源配置必定都是同样的github

  • 如何保证每一个节点都能被分配资源?golang

  • 集群资源如何可以被高效利用?web

  • 集群资源如何才能被最大化使用?算法

  • 如何保证 Pod 调度的性能和效率?编程

  • 用户是否能够根据本身的实际需求定制本身的调度策略?

考虑到实际环境中的各类复杂状况,kubernetes 的调度器采用插件化的形式实现,能够方便用户进行定制或者二次开发,咱们能够自定义一个调度器并以插件形式和 kubernetes 进行集成。api

通常来讲,咱们有4中扩展 Kubernetes 调度器的方法。缓存

  • 一种方法就是直接 clone 官方的 kube-scheduler 源代码,在合适的位置直接修改代码,而后从新编译运行修改后的程序,固然这种方法是最不建议使用的,也不实用,由于须要花费大量额外的精力来和上游的调度程序更改保持一致。

  • 第二种方法就是和默认的调度程序一块儿运行独立的调度程序,默认的调度器和咱们自定义的调度器能够经过 Pod 的 spec.schedulerName 来覆盖各自的 Pod,默认是使用 default 默认的调度器,可是多个调度程序共存的状况下也比较麻烦,好比当多个调度器将 Pod 调度到同一个节点的时候,可能会遇到一些问题,由于颇有可能两个调度器都同时将两个 Pod 调度到同一个节点上去,可是颇有可能其中一个 Pod 运行后其实资源就消耗完了,而且维护一个高质量的自定义调度程序也不是很容易的,由于咱们须要全面了解默认的调度程序,总体 Kubernetes 的架构知识以及各类 Kubernetes API 对象的各类关系或限制。

  • 第三种方法是调度器扩展程序(https://github.com/kubernetes/community/blob/master/contributors/design-proposals/scheduling/scheduler_extender.md),这个方案目前是一个可行的方案,能够和上游调度程序兼容,所谓的调度器扩展程序其实就是一个可配置的 Webhook 而已,里面包含 过滤器 和 优先级 两个端点,分别对应调度周期中的两个主要阶段(过滤和打分)。

  • 第四种方法是经过调度框架(Scheduling Framework),Kubernetes v1.15 版本中引入了可插拔架构的调度框架,使得定制调度器这个任务变得更加的容易。调库框架向现有的调度器中添加了一组插件化的 API,该 API 在保持调度程序“核心”简单且易于维护的同时,使得大部分的调度功能以插件的形式存在,并且在咱们如今的 v1.16 版本中上面的 调度器扩展程序 也已经被废弃了,因此之后调度框架才是自定义调度器的核心方式。

这里咱们先简单介绍下调度器扩展程序的实现。

调度器扩展程序

在进入调度器扩展程序以前,咱们再来了解下 Kubernetes 调度程序是如何工做的:

  1. 默认调度器根据指定的参数启动(咱们使用 kubeadm 搭建的集群,启动配置文件位于 /etc/kubernetes/manifests/kube-schdueler.yaml)

  2. watch apiserver,将 spec.nodeName 为空的 Pod 放入调度器内部的调度队列中

  3. 从调度队列中 Pod 出一个 Pod,开始一个标准的调度周期

  4. 从 Pod 属性中检索“硬性要求”(好比 CPU/内存请求值,nodeSelector/nodeAffinity),而后过滤阶段发生,在该阶段计算出知足要求的节点候选列表

  5. 从 Pod 属性中检索“软需求”,并应用一些默认的“软策略”(好比 Pod 倾向于在节点上更加聚拢或分散),最后,它为每一个候选节点给出一个分数,并挑选出得分最高的最终获胜者

  6. 和 apiserver 通讯(发送绑定调用),而后设置 Pod 的 spec.nodeName 属性以表示将该 Pod 调度到的节点。

咱们能够经过查看官方文档(https://kubernetes.io/docs/reference/command-line-tools-reference/kube-scheduler/),能够经过 --config 参数指定调度器将使用哪些参数,该配置文件应该包含一个 KubeSchedulerConfiguration(https://godoc.org/k8s.io/kubernetes/pkg/scheduler/apis/config#KubeSchedulerConfiguration) 对象,以下所示格式:(/etc/kubernetes/scheduler-extender.yaml)

# 经过"--config" 传递文件内容

apiVersion: kubescheduler.config.k8s.io/v1alpha1

kind: KubeSchedulerConfiguration

clientConnection:

  kubeconfig: "/etc/kubernetes/scheduler.conf"

algorithmSource:

  policy:

    file:

      path: "/etc/kubernetes/scheduler-extender-policy.yaml"  # 指定自定义调度策略文件

咱们在这里应该输入的关键参数是 algorithmSource.policy,这个策略文件能够是本地文件也能够是 ConfigMap 资源对象,这取决于调度程序的部署方式,好比咱们这里默认的调度器是静态 Pod 方式启动的,因此咱们能够用本地文件的形式来配置。

该策略文件 /etc/kubernetes/scheduler-extender-policy.yaml 应该遵循 kubernetes/pkg/scheduler/apis/config/legacy_types.go#L28(https://godoc.org/k8s.io/kubernetes/pkg/scheduler/apis/config#Policy) 的要求,在咱们这里的 v1.16.2 版本中已经支持 JSON 和 YAML 两种格式的策略文件,下面是咱们定义的一个简单的示例,能够查看 Extender(https://godoc.org/k8s.io/kubernetes/pkg/scheduler/apis/config#Extender) 描述了解策略文件的定义规范:

apiVersion: v1

kind: Policy

extenders:

- urlPrefix: "http://127.0.0.1:8888/"

  filterVerb: "filter"

  prioritizeVerb: "prioritize"

  weight: 1

  enableHttps: false

咱们这里的 Policy 策略文件是经过定义 extenders 来扩展调度器的,有时候咱们不须要去编写代码,能够直接在该配置文件中经过指定 predicates 和 priorities 来进行自定义,若是没有指定则会使用默认的 DefaultProvier

{

      "kind": "Policy",

      "apiVersion": "v1",

      "predicates": [

          {

              "name": "MatchNodeSelector"

          },

          {

              "name": "PodFitsResources"

          },

          {

              "name": "PodFitsHostPorts"

          },

          {

              "name": "HostName"

          },

          {

              "name": "NoDiskConflict"

          },

          {

              "name": "NoVolumeZoneConflict"

          },

          {

              "name": "PodToleratesNodeTaints"

          },

          {

              "name": "CheckNodeMemoryPressure"

          },

          {

              "name": "CheckNodeDiskPressure"

          },

          {

              "name": "CheckNodePIDPressure"

          },

          {

              "name": "CheckNodeCondition"

          },

          {

              "name": "MaxEBSVolumeCount"

          },

          {

              "name": "MaxGCEPDVolumeCount"

          },

          {

              "name": "MaxAzureDiskVolumeCount"

          },

          {

              "name": "MaxCSIVolumeCountPred"

          },

          {

              "name": "MaxCinderVolumeCount"

          },

          {

              "name": "MatchInterPodAffinity"

          },

          {

              "name": "GeneralPredicates"

          },

          {

              "name": "CheckVolumeBinding"

          },

          {

              "name": "TestServiceAffinity",

              "argument": {

                  "serviceAffinity": {

                      "labels": [

                          "region"

                      ]

                  }

              }

          },

          {

              "name": "TestLabelsPresence",

              "argument": {

                  "labelsPresence": {

                      "labels": [

                          "foo"

                      ],

                      "presence": true

                  }

              }

          }

      ],

      "priorities": [

          {

              "name": "EqualPriority",

              "weight": 2

          },

          {

              "name": "ImageLocalityPriority",

              "weight": 2

          },

          {

              "name": "LeastRequestedPriority",

              "weight": 2

          },

          {

              "name": "BalancedResourceAllocation",

              "weight": 2

          },

          {

              "name": "SelectorSpreadPriority",

              "weight": 2

          },

          {

              "name": "NodePreferAvoidPodsPriority",

              "weight": 2

          },

          {

              "name": "NodeAffinityPriority",

              "weight": 2

          },

          {

              "name": "TaintTolerationPriority",

              "weight": 2

          },

          {

              "name": "InterPodAffinityPriority",

              "weight": 2

          },

          {

              "name": "MostRequestedPriority",

              "weight": 2

          },

          {

              "name": "RequestedToCapacityRatioPriority",

              "weight": 2,

              "argument": {

                  "requestedToCapacityRatioArguments": {

                      "shape": [

                          {

                              "utilization": 0,

                              "score": 0

                          },

                          {

                              "utilization": 50,

                              "score": 7

                          }

                      ],

                      "resources": [

                          {

                              "name": "intel.com/foo",

                              "weight": 3

                          },

                          {

                              "name": "intel.com/bar",

                              "weight": 5

                          }

                      ]

                  }

              }

          }

      ],

      "extenders": [

          {

              "urlPrefix": "/prefix",

              "filterVerb": "filter",

              "prioritizeVerb": "prioritize",

              "weight": 1,

              "bindVerb": "bind",

              "enableHttps": true,

              "tlsConfig": {

                  "Insecure": true

              },

              "httpTimeout": 1,

              "nodeCacheCapable": true,

              "managedResources": [

                  {

                      "name": "example.com/foo",

                      "ignoredByScheduler": true

                  }

              ],

              "ignorable": true

          }

      ]

  }

改策略文件定义了一个 HTTP 的扩展程序服务,该服务运行在 127.0.0.1:8888 下面,而且已经将该策略注册到了默认的调度器中,这样在过滤和打分阶段结束后,能够将结果分别传递给该扩展程序的端点 <urlPrefix>/<filterVerb> 和 <urlPrefix>/<prioritizeVerb>,在扩展程序中,咱们能够进一步过滤并肯定优先级,以适应咱们的特定业务需求。

示例

咱们直接用 golang 来实现一个简单的调度器扩展程序,固然你可使用其余任何编程语言,以下所示:

func main() {

    router := httprouter.New()

    router.GET("/", Index)

    router.POST("/filter", Filter)

    router.POST("/prioritize", Prioritize)

    log.Fatal(http.ListenAndServe(":8888", router))

}

而后接下来咱们须要实现 /filter 和 /prioritize 两个端点的处理程序。

其中 Filter 这个扩展函数接收一个输入类型为 schedulerapi.ExtenderArgs 的参数,而后返回一个类型为 *schedulerapi.ExtenderFilterResult 的值。在函数中,咱们能够进一步过滤输入的节点:

// filter 根据扩展程序定义的预选规则来过滤节点

func filter(args schedulerapi.ExtenderArgs) *schedulerapi.ExtenderFilterResult {

  var filteredNodes []v1.Node

  failedNodes := make(schedulerapi.FailedNodesMap)

  pod := args.Pod

  for _, node := range args.Nodes.Items {

    fits, failReasons, _ := podFitsOnNode(pod, node)

    if fits {

      filteredNodes = append(filteredNodes, node)

    } else {

      failedNodes[node.Name] = strings.Join(failReasons, ",")

    }

  }

  result := schedulerapi.ExtenderFilterResult{

    Nodes: &v1.NodeList{

      Items: filteredNodes,

    },

    FailedNodes: failedNodes,

    Error:       "",

  }

  return &result

}

在过滤函数中,咱们循环每一个节点而后用咱们本身实现的业务逻辑来判断是否应该批准该节点,这里咱们实现比较简单,在 podFitsOnNode() 函数中咱们只是简单的检查随机数是否为偶数来判断便可,若是是的话咱们就认为这是一个幸运的节点,不然拒绝批准该节点。

var predicatesSorted = []string{LuckyPred}

var predicatesFuncs = map[string]FitPredicate{

    LuckyPred: LuckyPredicate,

}

type FitPredicate func(pod *v1.Pod, node v1.Node) (bool, []string, error)

func podFitsOnNode(pod *v1.Pod, node v1.Node) (bool, []string, error) {

    fits := true

    var failReasons []string

    for _, predicateKey := range predicatesSorted {

        fit, failures, err := predicatesFuncs[predicateKey](pod, node)

        if err != nil {

            return false, nil, err

        }

        fits = fits && fit

        failReasons = append(failReasons, failures...)

    }

    return fits, failReasons, nil

}

func LuckyPredicate(pod *v1.Pod, node v1.Node) (bool, []string, error) {

    lucky := rand.Intn(2) == 0

    if lucky {

        log.Printf("pod %v/%v is lucky to fit on node %v\n", pod.Name, pod.Namespace, node.Name)

        return true, nil, nil

    }

    log.Printf("pod %v/%v is unlucky to fit on node %v\n", pod.Name, pod.Namespace, node.Name)

    return false, []string{LuckyPredFailMsg}, nil

}

一样的打分功能用一样的方式来实现,咱们在每一个节点上随机给出一个分数:

// it's webhooked to pkg/scheduler/core/generic_scheduler.go#PrioritizeNodes()

// 这个函数输出的分数会被添加会默认的调度器

func prioritize(args schedulerapi.ExtenderArgs) *schedulerapi.HostPriorityList {

  pod := args.Pod

  nodes := args.Nodes.Items

  hostPriorityList := make(schedulerapi.HostPriorityList, len(nodes))

  for i, node := range nodes {

    score := rand.Intn(schedulerapi.MaxPriority + 1)  // 在最大优先级内随机取一个值

    log.Printf(luckyPrioMsg, pod.Name, pod.Namespace, score)

    hostPriorityList[i] = schedulerapi.HostPriority{

      Host:  node.Name,

      Score: score,

    }

  }

  return &hostPriorityList

}

而后咱们可使用下面的命令来编译打包咱们的应用:

$ GOOS=linux GOARCH=amd64 go build -o app
本节调度器扩展程序完整的代码获取地址:https://github.com/cnych/sample-scheduler-extender。

构建完成后,将应用 app 拷贝到 kube-scheduler 所在的节点直接运行便可。如今咱们就能够将上面的策略文件配置到 kube-scheduler 组件中去了,咱们这里集群是 kubeadm 搭建的,因此直接修改文件 /etc/kubernetes/manifests/kube-schduler.yaml 文件便可,内容以下所示:

apiVersion: v1

kind: Pod

metadata:

  creationTimestamp: null

  labels:

    component: kube-scheduler

    tier: control-plane

  name: kube-scheduler

  namespace: kube-system

spec:

  containers:

  - command:

    - kube-scheduler

    - --authentication-kubeconfig=/etc/kubernetes/scheduler.conf

    - --authorization-kubeconfig=/etc/kubernetes/scheduler.conf

    - --bind-address=127.0.0.1

    - --kubeconfig=/etc/kubernetes/scheduler.conf

    - --leader-elect=true

    - --config=/etc/kubernetes/scheduler-extender.yaml

    - --v=9

    image: gcr.azk8s.cn/google_containers/kube-scheduler:v1.16.2

    imagePullPolicy: IfNotPresent

    livenessProbe:

      failureThreshold: 8

      httpGet:

        host: 127.0.0.1

        path: /healthz

        port: 10251

        scheme: HTTP

      initialDelaySeconds: 15

      timeoutSeconds: 15

    name: kube-scheduler

    resources:

      requests:

        cpu: 100m

    volumeMounts:

    - mountPath: /etc/kubernetes/scheduler.conf

      name: kubeconfig

      readOnly: true

    - mountPath: /etc/kubernetes/scheduler-extender.yaml

      name: extender

      readOnly: true

    - mountPath: /etc/kubernetes/scheduler-extender-policy.yaml

      name: extender-policy

      readOnly: true

  hostNetwork: true

  priorityClassName: system-cluster-critical

  volumes:

  - hostPath:

      path: /etc/kubernetes/scheduler.conf

      type: FileOrCreate

    name: kubeconfig

  - hostPath:

      path: /etc/kubernetes/scheduler-extender.yaml

      type: FileOrCreate

    name: extender

  - hostPath:

      path: /etc/kubernetes/scheduler-extender-policy.yaml

      type: FileOrCreate

    name: extender-policy

status: {}
固然咱们这个地方是直接在系统默认的 kube-scheduler 上面配置的,咱们也能够复制一个调度器的 YAML 文件而后更改下 schedulerName 来部署,这样就不会影响默认的调度器了,而后在须要使用这个测试的调度器的 Pod 上面指定 spec.schedulerName 便可。对于多调度器的使用能够查看官方文档 配置多个调度器(https://kubernetes.io/zh/docs/tasks/administer-cluster/configure-multiple-schedulers/)。

kube-scheduler 从新配置后能够查看日志来验证是否重启成功,须要注意的是必定须要将 /etc/kubernetes/scheduler-extender.yaml 和 /etc/kubernetes/scheduler-extender-policy.yaml 两个文件挂载到 Pod 中去:

$ kubectl logs -f kube-scheduler-ydzs-master -n kube-system

I0102 15:17:38.824657       1 serving.go:319] Generated self-signed cert in-memory

I0102 15:17:39.472276       1 server.go:143] Version: v1.16.2

I0102 15:17:39.472674       1 defaults.go:91] TaintNodesByCondition is enabled, PodToleratesNodeTaints predicate is mandatory

W0102 15:17:39.479704       1 authorization.go:47] Authorization is disabled

W0102 15:17:39.479733       1 authentication.go:79] Authentication is disabled

I0102 15:17:39.479777       1 deprecated_insecure_serving.go:51] Serving healthz insecurely on [::]:10251

I0102 15:17:39.480559       1 secure_serving.go:123] Serving securely on 127.0.0.1:10259

I0102 15:17:39.682180       1 leaderelection.go:241] attempting to acquire leader lease  kube-system/kube-scheduler...

I0102 15:17:56.500505       1 leaderelection.go:251] successfully acquired lease kube-system/kube-scheduler

到这里咱们就建立并配置了一个很是简单的调度扩展程序,如今咱们来运行一个 Deployment 查看其工做原理,咱们准备一个包含20个副本的部署 Yaml:(test-scheduler.yaml)

apiVersion: apps/v1

kind: Deployment

metadata:

  name: pause

spec:

  replicas: 20

  selector:

    matchLabels:

      app: pause

  template:

    metadata:

      labels:

        app: pause

    spec:

      containers:

      - name: pause

        image: gcr.azk8s.cn/google_containers/pause:3.1

直接建立上面的资源对象:

$ kuectl apply -f test-scheduler.yaml

deployment.apps/pause created

这个时候咱们去查看下咱们编写的调度器扩展程序日志:

$ ./app

......

2020/01/03 12:27:29 pod pause-58584fbc95-bwn7t/default is unlucky to fit on node ydzs-node1

2020/01/03 12:27:29 pod pause-58584fbc95-bwn7t/default is lucky to get score 7

2020/01/03 12:27:29 pod pause-58584fbc95-bwn7t/default is lucky to get score 9

2020/01/03 12:27:29 pod pause-58584fbc95-86w92/default is unlucky to fit on node ydzs-node3

2020/01/03 12:27:29 pod pause-58584fbc95-86w92/default is unlucky to fit on node ydzs-node4

2020/01/03 12:27:29 pod pause-58584fbc95-86w92/default is lucky to fit on node ydzs-node1

2020/01/03 12:27:29 pod pause-58584fbc95-86w92/default is lucky to fit on node ydzs-node2

2020/01/03 12:27:29 pod pause-58584fbc95-86w92/default is lucky to get score 4

2020/01/03 12:27:29 pod pause-58584fbc95-86w92/default is lucky to get score 8

......

咱们能够看到 Pod 调度的过程,另外默认调度程序会按期重试失败的 Pod,所以它们将一次又一次地从新传递到咱们的调度扩展程序上,咱们的逻辑是检查随机数是否为偶数,因此最终全部 Pod 都将处于运行状态。

调度器扩展程序多是在一些状况下能够知足咱们的需求,可是他仍然有一些限制和缺点:

  • 通讯成本:数据在默认调度程序和调度器扩展程序之间以 http(s)传输,在执行序列化和反序列化的时候有必定成本

  • 有限的扩展点:扩展程序只能在某些阶段的末尾参与,例如 “Filter”和 “Prioritize”,它们不能在任何阶段的开始或中间被调用

  • 减法优于加法:与默认调度程序传递的节点候选列表相比,咱们可能有一些需求须要添加新的候选节点列表,但这是比较冒险的操做,由于不能保证新节点能够经过其余要求,因此,调度器扩展程序最好执行 “减法”(进一步过滤),而不是 “加法”(添加节点)

  • 缓存共享:上面只是一个简单的测试示例,但在真实的项目中,咱们是须要经过查看整个集群的状态来作出调度决策的,默认调度程序能够很好地调度决策,可是没法共享其缓存,这意味着咱们必须构建和维护本身的缓存

因为这些局限性,Kubernetes 调度小组就提出了上面第四种方法来进行更好的扩展,也就是 调度框架(SchedulerFramework),它基本上能够解决咱们遇到的全部难题,如今也已经成官方推荐的扩展方式,因此这将是之后扩展调度器的最主流的方式。

参考资料

K8S自定义调度器之调度器扩展程序

相关文章
相关标签/搜索