默认状况下, 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/