1. 为什么需要自定义调度逻辑

什么是所谓的调度?

  • 所谓调度就是指给 Pod 对象的 spec.nodeName 赋值

  • 待调度对象则是所有 spec.nodeName 为空的 Pod

  • 调度过程则是从集群现有的 Node 中为当前 Pod 选择一个最合适的

实际上 Pod 上还有一个平时比较少关注的属性: spec.schedulerName,用于指定该 Pod 要交给哪个调度器进行调度。

那么问题来了,平时用的时候也没给 spec.schedulerName 赋值过,怎么也能调度呢?

因为默认的 kube-scheduler 可以兼容 spec.schedulerName 为空或者为 default 的 Pod。

为什么需要自定义调度逻辑

自定义调度逻辑可以解决特定应用场景和需求,使集群资源使用更高效,适应特殊的调度策略。

比如:

  • 不同的工作负载可能有特定的资源需求,比如 GPU 或 NPU,需要确保 Pod 只能调度到满足这些资源条件的节点上。
  • 某些集群可能需要均衡资源消耗,避免将多个负载集中到某些节点上。
  • 为了降低延迟,可能需要将Pod调度到特定地理位置的节点上。自定义调度器可以根据节点的地理位置标签进行调度决策。
  • 某些应用需要与其他应用隔离运行,以避免资源争抢。通过自定义调度器,可以将特定类型的任务或工作负载隔离到专用的节点上。
  • ...

总之就是业务上有各种特殊的调度需求,因此我们需要通过实现自定义调度器来满足这些需求。

通过实现自定义调度器,可以根据具体的业务需求和集群环境,实现更灵活、更高效的资源管理和调度策略。

2.如何增加自定义调度逻辑

自定义调度器的几种方法

要增加自定义调度逻辑也并不复杂,K8s 整个调度流程都已经插件化了,我们并不需要重头开始实现一个调度器,而只需要实现一个调度插件,通过在调度过程中各个阶段加入我们的自定义逻辑,来控制最终的调度结果。

总体来说可以分为以下几个方向:

1)新增一个调度器

  • 直接修改 kube-scheduler 源码,编译替换
  • 使用 调度框架(Scheduling Framework),我们可以使用 scheduler-plugins 作为模板,简化自定义调度器的开发流程。
    • Kubernetes v1.15 版本中引入了可插拔架构的调度框架,使得定制调度器这个任务变得更加的容易。调库框架向现有的调度器中添加了一组插件化的 API,该 API 在保持调度程序“核心”简单且易于维护的同时,使得大部分的调度功能以插件的形式存在。

2)扩展原有调度器

  • 通过 Scheduler Extender 可以实现对已有调度器进行扩展。单独创建一个 HTTP 服务并实现对应接口,后续就可以将该服务作为外置调度器使用。通过配置 KubeSchedulerConfiguration原 Scheduler 会以 HTTP 调用方式和外置调度器交互,实现在不改动原有调度器基础上增加自定义逻辑。

3)其他非主流方案

  • 自定义 Webhook 直接修改未调度 Pod 的 spec.nodeName 字段,有点离谱但理论可行哈哈

二者都有自己的优缺点

优点缺点
新增调度器性能好:由于不依赖外部插件或 HTTP 调用,调度流程的延迟相对较低,适合对性能要求较高的场景复用性高:可复用现有的调度插件,如 scheduler-plugins,大大降低了开发难度,提升了开发效率。多个调度器可能会冲突:比如多个调度器同时调度了一个 Pod 到节点上,先启动的 Pod 把资源占用了,后续的 Pod 无法启动。
扩展调度器实现简单:无需重新编译调度器,通过配置 KubeSchedulerConfiguration 创建一个外部 HTTP 服务来实现自定义逻辑。零侵入性:不需要修改或重构调度器的核心代码,可快速上线新的调度逻辑。灵活性较高:原有调度器和自定义逻辑相对独立,方便维护与测试。性能差:调度请求需要经过 HTTP 调用,增加了调用延迟,对性能可能有影响。

一般在我们要改动的逻辑不多时,直接使用 Scheduler Extender 是比较简单的。

Scheduler 配置

调度器的配置有一个单独的对象:KubeSchedulerConfiguration,不过并没有以 CRD 形式存在,而是存放到 Configmap 中的。

以下是一个完整 KubeSchedulerConfiguration 的 yaml:

apiVersion: v1
data:config.yaml: |apiVersion: kubescheduler.config.k8s.io/v1beta2kind: KubeSchedulerConfigurationleaderElection:leaderElect: falseprofiles:- schedulerName: hami-schedulerextenders:- urlPrefix: "https://127.0.0.1:443"filterVerb: filterbindVerb: bindnodeCacheCapable: trueweight: 1httpTimeout: 30senableHTTPS: truetlsConfig:insecure: truemanagedResources:- name: nvidia.com/gpuignoredByScheduler: true- name: nvidia.com/gpumemignoredByScheduler: true- name: nvidia.com/gpucoresignoredByScheduler: true

