4. 调度器扩展:自定义调度器、Scheduler Framework、调度插件开发

Kubernetes 默认调度器(kube-scheduler)通过预选(Predicates)和优选(Priorities)算法为 Pod 选择最合适的 Node。但在多租户、GPU 拓扑感知、数据本地性等复杂场景下,原生调度策略无法满足需求。本章深入解析三种扩展路径:独立自定义调度器、Scheduler Framework 插件化架构、以及生产级调度插件开发实践。

4.1 三种调度扩展路径对比

扩展方式 架构模式 调度决策权 维护成本 典型场景
独立自定义调度器 独立 Deployment,监听 Pod 的 spec.schedulerName 完全自主 高(需自行实现调度逻辑、缓存、绑定) 批处理作业调度、特定硬件调度
Scheduler Framework 插件注入到 kube-scheduler 的扩展点 与默认调度器协作 中(只需实现特定接口) 多租户分区、拓扑感知、动态资源定价
调度插件开发 基于 Framework 的细粒度扩展 按扩展点局部干预 低(复用默认调度器大部分逻辑) 自定义过滤、打分、排序、预留
选型建议: 对于多租户发布窗口隔离场景,优先选择 Scheduler Framework 方式。它既能复用 kube-scheduler 的成熟调度循环,又能在 PreFilter 或 Filter 扩展点注入租户配额检查逻辑,避免重复造轮子。

4.2 Scheduler Framework 核心架构

Scheduler Framework 将调度周期拆分为多个可插拔的扩展点(Extension Points)。每个扩展点允许开发者注册自定义插件,在调度循环的特定阶段执行逻辑。

Scheduler Framework 扩展点与插件执行流程 调度队列(Scheduling Queue) 等待调度的 Pod 按优先级排序 过滤阶段(Filtering) PreFilter → Filter → PostFilter 排除不满足条件的 Node,如资源不足、租户隔离规则 扩展点:PreFilterPlugin, FilterPlugin, PostFilterPlugin 打分阶段(Scoring) PreScore → Score → NormalizeScore 对候选 Node 进行加权评分,选择最优节点 绑定阶段(Binding) Reserve → Permit → PreBind → Bind → PostBind 插件可注册到任意一个或多个扩展点,实现细粒度的调度干预

4.3 自定义调度器实现模式

当 Scheduler Framework 无法满足需求时(如需要完全独立的调度策略),可以部署独立的自定义调度器。以下是一个最小化自定义调度器的核心逻辑示例:

// 自定义调度器核心循环(伪代码)
package main

import (
    "context"
    "fmt"
    v1 "k8s.io/api/core/v1"
    "k8s.io/client-go/kubernetes"
    "k8s.io/client-go/tools/cache"
)

type CustomScheduler struct {
    client kubernetes.Interface
}

func (s *CustomScheduler) Run(ctx context.Context) {
    // 1. 监听未调度的 Pod(spec.schedulerName == "custom-scheduler")
    podInformer := cache.NewSharedIndexInformer(...)
    podInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{
        AddFunc: func(obj interface{}) {
            pod := obj.(*v1.Pod)
            if pod.Spec.SchedulerName != "custom-scheduler" {
                return
            }
            // 2. 执行自定义调度算法
            nodeName := s.schedulePod(pod)
            // 3. 通过 API 绑定 Pod 到 Node
            s.bindPod(pod, nodeName)
        },
    })
    go podInformer.Run(ctx.Done())
}

func (s *CustomScheduler) schedulePod(pod *v1.Pod) string {
    // 自定义调度逻辑:例如按租户 ID 分配到固定 Node 池
    tenantID := pod.Labels["tenant-id"]
    nodePool := getNodePoolForTenant(tenantID)
    // 选择负载最低的 Node
    return selectBestNode(nodePool)
}
生产注意事项: 独立自定义调度器需要自行处理 Pod 的 抢占(Preemption)、调度重试、缓存一致性 和 Leader Election。建议优先评估 Scheduler Framework 是否能满足需求,避免重复造轮子。

