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)。每个扩展点允许开发者注册自定义插件,在调度循环的特定阶段执行逻辑。
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 + 自定义 Filter 插件 是最优解。它既能利用 kube-scheduler 成熟的调度循环和抢占机制,又能通过
Filter 扩展点注入租户隔离规则,实现“零侵入”的调度策略扩展。