4. 自定义调度器:编写自定义调度器框架、调度器扩展点、调度器插件机制

Kubernetes 默认调度器(kube-scheduler)通过一系列内置的 调度插件 完成 Pod 与 Node 的绑定决策。但在多租户隔离、GPU 拓扑感知、窗口数据传输窗口抢占等复杂场景下,默认调度策略无法满足需求。本章深入自定义调度器的三种实现路径:框架扩展、扩展点注入、插件机制,并给出生产级代码骨架。

4.1 自定义调度器架构总览

Kubernetes 调度框架(Scheduling Framework)将调度周期拆解为多个可插拔的 扩展点(Extension Points)。自定义调度器通过实现这些扩展点的接口,以插件形式注入调度流程,无需重写整个调度器。

核心设计原则: 调度器本身只负责流程编排,所有决策逻辑由插件完成。插件可以组合、排序、禁用,实现高度定制化。
自定义调度器框架架构图 自定义调度器框架架构图 kube-scheduler 主流程 调度扩展点(Extension Points) QueueSort | PreFilter | Filter | PostFilter | PreScore | Score | NormalizeScore | Reserve | Permit | PreBind | Bind | PostBind 调度插件(Plugins) NodeResourcesFit NodePorts InterPodAffinity 自定义插件 窗口数据感知 自定义插件只需实现对应扩展点接口,即可注入调度流程 无需修改 kube-scheduler 主程序代码 Pod → Node 绑定 调度周期:QueueSort → PreFilter → Filter → PostFilter → PreScore → Score → NormalizeScore → Reserve → Permit → PreBind → Bind → PostBind

4.2 调度器扩展点详解

调度框架定义了 12 个扩展点,每个扩展点对应调度周期中的一个阶段。自定义插件可以选择实现其中任意一个或多个扩展点。

扩展点 阶段 作用 典型插件
QueueSort 排序 对调度队列中的 Pod 进行排序 PrioritySort
PreFilter 预过滤 预处理 Pod 信息,检查 Pod 是否可调度 NodeResourcesFit
Filter 过滤 过滤掉不满足条件的 Node NodePorts, NodeAffinity
PostFilter 后过滤 当所有 Node 都被过滤时,执行抢占或替代方案 DefaultPreemption
PreScore 预评分 在评分前进行数据准备 PodTopologySpread
Score 评分 对通过过滤的 Node 进行打分(0-100) NodeResourcesBalancedAllocation
NormalizeScore 归一化 将多个插件的评分结果归一化到统一范围 ImageLocality
Reserve 预留 在绑定前预留 Node 资源,防止并发冲突 VolumeBinding
Permit 许可 批准或拒绝 Pod 调度,支持等待/超时机制 InterPodAffinity
PreBind 预绑定 在绑定前执行准备工作(如挂载卷) VolumeBinding
Bind 绑定 将 Pod 绑定到 Node(写入 etcd) DefaultBinder
PostBind 后绑定 绑定后的清理或通知 VolumeBinding
窗口数据传输场景提示: 在 GitOps 窗口数据传输中,建议在 Filter 扩展点实现“窗口时间检查”,在 Score 扩展点实现“数据传输带宽评分”,在 Permit 扩展点实现“窗口并发控制”。

4.3 自定义调度器插件开发框架

编写自定义调度器插件需要实现 schedulerframework.Plugin 接口,并根据需要实现对应的扩展点接口。以下是一个完整的插件骨架示例:

package windowtransferscheduler

import (
    "context"
    "fmt"
    "k8s.io/kubernetes/pkg/scheduler/framework"
)

// 插件名称,在调度器配置中引用
const Name = "WindowTransfer"

// 插件结构体
type WindowTransfer struct {
    handle framework.Handle
    // 自定义配置:窗口时间窗口、带宽阈值等
    windowStart string
    windowEnd   string
    bandwidthThreshold int64
}

// 确保实现了 framework.Plugin 接口
var _ framework.Plugin = &WindowTransfer{}

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

// 实现 Filter 扩展点
// 在 Filter 阶段检查当前时间是否在数据传输窗口内
func (pl *WindowTransfer) Filter(ctx context.Context, 
    state *framework.CycleState, 
    pod *v1.Pod, 
    nodeInfo *framework.NodeInfo) *framework.Status {
    
    // 检查节点是否满足窗口数据传输条件
    if !pl.isInWindow() {
        return framework.NewStatus(framework.Unschedulable, 
            "当前时间不在数据传输窗口内")
    }
    
    // 检查节点带宽是否满足 Pod 需求
    if nodeInfo.Allocatable.MilliCPU < pl.bandwidthThreshold {
        return framework.NewStatus(framework.Unschedulable, 
            "节点带宽不足")
    }
    
    return framework.NewStatus(framework.Success, "")
}

// 实现 Score 扩展点
// 根据节点剩余带宽和窗口利用率进行评分
func (pl *WindowTransfer) Score(ctx context.Context, 
    state *framework.CycleState, 
    pod *v1.Pod, 
    nodeName string) (int64, *framework.Status) {
    
    nodeInfo, err := pl.handle.SnapshotSharedLister().NodeInfos().Get(nodeName)
    if err != nil {
        return 0, framework.NewStatus(framework.Error, 
            fmt.Sprintf("获取节点信息失败: %v", err))
    }
    
    // 评分逻辑:剩余带宽越高,分数越高
    availableBandwidth := nodeInfo.Allocatable.MilliCPU - 
        nodeInfo.Requested.MilliCPU
    score := (availableBandwidth * 100) / pl.bandwidthThreshold
    
    if score > 100 {
        score = 100
    }
    
    return score, framework.NewStatus(framework.Success, "")
}

