自定义开发调度器

Karmada自带了一个默认调度器,其详细描述请查阅这里。 如果默认调度器不适合你的需求,你可以实现自己的调度器。 Karmada的调度器框架与Kubernetes类似,但与K8s不同的是,Karmada是需要将应用部署至一组集群上,而不是一个单一对象,根据用户的调度策略的placement字段以及内部的调度插件算法将用户应用部署到预期的集群组上。

调度流程可以分为如下四步:

  • Predicate阶段:过滤不合适的集群
  • Priority阶段:为集群打分
  • SelectClusters选取阶段:根据集群得分以及SpreadConstraint选取集群组
  • ReplicaScheduling阶段:根据配置的副本调度策略将用户作业副本部署在选取的集群组上

schedule process

其中过滤与打分的插件可以基于调度器框架进行自定义的开发与配置。

Karmada默认的调度器有几个内置的插件:

  • APIEnablement: 一个过滤插件,用于检查需要下发的API资源(CRD)是否已在目标集群中被安装。
  • TaintToleration: 一个过滤插件,用于检查调度策略是否容忍集群的污点。
  • ClusterAffinity: 一个过滤和打分插件,用于实现集群的亲和性调度,支持通过names、labels、cluster的字段进行集群过滤。
  • SpreadConstraint: 一个过滤插件,用于检查集群是否满足调度策略的分发属性。
  • ClusterLocality: 一个打分插件,用于检查集群是否已存在被调度的资源,实现资源的聚合调度。
  • ClusterEviction: 一个过滤插件,用于检查目标集群是否在 GracefulEvictionTasks 中,这意味着该集群正处于被驱逐的过程中。

用户可以基于自身的场景自定义插件,并且通过Karmada的调度器框架实现自身的调度器。 以下给出了一个自定义开发调度器的具体例子。

开发前的准备

你需要已经安装Karmada,并拉取了Karmada的代码。 如果你想要安装Karmada,你可以按照这里的安装指南。 如果你想要试用Karmada,我们推荐通过hack/local-up-karmada.sh来部署一个开发环境。

  1. git clone https://github.com/karmada-io/karmada
  2. cd karmada
  3. hack/local-up-karmada.sh

开发一个插件

假设你想要开发一个名为TestFilter的过滤插件,你可以参考Karmada源代码中的调度器实现,代码位于pkg/scheduler/framework/plugins。 开发完成后的目录结构类似于:

  1. .
  2. ├── apienablement
  3. ├── clusteraffinity
  4. ├── clustereviction
  5. ├── clusterlocality
  6. ├── spreadconstraint
  7. ├── tainttoleration
  8. ├── testfilter
  9. ├── test_filter.go

其中test_filter.go文件的内容如下,隐去了具体的过滤逻辑实现。

  1. package testfilter
  2. import (
  3. "context"
  4. clusterv1alpha1 "github.com/karmada-io/karmada/pkg/apis/cluster/v1alpha1"
  5. policyv1alpha1 "github.com/karmada-io/karmada/pkg/apis/policy/v1alpha1"
  6. workv1alpha2 "github.com/karmada-io/karmada/pkg/apis/work/v1alpha2"
  7. "github.com/karmada-io/karmada/pkg/scheduler/framework"
  8. )
  9. const (
  10. // Name is the name of the plugin used in the plugin registry and configurations.
  11. Name = "TestFilter"
  12. )
  13. type TestFilter struct{}
  14. var _ framework.FilterPlugin = &TestFilter{}
  15. // New instantiates the TestFilter plugin.
  16. func New() (framework.Plugin, error) {
  17. return &TestFilter{}, nil
  18. }
  19. // Name returns the plugin name.
  20. func (p *TestFilter) Name() string {
  21. return Name
  22. }
  23. // Filter implements the filtering logic of the TestFilter plugin.
  24. func (p *TestFilter) Filter(ctx context.Context,
  25. bindingSpec *workv1alpha2.ResourceBindingSpec, bindingStatus *workv1alpha2.ResourceBindingStatus, cluster *clusterv1alpha1.Cluster) *framework.Result {
  26. // implementation
  27. return framework.NewResult(framework.Success)
  28. }

作为一个过滤插件,你需要实现framework.FilterPlugin接口。而作为一个打分插件,你需要实现framework.ScorePlugin接口。

注册插件

