引言:调度器——Kubernetes的"大脑"

在Kubernetes集群中,每一个Pod的创建都要经过一个关键决策:它应该被调度到哪个节点上?这个看似简单的问题背后,涉及资源匹配、亲和性规则、拓扑约束、优先级抢占、自定义评分策略等多重复杂逻辑。默认调度器scheduler_default能处理绝大多数场景,但在AI训练、高性能计算、混合部署等细粒度场景中,仅靠默认策略远不能满足需求。本文将从调度器核心原理出发,系统讲解如何调优默认调度器,并手建一个生产可用级别的自定义调度器。

一、调度器核心架构拆解

1.1 调度循环:两个阶段的精密协作

Kubernetes调度器的核心设计是"过滤+打分"两阶段循环:

// scheduling cycle 核心逻辑
func (sched *Scheduler) scheduleOne(ctx context.Context) {
    // 1. 从待调度队列获取Pod
    pod := sched.NextPod()
    
    // 2. 调度阶段(Scheduling Cycle)
    scheduleResult, err := sched.SchedulePod(ctx, pod)
    
    // 3. 绑定阶段(Binding Cycle)
    go func() {
        // 异步执行绑定
        sched.BindPod(pod, scheduleResult.SuggestedHost)
    }()
}

调度阶段(Scheduling Cycle)是同步执行的核心,包含:

  • Predicates(预选):过滤不满足硬性条件的节点(资源不足、节点污点、Pod亲和性冲突等)。从v1.15起已从独立AlgorithmProvider转为FilterPlugin集合
  • Priorities(优选):为通过预选的节点打分(LeastRequestedPriority、BalancedAllocation等)
  • Scoring(新评分框架):v1.23起分离为ScorePlugin,支持灵活配置权重
  • Preempt(抢占):低优先级Pod驱逐以腾出空间给高优先级Pod(如果开启了Preemption)

绑定阶段(Binding Cycle)异步执行PreBind→Bind→PostBind插件链,通过API Server将节点分配写入Pod的spec.nodeName字段。

1.2 调度框架(Scheduling Framework)

自v1.16引入的Scheduling Framework将调度流程拆分为可扩展的Plugin插槽(Extension Points):

插槽阶段Plugin类型作用
QueueSortQueueSortPlugin定义待调度队列排序(默认FIFO)
PreFilterPreFilterPlugin预计算Pod信息(如缓存节点资源快照)
FilterFilterPlugin硬性条件过滤(类似旧Predicates)
PreScorePreScorePlugin评分前预处理
ScoringScorePlugin节点评分(可配置权重)
ReserveReservePlugin资源预留(调度成功后锁定)
PermitPermitPlugin延迟绑定决策(Approve/Wait/Deny)
PreBindPreBindPlugin绑定前准备(如PVC创建)
BindBindPlugin执行节点绑定(默认bind.go)
PostBindPostBindPlugin绑定后清理
UnreserveUnreservePlugin调度失败时释放预留资源

二、默认调度器的关键Plugin与源码解析

2.1 NodeResourcesFit(资源匹配)

这是最常用的Filter+Score Plugin,核心逻辑在pkg/scheduler/framework/plugins/noderesources/fit.go中:

// Fit Filter: 检查节点是否有足够的CPU/Memory/Misc资源
func (f *Fit) Filter(ctx context.Context, state *framework.CycleState, pod *v1.Pod, nodeInfo *framework.NodeInfo) *framework.Status {
    inadequateResources := fitsRequest(pod, nodeInfo, f.ignoredResources, f.ignoredResourceGroups)
    if len(inadequateResources) != 0 {
        return framework.NewStatus(framework.Unschedulable, msg)
    }
    return nil
}

// Exact Scoring策略:LeastRequested / MostRequested / RequestedToCapacityRatio
func (f *Fit) Score(ctx context.Context, state *framework.CycleState, pod *v1.Pod, nodeName string) (int64, *framework.Status) {
    nodeInfo := f.infoProducer.GetNodeInfo(nodeName)
    return f.calculateScore(pod, nodeInfo), nil
}

2.2 NodeAffinity / PodAffinity(亲和性调度)