一般分为基础配置和 extenders 配置两部分。

基础配置

基础配置一般就是配置调度器的名称,Demo 如下:

apiVersion: v1
kind: ConfigMap
metadata:name: my-scheduler-confignamespace: kube-system
data:my-scheduler-config.yaml: |apiVersion: kubescheduler.config.k8s.io/v1beta2kind: KubeSchedulerConfigurationprofiles:- schedulerName: my-schedulerleaderElection:leaderElect: false   

通过 schedulerName 来指定该调度器的名称,比如这里就是 my-scheduler 。

创建 Pod 时除非手动指定 spec.schedulerName 为 my-scheduler,否则不会由该调度器进行调度。

扩展调度器:extenders 配置

extenders 部分通过额外指定一个 http 服务器来实现外置的自定义的调度逻辑。

一个简单的 Scheduler Extender 配置如下:

apiVersion: v1
kind: ConfigMap
metadata:name: i-scheduler-extendernamespace: kube-system
data:i-scheduler-extender.yaml: |apiVersion: kubescheduler.config.k8s.io/v1kind: KubeSchedulerConfigurationprofiles:- schedulerName: i-scheduler-extenderleaderElection:leaderElect: falseextenders:- urlPrefix: "http://localhost:8080"enableHTTPS: falsefilterVerb: "filter"prioritizeVerb: "prioritize"bindVerb: "bind"weight: 1nodeCacheCapable: true

核心部分为

    extenders:- urlPrefix: "http://localhost:8080"enableHTTPS: falsefilterVerb: "filter"prioritizeVerb: "prioritize"bindVerb: "bind"weight: 1nodeCacheCapable: true

几个核心参数含义如下:

  • urlPrefix: http://127.0.0.1:8080 用于指定外置的调度服务访问地址

  • filterVerb: "filter":表示 Filter 接口在外置服务中的访问地址为 filter,即完整地址为 http://127.0.0.1:8080/filter

  • prioritizeVerb: "prioritize":同上,Prioritize(Score) 接口的地址为 prioritize

  • bindVerb: "bind":同上,Bind 接口的地址为 bind

这样该调度器在执行 Filter 接口逻辑时,除了内置的调度器插件之外,还会通过 HTTP 方式调用外置的调度器。

这样我们只需要创建一个 HTTP 服务,实现对应接口即可实现自定义的调度逻辑,而不需要重头实现一个调度器。

ManagedResources 配置

在之前的配置中是所有 Pod 都会走 Extender 的调度逻辑,实际上 Extender 还有一个 ManagedResources 配置,用于限制只有申请使用指定资源的 Pod 才会走 Extender 调度逻辑,这样可以减少无意义的调度。

一个带 managedResources 的 KubeSchedulerConfiguration 内容如下

apiVersion: v1
data:config.yaml: |apiVersion: kubescheduler.config.k8s.io/v1kind: KubeSchedulerConfigurationleaderElection:leaderElect: falseprofiles:- schedulerName: hami-schedulerextenders:- urlPrefix: "https://127.0.0.1:443"filterVerb: filterbindVerb: bindnodeCacheCapable: falseenableHTTPS: falsemanagedResources:- name: nvidia.com/gpuignoredByScheduler: true- name: nvidia.com/gpumemignoredByScheduler: true- name: nvidia.com/gpucoresignoredByScheduler: true- name: nvidia.com/gpumem-percentageignoredByScheduler: true- name: nvidia.com/priorityignoredByScheduler: true

在原来的基础上增加了 managedResources 部分的配置

      managedResources:- name: nvidia.com/gpuignoredByScheduler: true- name: nvidia.com/gpumemignoredByScheduler: true- name: nvidia.com/gpucoresignoredByScheduler: true- name: nvidia.com/gpumem-percentageignoredByScheduler: true

只有 Pod 申请这些特殊资源时才走 Extender 调度逻辑,否则使用原生的 Scheduler 调度即可。

ignoredByScheduler: true 的作用是告诉调度器忽略指定资源,避免将它们作为调度决策的依据。也就是说,虽然这些资源(如 GPU 或其他加速硬件)会被 Pod 请求,但调度器不会在选择节点时基于这些资源的可用性做出决定。

ps:因为这些资源可能是虚拟的,并不会真正的出现在 Node 上,因此调度时需要忽略掉,否则就没有任何节点满足条件了,但是这些虚拟资源则是我们的自定义调度逻辑需要考虑的事情。

Scheduler 中的判断逻辑如下,和之前说的一样,只有当 Pod 申请了这些指定的资源时,Scheduler 才会调用 Extender。

// IsInterested returns true if at least one extended resource requested by
// this pod is managed by this extender.
func (h *HTTPExtender) IsInterested(pod *v1.Pod) bool {if h.managedResources.Len() == 0 {return true}if h.hasManagedResources(pod.Spec.Containers) {return true}if h.hasManagedResources(pod.Spec.InitContainers) {return true}return false
}func (h *HTTPExtender) hasManagedResources(containers []v1.Container) bool {for i := range containers {container := &containers[i]for resourceName := range container.Resources.Requests {if h.managedResources.Has(string(resourceName)) {return true}}for resourceName := range container.Resources.Limits {if h.managedResources.Has(string(resourceName)) {return true}}}return false
}