你需要编辑调度器的main函数 cmd/scheduler/main.go,在NewSchedulerCommand函数中传入自定义的插件配置。

  1. package main
  2. import (
  3. "os"
  4. "k8s.io/component-base/cli"
  5. _ "k8s.io/component-base/logs/json/register" // for JSON log format registration
  6. controllerruntime "sigs.k8s.io/controller-runtime"
  7. _ "sigs.k8s.io/controller-runtime/pkg/metrics"
  8. "github.com/karmada-io/karmada/cmd/scheduler/app"
  9. "github.com/karmada-io/karmada/pkg/scheduler/framework/plugins/testfilter"
  10. )
  11. func main() {
  12. stopChan := controllerruntime.SetupSignalHandler().Done()
  13. command := app.NewSchedulerCommand(stopChan, app.WithPlugin(testfilter.Name, testfilter.New))
  14. code := cli.Run(command)
  15. os.Exit(code)
  16. }

打包调度器

在你注册插件之后,你需要将你的调度器的二进制的调度器文件打包进一个容器镜像,并将上述镜像替换掉默认调度器的镜像。

  1. cd karmada
  2. export VERSION=## Your Image Tag
  3. make image-karmada-scheduler
  1. $ kubectl --kubeconfig ~/.kube/karmada.config --context karmada-host edit deploy/karmada-scheduler -nkarmada-system
  2. ...
  3. spec:
  4. automountServiceAccountToken: false
  5. containers:
  6. - command:
  7. - /bin/karmada-scheduler
  8. - --kubeconfig=/etc/kubeconfig
  9. - --bind-address=0.0.0.0
  10. - --secure-port=10351
  11. - --enable-scheduler-estimator=true
  12. - --v=4
  13. image: ## Your Image Address
  14. ...

当你启动调度器后,你可以从调度器的日志中发现TestFilter插件已启用。

  1. I0408 12:57:14.563522 1 scheduler.go:141] karmada-scheduler version: version.Info{GitVersion:"v1.9.0-preview5", GitCommit:"0126b90fc89d2f5509842ff8dc7e604e84288b96", GitTreeState:"clean", BuildDate:"2024-01-29T13:29:49Z", GoVersion:"go1.20.11", Compiler:"gc", Platform:"linux/amd64"}
  2. I0408 12:57:14.564979 1 registry.go:79] Enable Scheduler plugin "ClusterAffinity"
  3. I0408 12:57:14.564991 1 registry.go:79] Enable Scheduler plugin "SpreadConstraint"
  4. I0408 12:57:14.564996 1 registry.go:79] Enable Scheduler plugin "ClusterLocality"
  5. I0408 12:57:14.564999 1 registry.go:79] Enable Scheduler plugin "ClusterEviction"
  6. I0408 12:57:14.565002 1 registry.go:79] Enable Scheduler plugin "APIEnablement"
  7. I0408 12:57:14.565005 1 registry.go:79] Enable Scheduler plugin "TaintToleration"
  8. I0408 12:57:14.565008 1 registry.go:79] Enable Scheduler plugin "TestFilter"

配置插件的启停

你可以通过配置--plugins选项来配置插件的启停。 例如,以下的配置将会关闭TestFilter插件。

  1. $ kubectl --kubeconfig ~/.kube/karmada.config --context karmada-host edit deploy/karmada-scheduler -nkarmada-system
  2. ...
  3. spec:
  4. automountServiceAccountToken: false
  5. containers:
  6. - command:
  7. - /bin/karmada-scheduler
  8. - --kubeconfig=/etc/kubeconfig
  9. - --bind-address=0.0.0.0
  10. - --secure-port=10351
  11. - --enable-scheduler-estimator=true
  12. - --plugins=*,-TestFilter
  13. - --v=4
  14. image: ## Your Image Address
  15. ...

配置多个调度器

运行第二个调度器