4.4 调度插件开发实战:多租户隔离插件

以多租户发布窗口隔离为例,开发一个 TenantFilterPlugin,确保在发布窗口期间,只有指定租户的 Pod 能被调度到目标 Node 池。

4.4.1 插件注册与配置

// 1. 实现 FilterPlugin 接口
type TenantFilterPlugin struct {
    handle framework.Handle
}

// Name 返回插件名称
func (pl *TenantFilterPlugin) Name() string {
    return "TenantFilter"
}

// Filter 在过滤阶段执行:检查 Pod 的租户标签是否与 Node 的租户池匹配
func (pl *TenantFilterPlugin) Filter(ctx context.Context, state *framework.CycleState, pod *v1.Pod, nodeInfo *framework.NodeInfo) *framework.Status {
    podTenant := pod.Labels["tenant-id"]
    if podTenant == "" {
        return framework.NewStatus(framework.Unschedulable, "Pod missing tenant-id label")
    }

    node := nodeInfo.Node()
    nodeTenantPool := node.Labels["tenant-pool"]
    if nodeTenantPool == "" {
        // Node 未标记租户池,允许调度(或根据策略拒绝)
        return framework.NewStatus(framework.Success, "")
    }

    // 检查 Pod 租户是否属于该 Node 的租户池
    if !isTenantInPool(podTenant, nodeTenantPool) {
        return framework.NewStatus(framework.Unschedulable,
            fmt.Sprintf("Pod tenant %s not allowed in node pool %s", podTenant, nodeTenantPool))
    }
    return framework.NewStatus(framework.Success, "")
}

// 2. 在 main.go 中注册插件
func main() {
    command := app.NewSchedulerCommand(
        app.WithPlugin("TenantFilter", func(plArgs runtime.Object, f framework.Handle) (framework.Plugin, error) {
            return &TenantFilterPlugin{handle: f}, nil
        }),
    )
    if err := command.Execute(); err != nil {
        os.Exit(1)
    }
}

4.4.2 调度器配置(KubeSchedulerConfiguration)

apiVersion: kubescheduler.config.k8s.io/v1
kind: KubeSchedulerConfiguration
profiles:
  - schedulerName: tenant-aware-scheduler
    plugins:
      filter:
        enabled:
          - name: TenantFilter
        disabled:
          - name: NodeUnschedulable  # 可选:禁用默认插件
      score:
        enabled:
          - name: NodeResourcesBalancedAllocation
          - name: NodeResourcesLeastAllocated
    pluginConfig:
      - name: TenantFilter
        args:
          # 插件自定义参数(可选)
          defaultPolicy: "allow"

4.5 调度插件开发最佳实践

  • 幂等性: 插件的 Filter/Score 方法必须幂等,多次调用返回相同结果。
  • 性能敏感: 避免在 Filter 中执行网络调用或复杂计算;如需外部数据,在 PreFilter 中预加载并缓存到 CycleState。
  • 错误处理: 返回 framework.Unschedulable 状态时,附带清晰的错误信息,便于调度器记录事件。
  • 版本兼容: 使用 k8s.io/kubernetes/pkg/scheduler/framework 的稳定接口,避免依赖内部实现。
  • 测试覆盖: 利用 framework.FrameworkHandle 的 Mock 实现编写单元测试,覆盖正常、边界和异常场景。

4.6 调度扩展选型决策树

调度扩展选型决策树 需要自定义调度逻辑? 是:需要完全自主调度 独立自定义调度器 否:复用默认调度器 Scheduler Framework 开发调度插件 多租户场景推荐路径:Scheduler Framework → 开发 Filter/Score 插件
核心结论: 对于多租户发布窗口隔离场景,Scheduler Framework + 自定义 Filter 插件 是最优解。它既能利用 kube-scheduler 成熟的调度循环和抢占机制,又能通过 Filter 扩展点注入租户隔离规则,实现“零侵入”的调度策略扩展。