3. Scheduler Extender 规范

Scheduler Extender 通过 HTTP 请求的方式,将调度框架阶段中的调度决策委托给外部的调度器,然后将调度结果返回给调度框架。

我们只需要实现一个 HTTP 服务,然后通过配置文件将其注册到调度器中,就可以实现自定义调度器。

通过 Scheduler Extender 扩展原有调度器一般分为以下两步:

  • 1)创建一个 HTTP 服务,实现对应接口
  • 2)修改调度器配置 KubeSchedulerConfiguration,增加 extenders 相关配置

外置调度器可以影响到三个阶段:

  • Filter:调度框架将调用 Filter 函数,过滤掉不适合被调度的节点。

  • Priority:调度框架将调用 Priority 函数,为每个节点计算一个优先级,优先级越高,节点越适合被调度。

  • Bind:调度框架将调用 Bind 函数,将 Pod 绑定到一个节点上。

Filter、Priority、Bind 三个阶段分别对应三个 HTTP 接口,三个接口都接收 POST 请求,各自的请求、响应结构定义在这里:#kubernetes/kube-scheduler/extender/v1/types.go

在这个 HTTP 服务中,我们可以实现上述阶段中的任意一个或多个阶段的接口,来定制我们的调度需求。

每个接口的请求和响应值请求如下。

Filter

请求参数

// ExtenderArgs represents the arguments needed by the extender to filter/prioritize
// nodes for a pod.
type ExtenderArgs struct {// Pod being scheduledPod *v1.Pod// List of candidate nodes where the pod can be scheduled; to be populated// only if Extender.NodeCacheCapable == falseNodes *v1.NodeList// List of candidate node names where the pod can be scheduled; to be// populated only if Extender.NodeCacheCapable == trueNodeNames *[]string
}

响应结果

// ExtenderFilterResult represents the results of a filter call to an extender
type ExtenderFilterResult struct {// Filtered set of nodes where the pod can be scheduled; to be populated// only if Extender.NodeCacheCapable == falseNodes *v1.NodeList// Filtered set of nodes where the pod can be scheduled; to be populated// only if Extender.NodeCacheCapable == trueNodeNames *[]string// Filtered out nodes where the pod can't be scheduled and the failure messagesFailedNodes FailedNodesMap// Filtered out nodes where the pod can't be scheduled and preemption would// not change anything. The value is the failure message same as FailedNodes.// Nodes specified here takes precedence over FailedNodes.FailedAndUnresolvableNodes FailedNodesMap// Error message indicating failureError string
}

Prioritize

请求参数

// ExtenderArgs represents the arguments needed by the extender to filter/prioritize
// nodes for a pod.
type ExtenderArgs struct {// Pod being scheduledPod *v1.Pod// List of candidate nodes where the pod can be scheduled; to be populated// only if Extender.NodeCacheCapable == falseNodes *v1.NodeList// List of candidate node names where the pod can be scheduled; to be// populated only if Extender.NodeCacheCapable == trueNodeNames *[]string
}

响应结果

// HostPriority represents the priority of scheduling to a particular host, higher priority is better.
type HostPriority struct {// Name of the hostHost string// Score associated with the hostScore int64
}// HostPriorityList declares a []HostPriority type.
type HostPriorityList []HostPriority

Bind

请求参数

// ExtenderBindingArgs represents the arguments to an extender for binding a pod to a node.
type ExtenderBindingArgs struct {// PodName is the name of the pod being boundPodName string// PodNamespace is the namespace of the pod being boundPodNamespace string// PodUID is the UID of the pod being boundPodUID types.UID// Node selected by the schedulerNode string
}

响应结果

// ExtenderBindingResult represents the result of binding of a pod to a node from an extender.
type ExtenderBindingResult struct {// Error message indicating failureError string
}

4. Demo

这部分则是手把手实现一个简单的扩展调度器,完整代码见:lixd/i-scheduler-extender

功能如下:

  • 1)过滤阶段:仅调度到带有 Label priority.lixueduan.com 的节点上
  • 2)打分阶段:直接将节点上 Label priority.lixueduan.com 的值作为得分
    • 比如某节点携带 Label priority.lixueduan.com=50 则打分阶段该节点则是 50 分

代码实现

main.go

比较简单,直接通过内置的 net.http 包启动一个 http 服务即可。

var h *server.Handlerfunc init() {h = server.NewHandler(extender.NewExtender())
}func main() {http.HandleFunc("/filter", h.Filter)http.HandleFunc("/filter_onlyone", h.FilterOnlyOne) // Filter 接口的一个额外实现http.HandleFunc("/priority", h.Prioritize)http.HandleFunc("/bind", h.Bind)http.ListenAndServe(":8080", nil)
}