亲和性的两阶段设计值得注意——requiredDuringSchedulingIgnoredDuringExecution(硬约束)在Filter阶段执行,而preferredDuringSchedulingIgnoredDuringExecution(软约束)则在Scoring阶段通过权重累加。源码中,PreFilter阶段构建一个NodeAffinityLister快速查询模式避免O(n*m)性能问题。

2.3 默认Profile配置

通过KubeSchedulerConfiguration的profiles.plugins可以灵活启用/禁用/加权:

apiVersion: kubescheduler.config.k8s.io/v1
kind: KubeSchedulerConfiguration
profiles:
  - schedulerName: default-scheduler
    plugins:
      score:
        disabled:
        - name: NodeResourcesLeastRequested
        enabled:
        - name: NodeResourcesBalancedAllocation
          weight: 2
      filter:
        enabled:
        - name: NodePorts
        - name: NodeResourcesFit
        - name: NodeUnschedulable
    pluginConfig:
    - name: NodeResourcesFit
      args:
        scoringStrategy:
          type: RequestedToCapacityRatio
          requestedToCapacityRatioParams:
            shape:
            - utilization: 0
              score: 0
            - utilization: 50
              score: 5
            - utilization: 100
              score: 10

三、自定义调度器实战:AI训练场景的GPU感知调度

3.1 为什么需要自定义调度器

在AI训练集群中,默认调度器无法感知:

  • GPU显存分布(两张24GB卡 vs 一张48GB卡的利用率差异)
  • NVLink/PCIe拓扑(跨GPU通信带宽对AllReduce性能影响翻倍)
  • 训练任务的弹性扩缩容需求(抢占式实例干扰下的任务迁移)
  • GPU碎片管理(Bin-Packing与Spread策略的权衡)

3.2 自定义调度器核心实现

基于scheduling-framework库(k8s.io/kubernetes/pkg/scheduler/framework)构建自定义调度器:

第一步:注册自定义Plugin

func main() {
    registry := schedframework.NewRegistry()
    registry.Register("GPUNodeFit", NewGPUNodeFit)
    registry.Register("GPUTopologyScore", NewGPUTopologyScore)
    registry.Register("FragmentAwareScore", NewFragmentAwareScore)
    
    profile := schedframework.NewProfile(
        "gpu-aware-scheduler",
        "default-scheduler",
        registry,
    )
    
    sched := scheduler.New(
        scheduler.WithProfiles(profile),
        scheduler.WithPercentageOfNodesToScore(30),
        scheduler.WithParallelism(16),
    )
    sched.Run(context.Background())
}

第二步:GPU显存精确过滤(GPUNodeFit Plugin)

type GPUNodeFit struct{}

func NewGPUNodeFit(obj runtime.Object, handle framework.Handle) (framework.Plugin, error) {
    return &GPUNodeFit{}, nil
}

func (p *GPUNodeFit) Name() string { return "GPUNodeFit" }

func (p *GPUNodeFit) Filter(ctx context.Context, state *framework.CycleState, pod *v1.Pod, nodeInfo *framework.NodeInfo) *framework.Status {
    gpuMemoryReq := getGPUMemoryRequest(pod)
    if gpuMemoryReq == 0 {
        return nil
    }
    
    node := nodeInfo.Node()
    allocatableGPU := getAllocatableGPUMemory(node)
    requestedGPU := getRequestedGPUMemory(nodeInfo)
    availableGPU := allocatableGPU - requestedGPU
    
    if availableGPU < gpuMemoryReq {
        return framework.NewStatus(framework.Unschedulable,
            fmt.Sprintf("insufficient GPU memory: need=%d, available=%d", gpuMemoryReq, availableGPU))
    }
    return nil
}

func getGPUMemoryRequest(pod *v1.Pod) int64 {
    if val, ok := pod.Annotations["gpu.ybb.press/memory-request"]; ok {
        mem, _ := strconv.ParseInt(val, 10, 64)
        return mem
    }
    return 0
}

第三步:NVLink拓扑感知评分(GPUTopologyScore Plugin)

type GPUTopologyScore struct {
    topologyCache *GPUTopologyCache
}

type GPUNetworkTopology struct {
    NodeName   string
    GPUs       []GPUInfo
    NVLinks    map[string][]string
    PCIeSwitch map[string][]string
}

type GPUInfo struct {
    UUID      string
    Memory    int64
    PCIeBusID string
    LinkWidth int
    FabricID  string
}

