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自定義調度器之調度器擴展程序

相關文章
相關標籤/搜索