由于 Priority 阶段返回的得分 kube-Scheduler 会自行汇总后重新计算,因此扩展调度器的 priority 接口并不能安全控制最终调度结果,因此额外实现了一个 filter_onlyone 接口。

Filter 实现

filter 接口用于过滤掉不满足条件的接口,这里直接过滤掉没有指定 label 的节点即可。

// Filter 过滤掉不满足条件的节点
func (ex *Extender) Filter(args extenderv1.ExtenderArgs) *extenderv1.ExtenderFilterResult {nodes := make([]v1.Node, 0)nodeNames := make([]string, 0)for _, node := range args.Nodes.Items {_, ok := node.Labels[Label]if !ok { // 排除掉不带指定标签的节点continue}nodes = append(nodes, node)nodeNames = append(nodeNames, node.Name)}// 没有满足条件的节点就报错if len(nodes) == 0 {return &extenderv1.ExtenderFilterResult{Error: fmt.Errorf("all node do not have label %s", Label).Error()}}args.Nodes.Items = nodesreturn &extenderv1.ExtenderFilterResult{Nodes:     args.Nodes, // 当 NodeCacheCapable 设置为 false 时会使用这个值NodeNames: &nodeNames, // 当 NodeCacheCapable 设置为 true 时会使用这个值}
}

具体返回 Nodes 还是 NodeNames 决定了后续 Scheduler 部署的 NodeCacheCapable 参数的配置,二者对于即可。

Prioritize 实现

Prioritize 对应的就是 Score 阶段,给 Filter 之后留下来的节点打分,选择一个最适合的节点进行调度。

这里的逻辑就是之前说的:解析 Node 上的 label ,直接将其 value 作为节点分数。

// Prioritize 给 Pod 打分
func (ex *Extender) Prioritize(args extenderv1.ExtenderArgs) *extenderv1.HostPriorityList {var result extenderv1.HostPriorityListfor _, node := range args.Nodes.Items {// 获取 Node 上的 Label 作为分数priorityStr, ok := node.Labels[Label]if !ok {klog.Errorf("node %q does not have label %s", node.Name, Label)continue}priority, err := strconv.Atoi(priorityStr)if err != nil {klog.Errorf("node %q has priority %s are invalid", node.Name, priorityStr)continue}result = append(result, extenderv1.HostPriority{Host:  node.Name,Score: int64(priority),})}return &result
}
Bind 实现

就是通过 clientset 创建一个 Binding 对象,指定 Pod 和 Node 信息。

// Bind 将 Pod 绑定到指定节点
func (ex *Extender) Bind(args extenderv1.ExtenderBindingArgs) *extenderv1.ExtenderBindingResult {log.Printf("bind pod: %s/%s to node:%s", args.PodNamespace, args.PodName, args.Node)// 创建绑定关系binding := &corev1.Binding{ObjectMeta: metav1.ObjectMeta{Name: args.PodName, Namespace: args.PodNamespace, UID: args.PodUID},Target:     corev1.ObjectReference{Kind: "Node", APIVersion: "v1", Name: args.Node},}result := new(extenderv1.ExtenderBindingResult)err := ex.ClientSet.CoreV1().Pods(args.PodNamespace).Bind(context.Background(), binding, metav1.CreateOptions{})if err != nil {klog.ErrorS(err, "Failed to bind pod", "pod", args.PodName, "namespace", args.PodNamespace, "podUID", args.PodUID, "node", args.Node)result.Error = err.Error()}return result
}
Filter OnlyOne 实现

Extender 仅作为一个额外的调度插件接入, 接口返回得分最终 Scheduler 会将其和其他插件打分合并之后再选出最终节点,因此 extender 中无法通过 prioritie 接口的分数完全控制调度结果。

不过也不是没有办法!

想要完全控制调度结果,我们可以在 Filter 接口中特殊处理。

Filter 接口先过滤掉不满足条件的节点,然后对剩余节点进行打分,最终只返回得分最高的那个节点,这样就一定会调度到该接口,从而实现完全控制调度结果。

具体实现如下:

// FilterOnlyOne 过滤掉不满足条件的节点,并将其余节点打分排序,最终只返回得分最高的节点以实现完全控制调度结果
func (ex *Extender) FilterOnlyOne(args extenderv1.ExtenderArgs) *extenderv1.ExtenderFilterResult {// 过滤掉不满足条件的节点nodeScores := &NodeScoreList{NodeList: make([]*NodeScore, 0)}for _, node := range args.Nodes.Items {_, ok := node.Labels[Label]if !ok { // 排除掉不带指定标签的节点continue}// 对剩余节点打分score := ComputeScore(node)nodeScores.NodeList = append(nodeScores.NodeList, &NodeScore{Node: node, Score: score})}// 没有满足条件的节点就报错if len(nodeScores.NodeList) == 0 {return &extenderv1.ExtenderFilterResult{Error: fmt.Errorf("all node do not have label %s", Label).Error()}}// 排序sort.Sort(nodeScores)// 然后取最后一个,即得分最高的节点,这样由于 Filter 只返回了一个节点,因此最终肯定会调度到该节点上m := (*nodeScores).NodeList[len((*nodeScores).NodeList)-1]// 组装一下返回结果args.Nodes.Items = []v1.Node{m.Node}return &extenderv1.ExtenderFilterResult{Nodes:     args.Nodes,NodeNames: &[]string{m.Node.Name},}
}

部署

构建镜像

Dockerfile 如下:

# syntax=docker/dockerfile:1# Build the manager binary
FROM golang:1.22.5 as builder
ARG TARGETOS
ARG TARGETARCHENV GOPROXY=https://goproxy.cnWORKDIR /workspace
# Copy the go source
COPY . /workspace
# cache deps before building and copying source so that we don't need to re-download as much
# and so that source changes don't invalidate our downloaded layer
RUN go mod download# Build
# the GOARCH has not a default value to allow the binary be built according to the host where the command
# was called. For example, if we call make docker-build in a local env which has the Apple Silicon M1 SO
# the docker BUILDPLATFORM arg will be linux/arm64 when for Apple x86 it will be linux/amd64. Therefore,
# by leaving it empty we can ensure that the container and binary shipped on it will have the same platform.
RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -o extender main.go# Use distroless as minimal base image to package the manager binary
# Refer to https://github.com/GoogleContainerTools/distroless for more details
#FROM gcr.io/distroless/static:nonroot
FROM busybox:1.36
WORKDIR /
COPY --from=builder /workspace/extender .
USER 65532:65532ENTRYPOINT ["/extender"]
部署到集群

部分也是分为两步:

  • 1)部署 Extender:由于 Extender 只是一个 HTTP 服务器,只需要使用 Deployment 将其部署到集群即可。

  • 2)配置 Extender:修改调度器的 KubeSchedulerConfiguration 配置,在其中 extender 部分指定 url 以及对应的 path,进行接入。

这里为了不影响到默认的 kube-scheduler,我们使用 kube-scheduler 镜像单独启动一个 Scheduler,然后为该调度器配置上 Extender,同时为了降低网络请求的影响,直接将 kube-scheduler 和 Extender 直接运行在同一个 Pod 里,通过 localhost 进行访问。

完整 yaml 如下:

apiVersion: v1
kind: ServiceAccount
metadata:name: i-scheduler-extendernamespace: kube-system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:name: i-scheduler-extender
subjects:- kind: ServiceAccountname: i-scheduler-extendernamespace: kube-system
roleRef:kind: ClusterRolename: cluster-adminapiGroup: rbac.authorization.k8s.io
---
apiVersion: v1
kind: ConfigMap
metadata:name: i-scheduler-extendernamespace: kube-system
data:i-scheduler-extender.yaml: |apiVersion: kubescheduler.config.k8s.io/v1kind: KubeSchedulerConfigurationprofiles:- schedulerName: i-scheduler-extenderleaderElection:leaderElect: falseextenders:- urlPrefix: "http://localhost:8080"enableHTTPS: falsefilterVerb: "filter"prioritizeVerb: "prioritize"bindVerb: "bind"weight: 1nodeCacheCapable: true
---
apiVersion: apps/v1
kind: Deployment
metadata:labels:component: i-scheduler-extendertier: control-planename: i-scheduler-extendernamespace: kube-system
spec:replicas: 1selector:matchLabels:component: i-scheduler-extendertier: control-planetemplate:metadata:labels:component: i-scheduler-extendertier: control-planespec:serviceAccountName: i-scheduler-extendercontainers:- name: kube-schedulerimage: registry.k8s.io/kube-scheduler:v1.29.0command:- kube-scheduler- --config=/etc/kubernetes/i-scheduler-extender.yamllivenessProbe:httpGet:path: /healthzport: 10259scheme: HTTPSinitialDelaySeconds: 15readinessProbe:httpGet:path: /healthzport: 10259scheme: HTTPSresources:requests:cpu: '0.1'volumeMounts:- name: config-volumemountPath: /etc/kubernetes- name: i-scheduler-extenderimage: lixd96/i-scheduler-extender:v1ports:- containerPort: 8080volumes:- name: config-volumeconfigMap:name: i-scheduler-extender
kubectl apply -f deploy

确认服务正常运行

[root@scheduler-1 ~]# kubectl -n kube-system get po|grep i-scheduler-extender
i-scheduler-extender-f9cff954c-dkwz2   2/2     Running   0          1m

接下来就可以开始测试了。

测试

创建 Pod

创建一个 Deployment 并指定使用上一步中部署的 Scheduler,然后测试会调度到哪个节点上。

apiVersion: apps/v1
kind: Deployment
metadata:name: test
spec:replicas: 1selector:matchLabels:app: testtemplate:metadata:labels:app: testspec:schedulerName: i-scheduler-extendercontainers:- image: busybox:1.36name: nginxcommand: ["sleep"]         args: ["99999"]

创建之后 Pod 会一直处于 Pending 状态

[root@scheduler-1 lixd]# k get po
NAME                    READY   STATUS    RESTARTS   AGE
test-58794bff9f-ljxbs   0/1     Pending   0          17s

查看具体情况

[root@scheduler-1]# k describe po test-58794bff9f-ljxbs
Events:Type     Reason            Age                From                  Message----     ------            ----               ----                  -------Warning  FailedScheduling  99s                i-scheduler-extender  all node do not have label priority.lixueduan.comWarning  FailedScheduling  95s (x2 over 97s)  i-scheduler-extender  all node do not have label priority.lixueduan.com

可以看到,是因为 Node 上没有我们定义的 Label,因此都不满足条件,最终 Pod 就一直 Pending 了。

添加 Label

由于我们实现的 Filter 逻辑是需要 Node 上有priority.lixueduan.com 才会用来调度,否则直接会忽略。

理论上,只要给任意一个 Node 打上 Label 就可以了。

[root@scheduler-1 install]# k get node
NAME          STATUS   ROLES           AGE     VERSION
scheduler-1   Ready    control-plane   4h34m   v1.27.4
scheduler-2   Ready    <none>          4h33m   v1.27.4
[root@scheduler-1 install]# k label node scheduler-1 priority.lixueduan.com=10
node/scheduler-1 labeled

再次查看 Pod 状态

[root@scheduler-1 lixd]# k get po -owide
NAME                    READY   STATUS    RESTARTS   AGE    IP               NODE          NOMINATED NODE   READINESS GATES
test-58794bff9f-ljxbs   1/1     Running   0          104s   172.25.123.201   scheduler-1   <none>           <none>

已经被调度到 node1 上了,查看详细日志

[root@scheduler-1 install]# k describe po test-7f7bb8f449-w6wvv
Events:Type     Reason            Age                  From                  Message----     ------            ----                 ----                  -------Warning  FailedScheduling  116s                 i-scheduler-extender  0/2 nodes are available: preemption: 0/2 nodes are available: 2 No preemption victims found for incoming pod.Warning  FailedScheduling  112s (x2 over 115s)  i-scheduler-extender  0/2 nodes are available: preemption: 0/2 nodes are available: 2 No preemption victims found for incoming pod.Normal   Scheduled         26s                  i-scheduler-extender  Successfully assigned default/test-58794bff9f-ljxbs to scheduler-1

可以看到,确实是 i-scheduler-extender 这个调度器在处理,调度到了 node1.

多节点排序

我们实现的 Score 是根据 Node 上的 priority.lixueduan.com 对应的 Value 作为得分的,因此调度器会优先考虑调度到 Value 比较大的一个节点。

因为 Score 阶段也有很多调度插件,Scheduler 会汇总所有得分,最终才会选出结果,因此这里的分数也是仅供参考,不能完全控制调度结果。

给 node2 也打上 label,value 设置为 20

[root@scheduler-1 install]# k get node
NAME          STATUS   ROLES           AGE     VERSION
scheduler-1   Ready    control-plane   4h34m   v1.27.4
scheduler-2   Ready    <none>          4h33m   v1.27.4
[root@scheduler-1 install]# k label node scheduler-2 priority.lixueduan.com=20
node/scheduler-2 labeled

然后更新 Deployment ,触发创建新 Pod ,测试调度逻辑。

[root@scheduler-1 lixd]# kubectl rollout restart deploy test
deployment.apps/test restarted

因为 Node2 上的 priority 为 20,node1 上为 10,那么理论上会调度到 node2 上。

[root@scheduler-1 lixd]# k get po -owide
NAME                    READY   STATUS    RESTARTS   AGE   IP             NODE          NOMINATED NODE   READINESS GATES
test-84fdbbd8c7-47mtr   1/1     Running   0          38s   172.25.0.162   scheduler-1   <none>           <none>

结果还是调度到了 node1,为什么呢?

这就是前面提到的:因为 Extender 仅作为一个额外的调度插件接入,Prioritize 接口返回得分最终 Scheduler 会将其和其他插件打分合并之后再选出最终节点,因此 Extender 想要完全控制调度结果,只能在 Filter 接口中实现,过滤掉不满足条件的节点,并对剩余节点进行打分,最终 Filter 接口只返回得分最高的那个节点,从而实现完全控制调度结果。

ps:即之前的 Filter OnlyOne 实现,可以在 KubeSchedulerConfiguration 中配置不同的 path 来调用不同接口进行测试。

修改 KubeSchedulerConfiguration 配置,

apiVersion: v1
kind: ConfigMap
metadata:name: i-scheduler-extendernamespace: kube-system
data:i-scheduler-extender.yaml: |apiVersion: kubescheduler.config.k8s.io/v1kind: KubeSchedulerConfigurationprofiles:- schedulerName: i-scheduler-extenderleaderElection:leaderElect: falseextenders:- urlPrefix: "http://localhost:8080"enableHTTPS: falsefilterVerb: "filter_onlyone"prioritizeVerb: "prioritize"bindVerb: "bind"weight: 1nodeCacheCapable: true

修改点:

filterVerb: "filter_onlyone"

Path 从 filter 修改成了 filter_onlyone,这里的 path 和前面注册服务时的路径对应:

    http.HandleFunc("/filter", h.Filter)http.HandleFunc("/filter_onlyone", h.FilterOnlyOne) // Filter 接口的一个额外实现

修改后重启一下 Scheduler

kubectl -n kube-system rollout restart deploy i-scheduler-extender

再次更新 Deployment 触发调度

[root@scheduler-1 install]# k rollout restart deploy test
deployment.apps/test restarted

这样应该是调度到 node2 了,确认一下

[root@scheduler-1 lixd]# k get po -owide
NAME                    READY   STATUS    RESTARTS   AGE   IP             NODE          NOMINATED NODE   READINESS GATES
test-849f549d5b-pbrml   1/1     Running       0          12s   172.25.0.166     scheduler-2   <none>           <none>

现在我们更新 Node1 的 label,改成 30

k label node scheduler-1 priority.lixueduan.com=30 --overwrite

再次更新 Deployment 触发调度

[root@scheduler-1 install]# k rollout restart deploy test
deployment.apps/test restarted

这样应该是调度到 node1 了,确认一下

[root@scheduler-1 lixd]# k get po -owide
NAME                    READY   STATUS        RESTARTS   AGE   IP               NODE          NOMINATED NODE   READINESS GATES
test-69d9ccb877-9fb6t   1/1     Running       0          5s    172.25.123.203   scheduler-1   <none>           <none>

说明修改 Filter 方法实现之后,确实可以直接控制调度结果。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/diannao/91843.shtml
繁体地址,请注明出处:http://hk.pswp.cn/diannao/91843.shtml
英文地址,请注明出处:http://en.pswp.cn/diannao/91843.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

7.19 换根dp | vpp |滑窗

lcr147.最小栈通过两个栈 维护实现class MinStack { public:stack<int> A, B;MinStack() {}void push(int x) {A.push(x);if(B.empty() || B.top() > x)B.push(x);}void pop() {if(A.top() B.top())B.pop();A.pop();}int top() {return A.top();}int getMin() {retur…

以太坊的心脏与大脑:详解执行客户端(EL)与共识客户端(CL)

好的&#xff0c;各位技术同道&#xff0c;欢迎再次光临我的博客。在上一篇文章中&#xff0c;我们聊了如何搭建一个以太坊测试节点&#xff0c;并提到了节点需要同时运行“执行客户端”和“共识客户端”。很多朋友对此表示了浓厚兴趣&#xff0c;想深入了解这两者究竟是什么&a…

Debian-10,用glibc二进制预编译包,安装Mysql-5.7.44 笔记250716

Debian-10,用glibc二进制预编译包,安装Mysql-5.7.44 笔记250716 &#x1f4e6; 一步脚本 #!/bin/bash### 安装依赖 apt install -y libaio1 libnuma1 libncurses5### 下载MySQL-5.7.44 的 glib二进制包: mysql-5.7.44-linux-glibc2.12-x86_64.tar.gz ,(如果不存在) mkdir…

用逻辑回归(Logistic Regression)处理鸢尾花(iris)数据集

# 导入必要的库 import numpy as np import pandas as pd import matplotlib.pyplot as plt import seaborn as sns from sklearn.datasets import load_iris from sklearn.model_selection import train_test_split from sklearn.linear_model import LogisticRegression from…

华大北斗TAU1201-1216A00高精度双频GNSS定位模块 自动驾驶专用

在万物互联的时代&#xff0c;您还在为定位不准、信号丢失而烦恼吗&#xff1f;TAU1201-1216A00华大北斗高精度定位模块TAU1201是一款高性能的双频GNSS定位模块&#xff0c;搭载了华大北斗的CYNOSURE III GNSS SoC 芯片&#xff0c;该模块支持新一代北斗三号信号体制&#xff0…

坚持继续布局32位MCU,进一步完善产品阵容,96Mhz主频CW32L012新品发布!

在全球MCU市场竞争加剧、国产替代加速的背景下&#xff0c;嵌入式设备对核心控制芯片的性能、功耗、可靠性及性价比提出了前所未有的严苛需求。为适应市场竞争&#xff0c;2025年7月16日&#xff0c;武汉芯源半导体正式推出基于CW32L01x系列低功耗微控制器家族的全新成员&#…

用线性代数推导码分多址(CDMA)

什么是码分多址 码分多址&#xff1a;CDMA允许多个用户同时、在同一频率上传输数据。它通过给每个用户分配唯一的、相互正交的二进制序列来实现区分。用户的数据比特被这个码片序列扩展成一个高速率的信号&#xff0c;然后在接收端通过相同的码片序列进行相关运算来回复原数据 …

mac 配置svn

1.查看brew的版本&#xff1a;brew install subversion2.安装brew命令&#xff1a;bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"3.把路径添加到path环境变量&#xff1a;echo export PATH"/opt/homebrew/b…

使用 .NET Core 的原始 WebSocket

在 Web 开发中&#xff0c;后端存在一些值得注意的通信协议&#xff0c;用于将更改通知给已连接的客户端。所有这些协议都用于处理同一件事。但鲜为人知的协议很少&#xff0c;鲜为人知的协议也很少。今天&#xff0c;将讨论 WebSocket&#xff0c;它在开发中使用最少&#xff…

编程实现Word自动排版:从理论到实践的全面指南

在现代办公环境中&#xff0c;文档排版是一项常见但耗时的工作。特别是对于需要处理大量文档的专业人士来说&#xff0c;手动排版不仅费时费力&#xff0c;还容易出现不一致的问题。本文将深入探讨如何通过编程方式实现Word文档的自动排版&#xff0c;从理论基础到实际应用&…

力扣经典算法篇-25-删除链表的倒数第 N 个结点(计算链表的长度,利用栈先进后出特性,双指针法)

1、题干 给你一个链表&#xff0c;删除链表的倒数第 n 个结点&#xff0c;并且返回链表的头结点。 示例 1&#xff1a;输入&#xff1a;head [1,2,3,4,5], n 2 输出&#xff1a;[1,2,3,5] 示例 2&#xff1a; 输入&#xff1a;head [1], n 1 输出&#xff1a;[] 示例 3&…

VIT速览

当我们取到一张图片&#xff0c;我们会把它划分为一个个patch&#xff0c;如上图把一张图片划分为了9个patch&#xff0c;然后通过一个embedding把他们转换成一个个token&#xff0c;每个patch对应一个token&#xff0c;然后在输入到transformer encoder之前还要经过一个class …

【服务器与部署 14】消息队列部署:RabbitMQ、Kafka生产环境搭建指南

【服务器与部署 14】消息队列部署&#xff1a;RabbitMQ、Kafka生产环境搭建指南 关键词&#xff1a;消息队列、RabbitMQ集群、Kafka集群、消息中间件、异步通信、微服务架构、高可用部署、消息持久化、生产环境配置、分布式系统 摘要&#xff1a;本文从实际业务场景出发&#x…

LeetCode中等题--167.两数之和II-输入有序数组

1. 题目 给你一个下标从 1 开始的整数数组 numbers &#xff0c;该数组已按 非递减顺序排列 &#xff0c;请你从数组中找出满足相加之和等于目标数 target 的两个数。如果设这两个数分别是 numbers[index1] 和 numbers[index2] &#xff0c;则 1 < index1 < index2 <…

【C# in .NET】19. 探秘抽象类:具体实现与抽象契约的桥梁

探秘抽象类:具体实现与抽象契约的桥梁 在.NET类型系统中,抽象类是连接具体实现与抽象契约的关键桥梁,它既具备普通类的状态承载能力,又拥有类似接口的行为约束特性。本文将从 IL 代码结构、CLR 类型加载机制、方法调度逻辑三个维度,全面揭示抽象类的底层工作原理,通过与…

Apache RocketMQ + “太乙” = 开源贡献新体验

Apache RocketMQ 是 Apache 基金会托管的顶级项目&#xff0c;自 2012 年诞生于阿里巴巴&#xff0c;服务于淘宝等核心交易系统&#xff0c;历经多次双十一万亿级数据洪峰稳定性验证&#xff0c;至今已有十余年发展历程。RocketMQ 致力于构建低延迟、高并发、高可用、高可靠的分…

永磁同步电机控制算法--弱磁控制(变交轴CCR-VQV)

一、原理介绍CCR-FQV弱磁控制不能较好的利用逆变器的直流侧电压&#xff0c;造成电机的调速范围窄、效率低和带载能力差。为了解决CCR-FQV弱磁控制存在的缺陷&#xff0c;可以在电机运行过程中根据工况的不同实时的改变交轴电压给定uq的值&#xff0c;实施 CCR-VQV弱磁控制。…

达梦数据守护集群搭建(1主1实时备库1同步备库1异步备库)

目录 1 环境信息 1.1 目录信息 1.2 其他环境信息 2 环境准备 2.1 新建dmdba用户 2.2 关闭防火墙 2.3 关闭Selinux 2.4 关闭numa和透明大页 2.5 修改文件打开最大数 2.6 修改磁盘调度 2.7 修改cpufreq模式 2.8 信号量修改 2.9 修改sysctl.conf 2.10 修改 /etc/sy…

电感与电容充、放电极性判断和电感选型

目录 一、电感 二、电容 三、电感选型 一、电感 充电&#xff1a;左右-为例 放电&#xff1a;极性相反&#xff0c;左-右 二、电容 充电&#xff1a;左右-为例 放电&#xff1a;左右-&#xff08;与充电极性一致&#xff09; 三、电感选型 主要考虑额定电流和饱和电流。…

新建模范式Mamba——“Selectivity is All You Need?”

目录 一、快速走进和理解Mamba建模架构 &#xff08;一&#xff09;从Transformer的统治地位谈起 &#xff08;二&#xff09;另一条道路&#xff1a;结构化状态空间模型&#xff08;SSM&#xff09; &#xff08;三&#xff09;Mamba 的核心创新&#xff1a;Selective SSM…