默認狀況下, kube-scheduler 提供的默認調度器可以知足咱們絕大多數的要求,咱們前面和你們接觸的示例也基本上用的默認的策略,均可以保證咱們的 Pod 能夠被分配到資源充足的節點上運行。可是在實際的線上項目中,可能咱們本身會比 kubernetes 更加了解咱們本身的應用,好比咱們但願一個 Pod 只能運行在特定的幾個節點上,或者這幾個節點只能用來運行特定類型的應用,這就須要咱們的調度器可以可控。node
kube-scheduler 的主要做用就是根據特定的調度算法和調度策略將 Pod 調度到合適的 Node 節點上去,是一個獨立的二進制程序,啓動以後會一直監聽 API Server,獲取到 PodSpec.NodeName 爲空的 Pod,對每一個 Pod 都會建立一個 binding。linux
這個過程在咱們看來好像比較簡單,但在實際的生產環境中,須要考慮的問題就有不少了: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 而已,裏面包含 過濾器 和 優先級 兩個端點,分別對應調度週期中的兩個主要階段(過濾和打分)。
這裏咱們先簡單介紹下調度器擴展程序的實現。
在進入調度器擴展程序以前,咱們再來了解下 Kubernetes 調度程序是如何工做的:
默認調度器根據指定的參數啓動(咱們使用 kubeadm 搭建的集羣,啓動配置文件位於 /etc/kubernetes/manifests/kube-schdueler.yaml)
watch apiserver,將 spec.nodeName 爲空的 Pod 放入調度器內部的調度隊列中
從調度隊列中 Pod 出一個 Pod,開始一個標準的調度週期
從 Pod 屬性中檢索「硬性要求」(好比 CPU/內存請求值,nodeSelector/nodeAffinity),而後過濾階段發生,在該階段計算出知足要求的節點候選列表
從 Pod 屬性中檢索「軟需求」,並應用一些默認的「軟策略」(好比 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),它基本上能夠解決咱們遇到的全部難題,如今也已經成官方推薦的擴展方式,因此這將是之後擴展調度器的最主流的方式。
https://developer.ibm.com/articles/creating-a-custom-kube-scheduler/