你可以和默认调度器一起同时运行多个调度器,并告诉 Karmada 为每个工作负载使用哪个调度器。 以下是一个示例的调度器配置文件。 你可以将它保存为 my-scheduler.yaml

  1. apiVersion: apps/v1
  2. kind: Deployment
  3. metadata:
  4. name: my-karmada-scheduler
  5. namespace: karmada-system
  6. labels:
  7. app: my-karmada-scheduler
  8. spec:
  9. replicas: 1
  10. selector:
  11. matchLabels:
  12. app: my-karmada-scheduler
  13. template:
  14. metadata:
  15. labels:
  16. app: my-karmada-scheduler
  17. spec:
  18. automountServiceAccountToken: false
  19. tolerations:
  20. - key: node-role.kubernetes.io/master
  21. operator: Exists
  22. containers:
  23. - name: karmada-scheduler
  24. image: docker.io/karmada/karmada-scheduler:latest
  25. imagePullPolicy: IfNotPresent
  26. livenessProbe:
  27. httpGet:
  28. path: /healthz
  29. port: 10351
  30. scheme: HTTP
  31. failureThreshold: 3
  32. initialDelaySeconds: 15
  33. periodSeconds: 15
  34. timeoutSeconds: 5
  35. command:
  36. - /bin/karmada-scheduler
  37. - --kubeconfig=/etc/kubeconfig
  38. - --bind-address=0.0.0.0
  39. - --secure-port=10351
  40. - --enable-scheduler-estimator=true
  41. - --leader-elect-resource-name=my-scheduler # 你的自定义调度器名称
  42. - --scheduler-name=my-scheduler # 你的自定义调度器名称
  43. - --v=4
  44. volumeMounts:
  45. - name: kubeconfig
  46. subPath: kubeconfig
  47. mountPath: /etc/kubeconfig
  48. volumes:
  49. - name: kubeconfig
  50. secret:
  51. secretName: kubeconfig

Note: 对于 --leader-elect-resource-name 选项,默认为 karmada-scheduler。如果你将另一个调度器与默认的调度器一起部署, 需要指定此选项,并且建议使用你的自定义调度器名称作为值。

为了在 Karmada 中运行我们的第二个调度器,在 host 集群中创建上面配置中指定的 Deployment:

  1. kubectl --context karmada-host create -f my-scheduler.yaml

验证调度器 Pod 正在运行:

  1. kubectl --context karmada-host get pods --namespace=karmada-system

输出类似于:

  1. NAME READY STATUS RESTARTS AGE
  2. ....
  3. my-karmada-scheduler-lnf4s-4744f 1/1 Running 0 2m
  4. ...

此列表中,除了默认的 karmada-scheduler Pod 之外,你应该还能看到处于 “Running” 状态的 my-karmada-scheduler Pod。

为 Deployment 指定调度器

现在第二个调度器正在运行,创建一些 Deployment,并指定它们由默认调度器或部署的调度器进行调度。 为了使用特定的调度器调度给定的 Deployment,在命中那个 Deployment 的 Propagation spec 中指定调度器的名称。让我们看看三个例子。

  • PropagationPolicy spec 没有任何调度器名称
  1. apiVersion: policy.karmada.io/v1alpha1
  2. kind: PropagationPolicy
  3. metadata:
  4. name: nginx-propagation
  5. spec:
  6. resourceSelectors:
  7. - apiVersion: apps/v1
  8. kind: Deployment
  9. name: nginx
  10. placement:
  11. clusterAffinity:
  12. clusterNames:
  13. - member1
  14. - member2

如果未提供调度器名称,则会使用 default-scheduler 自动调度 Deployment。

  • PropagationPolicy spec 设置为 default-scheduler
  1. apiVersion: policy.karmada.io/v1alpha1
  2. kind: PropagationPolicy
  3. metadata:
  4. name: nginx-propagation
  5. spec:
  6. schedulerName: default-scheduler
  7. resourceSelectors:
  8. - apiVersion: apps/v1
  9. kind: Deployment
  10. name: nginx
  11. placement:
  12. clusterAffinity:
  13. clusterNames:
  14. - member1
  15. - member2

通过将调度器名称作为 spec.schedulerName 参数的值来指定调度器。 我们提供默认调度器的名称,即 default-scheduler

  • PropagationPolicy spec 设置为 my-scheduler
  1. apiVersion: policy.karmada.io/v1alpha1
  2. kind: PropagationPolicy
  3. metadata:
  4. name: nginx-propagation
  5. spec:
  6. schedulerName: my-scheduler
  7. resourceSelectors:
  8. - apiVersion: apps/v1
  9. kind: Deployment
  10. name: nginx
  11. placement:
  12. clusterAffinity:
  13. clusterNames:
  14. - member1
  15. - member2

在这种情况下,我们指定此 Deployment 使用我们部署的 my-scheduler 来进行调度。 请注意, spec.schedulerName 参数的值应该与调度器提供的选项中的 schedulerName 相匹配。

验证是否使用所需的调度器调度了 Deployment

为了更容易地完成这些示例, 你可以查看与此 Deployment 相关的事件日志,以验证是否由所需的调度器调度了该 Deployment。

  1. kubectl --context karmada-apiserver describe deploy/nginx