// 实现 NormalizeScore 扩展点(可选)
func (pl *WindowTransfer) NormalizeScore(ctx context.Context, 
    state *framework.CycleState, 
    pod *v1.Pod, 
    scores framework.NodeScoreList) *framework.Status {
    
    // 归一化处理,确保所有分数在 0-100 之间
    return framework.NewStatus(framework.Success, "")
}

// 辅助方法:检查当前时间是否在窗口内
func (pl *WindowTransfer) isInWindow() bool {
    // 实现窗口时间检查逻辑
    return true
}

// 插件构造函数
func New(obj runtime.Object, handle framework.Handle) (framework.Plugin, error) {
    // 解析配置
    config, ok := obj.(*WindowTransferArgs)
    if !ok {
        return nil, fmt.Errorf("期望 WindowTransferArgs,得到 %T", obj)
    }
    
    return &WindowTransfer{
        handle:             handle,
        windowStart:        config.WindowStart,
        windowEnd:          config.WindowEnd,
        bandwidthThreshold: config.BandwidthThreshold,
    }, nil
}

4.4 插件注册与调度器配置

自定义插件编写完成后,需要通过 调度器配置 注册并启用。Kubernetes 1.19+ 支持通过 KubeSchedulerConfiguration 声明式配置插件。

4.4.1 插件注册

在调度器主程序中注册自定义插件:

package main

import (
    "k8s.io/kubernetes/cmd/kube-scheduler/app"
    "k8s.io/kubernetes/pkg/scheduler/framework"
    
    // 引入自定义插件包
    windowtransferscheduler "example.com/window-transfer-scheduler"
)

func main() {
    // 注册自定义插件
    cmd := app.NewSchedulerCommand(
        app.WithPlugin(windowtransferscheduler.Name, 
            windowtransferscheduler.New),
    )
    
    if err := cmd.Execute(); err != nil {
        panic(err)
    }
}

4.4.2 调度器配置文件

通过 YAML 配置文件指定插件的启用顺序和权重:

apiVersion: kubescheduler.config.k8s.io/v1
kind: KubeSchedulerConfiguration
profiles:
  - schedulerName: window-transfer-scheduler
    plugins:
      # 在 Filter 阶段启用自定义插件
      filter:
        enabled:
          - name: "WindowTransfer"
        disabled:
          - name: "NodeResourcesFit"  # 可选:禁用默认插件
      # 在 Score 阶段启用自定义插件,权重为 3
      score:
        enabled:
          - name: "WindowTransfer"
            weight: 3
      # 在 Permit 阶段启用自定义插件
      permit:
        enabled:
          - name: "WindowTransfer"
    pluginConfig:
      - name: "WindowTransfer"
        args:
          windowStart: "22:00"
          windowEnd: "06:00"
          bandwidthThreshold: 1000  # 单位:mCPU
重要: 插件配置中的 schedulerName 必须与 Pod 的 spec.schedulerName 字段一致,否则 Pod 不会使用自定义调度器。

4.5 调度器扩展点执行流程

以下 SVG 图展示了自定义插件在调度周期中的完整执行流程,以及各扩展点的调用顺序:

调度器扩展点执行流程 调度器扩展点执行流程(含自定义插件注入点) 1. QueueSort 2. PreFilter 3. Filter ★ 自定义插件 窗口时间检查 + 带宽过滤 通过? PostFilter 抢占 4. PreScore 5. Score ★ 自定义插件 带宽评分 + 窗口利用率评分 6. NormalizeScore 7. Reserve 8. Permit ★ 自定义插件 窗口并发控制 + 超时等待 ★ 标记为自定义插件推荐注入的扩展点,可根据业务需求选择实现

4.6 多调度器部署与选择

Kubernetes 支持集群中同时运行多个调度器实例。Pod 通过 spec.schedulerName 字段选择使用哪个调度器。

调度器名称 调度器类型 适用场景
default-scheduler 内置默认调度器 通用工作负载
window-transfer-scheduler 自定义调度器 窗口数据传输、带宽敏感型 Pod
gpu-scheduler 自定义调度器 GPU 拓扑感知调度
生产实践: 在 GitOps 窗口数据传输场景中,建议将窗口数据传输相关的 Pod 通过 schedulerName: window-transfer-scheduler 指定使用自定义调度器,其他 Pod 继续使用默认调度器,实现调度策略的精细化隔离。

4.7 自定义调度器调试与验证

部署自定义调度器后,通过以下步骤验证其正确性:

  1. 检查调度器 Pod 日志: kubectl logs -n kube-system <scheduler-pod>,确认插件加载成功。
  2. 查看调度事件: kubectl describe pod <pod-name>,在 Events 中查看调度决策。
  3. 使用调度器指标: 自定义调度器暴露 Prometheus 指标,监控 scheduler_binding_latency_microseconds 和 scheduler_e2e_scheduling_latency_microseconds。
  4. 单元测试: 使用 schedulerframework.Framework 的测试框架编写插件单元测试。
// 插件单元测试示例
func TestWindowTransferFilter(t *testing.T) {
    // 创建测试框架
    f, _ := framework.NewFramework(
        []framework.Plugin{&WindowTransfer{windowStart: "22:00", windowEnd: "06:00"}},
        framework.WithSnapshotSharedLister(testNodeInfo()),
    )
    
    // 创建测试 Pod
    pod := testPod("test-pod", "default")
    
    // 执行 Filter 扩展点
    status := f.RunFilterPlugins(context.TODO(), nil, pod, "test-node")
    
    if status.IsSuccess() {
        t.Log("Filter 通过")
    } else {
        t.Errorf("Filter 未通过: %v", status.Message())
    }
}
调试技巧: 在开发阶段,可以在自定义插件中添加 klog.V(5).Info() 日志,通过 --v=5 参数启动调度器,查看详细的调度决策日志。