func (p *GPUTopologyScore) Score(ctx context.Context, state *framework.CycleState, pod *v1.Pod, nodeName string) (int64, *framework.Status) {
    gpuCount := getGPUCount(pod)
    if gpuCount <= 1 {
        return 50, nil
    }
    
    topology := p.topologyCache.Get(nodeName)
    if topology == nil {
        return 50, nil
    }
    
    bestGroup := topology.FindLargestFabricGroup()
    if len(bestGroup) >= int(gpuCount) {
        return 100, nil
    }
    
    if topology.FitsOnSingleSwitch(int(gpuCount)) {
        return 70, nil
    }
    
    return 30, nil
}

第四步:GPU碎片感知评分(FragmentAwareScore)

func (p *FragmentAwareScore) Score(ctx context.Context, state *framework.CycleState, pod *v1.Pod, nodeName string) (int64, *framework.Status) {
    info := f.infoProducer.GetNodeInfo(nodeName)
    node := info.Node()
    
    fragmentCount := calculateFragment(info)
    newFragment := calculateFragmentAfter(info, pod)
    
    if newFragment < fragmentCount {
        return 100, nil
    } else if newFragment == fragmentCount {
        return 50, nil
    } else {
        return 20, nil
    }
}

3.3 Pod级别Selector:使用自定义调度器

通过spec.schedulerName指定使用自定义调度器:

apiVersion: v1
kind: Pod
metadata:
  name: ai-training-node-0
  namespace: ml-pipeline
  annotations:
    gpu.ybb.press/memory-request: "16384"
    gpu.ybb.press/pod-count: "4"
spec:
  schedulerName: gpu-aware-scheduler
  containers:
  - name: pytorch-trainer
    image: nvcr.io/nvidia/pytorch:25.03-py3
    resources:
      limits:
        nvidia.com/gpu: 4
      requests:
        nvidia.com/gpu: 4
    env:
    - name: NCCL_IB_DISABLE
      value: "0"
  nodeSelector:
    node-type: gpu-a100

四、高级特性:动态资源共享与弹性抢占

4.1 动态NodeResources配额

Kubernetes的Node.Status.Allocatable是静态的。在高性能集群中,需要动态调整可分配资源以支持超卖(Overcommit)。这可以通过Validating/Mutating Admission Webhook在Pod创建时注入动态资源限额。

4.2 弹性抢占策略

Kubernetes默认抢占策略是纯优先级模式。在生产环境中,更好的方式是"效用抢占":

func preemptDecision(highPod, candidatePods) []PreemptionTarget {
    costs := map[string]float64{}
    for _, low := range candidatePods {
        costs[low.Name] = calculatePreemptionCost(low)
    }
    targets := knapsackSelect(costs, needed)
    return targets
}

五、生产落地的十大最佳实践

  1. 合理设置Requests/Limits比:Burst类Pod的Limit可设为Request的2-3倍提升集群利用率
  2. 禁用不必要的打分插件:关闭ImageLocality、HostPriority等场景不需要的Plugin降低调度延迟
  3. 控制PercentageOfNodesToScore:千级节点建议设为10-20%,避免对所有节点执行全量评分
  4. 优先级抢占配合配额管理:ResourceQuota限制低优先级Pod数量,防止抢占风暴
  5. 拓扑域合理规划:设置合适的拓扑约束粒度(hostname < rack < zone)
  6. 调度器性能调参:parallelism(默认16)因集群规模调整,大规模集群可设为32+
  7. Webhook超时控制:自定义Permit/Webhook的超时必须小于调度器默认10秒
  8. Pod队列优先级配合抢占:PriorityClass设置合理的分层阈值(100/1000/10000三级)
  9. GPU碎片监控:建立可视化Dashboard跟踪GPU碎片率,超过15%触发告警
  10. 灰度上线策略:新调度策略先在影子模式(Shadow Mode)运行对比,确认无误后再切流

结语:调度器是平台工程的基石

Kubernetes调度器的深度定制是迈向"平台工程"(Platform Engineering)的核心能力之一。无论是默认Plugin的精细调参,还是基于框架的定制Plugin开发,本质目标都是让集群资源与实际业务需求精确匹配。在AI工程师价格昂贵的2026年,一个优秀的GPU感知调度器可以直接降低30%以上的GPU闲置成本。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部