Kubernetes 源码【左扬精讲】—— kube-scheduler(调度专题 · 九):内部架构与核心组件

Kubernetes 源码【左扬精讲】—— kube-scheduler(调度专题 · 八):内部架构与核心组件

本篇是 kube-scheduler 调度专题的开篇。kube-scheduler 是 Kubernetes 控制平面的"大脑"——它负责将未调度的 Pod 分配到最合适的节点上。这个看似简单的决策过程,背后是一套精密的工程系统:Informer 负责感知集群状态,Scheduling Queue 管理调度任务队列,Cache 维护节点与 Pod 的内存快照,Scheduling Framework 则将调度流水线插件化。

读完本篇,你应该能回答:kube-scheduler 有哪些核心组件?它们各自的源码路径核心结构体是什么?调度流程是如何将这四个组件串联起来的?

Kubernetes Scheduler Informer Scheduling Queue Cache Scheduling Framework k8s v1.36.1

学习重点提示建议先通读全文,再重点回顾标注内容

重点掌握(必须)

  • Scheduler struct 四大成员(pkg/scheduler/scheduler.go:68-109):CacheSchedulingQueueProfilesnodeInfoSnapshot
  • PriorityQueue 三个内部队列(pkg/scheduler/backend/queue/scheduling_queue.go:183-187):activeQ / backoffQ / unschedulablePods
  • cacheImpl 双向链表结构(pkg/scheduler/backend/cache/cache.go:53-84):nodeInfoListItem 实现 MRU 顺序
  • ScheduleOne 调度主循环(pkg/scheduler/schedule_one.go:67-96):schedulingCycle + bindingCycle 两阶段分离
  • frameworkImpl 扩展点插件切片(pkg/scheduler/framework/runtime/framework.go:64-81):11 类插件接口

次重点(了解即可)

  • Cache 的 Pod 状态机(pkg/scheduler/backend/cache/cache.go:34-49):Assumed / Added / Forgotten 三态
  • Informers 注册的 9 类资源(pkg/scheduler/eventhandlers.go:481-706
  • Opportunistic Batching 与 Pod Signer(pkg/scheduler/framework/runtime/framework.go:872

文章目录

一、架构总览:四大组件的协同关系

思考记忆提示本节是调度器整体认知的起点,串联后续三章

  • 四大组件(Informer / Scheduling Queue / Cache / Scheduling Framework)是后续所有章节的底层基础
  • 理解"读本地内存快照、写走 APIServer"这个读写分离设计,是贯穿全篇的主线
  • 面试高频提问:kube-scheduler 有哪四大组件?各自职责是什么?

kube-scheduler 的源码组织在 pkg/scheduler/ 下,核心文件如下:

文件组件归属职责
scheduler.go 编排层 Scheduler struct,组装四大组件,主循环入口
schedule_one.go 编排层 ScheduleOne 主循环,一次调度一个 Pod
eventhandlers.go Informer 注册所有 Informer 的事件回调
backend/queue/scheduling_queue.go Scheduling Queue PriorityQueue,三个内部队列管理待调度 Pod
backend/cache/cache.go Cache 内存缓存,维护节点信息和 Pod 状态
framework/runtime/framework.go Scheduling Framework frameworkImpl,执行扩展点插件流水线

四者的关系用一句话概括:Informer 感知变化,Scheduling Queue 管理任务,Cache 提供快照,Framework 执行调度逻辑。下面这张时序图展示了 kube-scheduler 启动时四组件的组装顺序:

Scheduler.New(ctx)
│
├─ 1. internalcache.New()
│     → cacheImpl{}          // 内存缓存,空壳,等待 Informer 填充
│
├─ 2. internalqueue.NewSchedulingQueue()
│     → PriorityQueue{}      // activeQ / backoffQ / unschedulablePods
│
├─ 3. profile.NewMap()
│     → frameworkImpl{}     // 注册插件,初始化扩展点切片
│
├─ 4. addAllEventHandlers()
│     → 注册 9 类资源的 Informer handlers
│     → handler 回调写入 cache / schedulingQueue
│
└─ Scheduler.Run()
      ├─ SchedulingQueue.Run()       → BackoffQ pump goroutine
      ├─ APIDispatcher.Run()         → 异步 API 调用 goroutine
      └─ go wait.Until(ScheduleOne) → 调度主循环(goroutine)

设计精髓

kube-scheduler 采用了"事件驱动 + 内存快照"的设计:所有对 apiserver 的读取都通过 Informer 的本地 Cache(reflector → DeltaFIFO → Indexer),调度时不直接访问 etcd,只在 Bind 阶段才写入 apiserver。这种设计让调度器能高性能地遍历集群所有节点,同时解耦了调度器与 apiserver 的网络依赖。

必记闭环逻辑(核心考点)

kube-scheduler 的四大组件职责:Informer 感知集群变化Scheduling Queue 缓冲待调度 PodCache 提供调度所需的内存快照Scheduling Framework 执行插件化流水线。读写分离(读本地内存、写 APIServer)是全篇的设计主线。

二、Component 1:Informer —— 事件驱动的状态感知

2.1 整体架构:为什么需要 Informer?

kube-scheduler 需要感知集群中 Pod、Node、PV、PVC、StorageClass 等资源的变化。如果每次调度都直接请求 apiserver,数千节点的集群会导致 apiserver 过载。Informer 模式(基于 client-go 的 SharedInformer)通过"本地缓存 + 增量推送"解决了这个问题:

  • 本地缓存:Informer 在内存中维护一份完整资源副本,调度器读本地缓存而不每次查 etcd
  • 增量推送:Informer 监听 apiserver 的 watch 事件,增量更新本地缓存,触发 Handler 回调
  • SharedInformerFactory:多个组件共享同一个 Informer 实例,减少 apiserver 连接数

必记核心

Informer 是 kube-scheduler 的感知层:本地缓存让调度器读内存不碰网络,Watch 让 APIServer 主动推变更不轮询,SharedInformerFactory 让同资源全局共用一条连接。理解这三层,是理解整个调度器事件驱动架构的前提。

2.2 源码位置与核心结构

思考记忆提示本节是 Informer 的源码入口,关联 2.1 三大机制

  • addAllEventHandlers() = 9 类资源的 Handler 注册总入口,是 Informer 落地的代码证明
  • Handler 回调只做两件事:写 Cache 或写 Scheduling Queue,不要在这里写复杂逻辑
  • 面试高频提问:Informer 的 Handler 里一般写什么?为什么要最小化?

scheduler 的 Informer 注册逻辑集中在 pkg/scheduler/eventhandlers.go,入口函数 addAllEventHandlers()(行 481-706)。这个函数在 Scheduler.New()pkg/scheduler/scheduler.go:463)中被调用。

// pkg/scheduler/eventhandlers.go (行 481-520, k8s v1.36.1)
// addAllEventHandlers — 一次性注册所有 Informer 的事件回调
func addAllEventHandlers(
    sched *Scheduler,
    informerFactory informers.SharedInformerFactory,
    dynInformerFactory dynamicinformer.DynamicSharedInformerFactory,
    resourceClaimCache *assumecache.AssumeCache,
    resourceSliceTracker *resourceslicetracker.Tracker,
    draManager fwk.SharedDRAManager,
    gvkMap map[fwk.EventResource]fwk.ActionType,
) error {

    // ---- Pod Informer ----
    podInformer := informerFactory.Core().V1().Pods()
    _, err := podInformer.Informer().AddEventHandler(
        cache.FilteredResourceEventHandler{
            Handler: cache.ResourceEventHandlerFuncs{
                AddFunc:    func(obj interface{}) { sched.addPod(logger, obj) },
                UpdateFunc: func(oldObj, newObj interface{}) { sched.updatePod(logger, oldObj, newObj) },
                DeleteFunc: func(obj interface{}) { sched.deletePod(logger, obj) },
            },
            FilterFunc: func(obj interface{}) bool { ... },
        },
    )

    // ---- Node Informer ----
    _, err = informerFactory.Core().V1().Nodes().Informer().AddEventHandler(
        cache.ResourceEventHandlerFuncs{
            AddFunc:    func(obj interface{}) { sched.addNodeToCache(logger, obj) },
            UpdateFunc: func(oldObj, newObj interface{}) { sched.updateNodeInCache(logger, oldObj, newObj) },
            DeleteFunc: func(obj interface{}) { sched.deleteNodeFromCache(logger, obj) },
        },
    )
    // ...
}

必记核心

Informer 的 Handler 设计哲学是最小化回调:Add/Update/Delete 只做写 Cache 或写 Queue,复杂判断("哪些 Pod 需要重新调度")交给 MoveAllToActiveOrBackoffQueue()。Pod 的四态流转(未调度 ↔ 已调度)对应 Queue ↔ Cache 的切换。

2.3 Handler 分类与分发逻辑

思考记忆提示本节讲 Handler 如何把事件分流到 Cache 和 Queue

  • Handler 只分两类:写 Cache(已调度 Pod)和写 Scheduling Queue(未调度 Pod)
  • Pod 的四态流转(未调度 ↔ 已调度)对应 Queue ↔ Cache 的切换
  • 面试高频提问:Pod 从创建到调度成功,Handler 经历了哪几步?

所有 Handler 分为两类:写 Cache 和写 Scheduling Queue。addAllEventHandlers() 的设计哲学是——事件回调只做最小化工作,真正复杂的逻辑(如"节点变化后哪些 Pod 需要重新调度")由队列模块的 MoveAllToActiveOrBackoffQueue() 处理。

Pod Handler(行 128-208)

// pkg/scheduler/eventhandlers.go (行 128-141, k8s v1.36.1)
// addPod — Pod 创建时,区分"已调度"和"未调度"走不同分支
func (sched *Scheduler) addPod(logger klog.Logger, podObj interface{}) {
    pod, err := util.GetPod(podObj)
    if err != nil { return }

    if len(pod.Spec.NodeName) == 0 {
        // 未调度 Pod → 进入 SchedulingQueue
        sched.addPodToSchedulingQueue(logger, pod)
    } else {
        // 已调度 Pod → 进入 Cache(通知调度器该 Pod 已在某节点运行)
        sched.addAssignedPodToCache(logger, pod)
    }
}

// updatePod — Pod 更新时,判断是否需要重新调度
func (sched *Scheduler) updatePod(logger klog.Logger, oldObj, newObj interface{}) {
    oldPod, _ := util.GetPod(oldObj)
    newPod, _ := util.GetPod(newObj)
    if oldPod == nil || newPod == nil { return }

    if len(newPod.Spec.NodeName) == 0 && len(oldPod.Spec.NodeName) == 0 {
        // 未调度 → 未调度:更新 SchedulingQueue
        sched.updatePodInSchedulingQueue(logger, oldPod, newPod)
    } else if len(newPod.Spec.NodeName) > 0 && len(oldPod.Spec.NodeName) == 0 {
        // 未调度 → 已调度:移出 SchedulingQueue,加入 Cache
        sched.addAssignedPodToCache(logger, newPod)
        sched.deletePodFromSchedulingQueue(logger, oldPod)
    } else if len(oldPod.Spec.NodeName) > 0 && len(newPod.Spec.NodeName) == 0 {
        // 已调度 → 未调度(解绑):移出 Cache,进入 SchedulingQueue
        sched.addPodToSchedulingQueue(logger, newPod)
        sched.deleteAssignedPodFromCache(logger, oldPod)
    }
    // 已调度 → 已调度:仅更新 Cache 中的 Pod 信息
}

Node Handler(行 53-126)

// pkg/scheduler/eventhandlers.go (行 53-66, k8s v1.36.1)
// addNodeToCache — 节点新增时,更新 Cache 并触发待调度 Pod 重试
func (sched *Scheduler) addNodeToCache(logger klog.Logger, obj interface{}) {
    node, err := util.GetNode(obj)
    if err != nil { return }

    nodeInfo := sched.Cache.AddNode(logger, node)
    if nodeInfo != nil {
        sched.SchedulingQueue.MoveAllToActiveOrBackoffQueue(logger,
            fwk.ClusterEvent{Resource: fwk.Node, ActionType: fwk.Added},
            nil, nil, nil,
        )
    }
}

// updateNodeInCache — 节点变化时,判断是否需要重排待调度 Pod
func (sched *Scheduler) updateNodeInCache(logger klog.Logger, oldObj, newObj interface{}) {
    node, _ := util.GetNode(newObj)
    oldNode, _ := util.GetNode(oldObj)
    if node == nil { return }

    oldNodeInfo := sched.Cache.UpdateNode(logger, oldNode, node)
    if oldNodeInfo != nil && oldNodeInfo.Node().Spec.Unschedulable != node.Spec.Unschedulable {
        sched.SchedulingQueue.MoveAllToActiveOrBackoffQueue(logger, ...)
    }
}

必记核心

Handler 分类:Cache 写手(Node / PV / PVC / StorageClass / Service / CSINode / CSIDriver / VolumeAttachment,事件触发缓存更新)和Queue 写手(Pod,事件决定 Pod 是否需要重新调度)。核心设计原则:Handler 回调只做最小化工作,复杂逻辑交给 Queue 的 MoveAllToActiveOrBackoffQueue()

2.4 Informer 三大核心机制

思考记忆提示本节属于调度器底层核心重难点,结合逻辑链理解 + 背诵闭环流程

  • 三个机制是流水线闭环,前后强依赖,不要割裂记忆
  • 每个机制关联 ZoneSpreadScore 插件场景:Index 本地索引秒查 Zone Pod 数量
  • 面试高频提问:不用 Informer 会有什么问题?SharedInformer 解决什么痛点?

Informer 不是简单的"缓存",而是一套精密的本地镜像 + 事件驱动 + 共享复用机制。理解这三个机制,才能真正理解为什么调度器离不开 Informer。

机制一:本地内存缓存(Store / Indexer)—— 调度器读内存不碰网络

Informer 启动时先做一次 List(全量拉取):一次性从 APIServer 获取当前集群所有 Node、Pod、PV 资源,存入本地内存(Store)。之后调度器查节点、统计 Zone 下 Pod 数量、匹配存储资源,全部直接读本地内存,不碰网络、不碰 ETCD,查询延迟微秒级。

Store 只解决"能查到",Indexer 解决"查得快":Indexer 在内存中建立多维索引——按 Node 名称索引、按 Pod 绑定的 NodeName 索引、按 PVC 绑定 PV 索引。ZoneSpreadScore 插件统计某 Zone 下 Pod 数量,靠的就是本地索引秒查。

机制二:Watch 增量推送(DeltaFIFO)—— APIServer 主动推变更,不轮询

List 只做一次初始化。之后全程走 Watch 长连接:Informer 和 APIServer 建立一条 HTTP 持久连接,APIServer 监听 ETCD 变更,只把变化的那一条资源(Add / Update / Delete)推送给本地 Informer,Informer 自动更新本地缓存 + 触发回调。相比轮询:没有重复全量拉取、网络流量极低、状态感知实时。

Watch 推送的资源通过 DeltaFIFO Queue 有序分发:每个资源变更(Delta)按顺序入队,消费端按顺序处理,保证本地缓存与 ETCD 的最终一致性。

机制三:SharedInformerFactory —— 同资源全局共享一条连接

如果不共享:Node / Pod / PV / PVC 各建独立 Informer = 5 条 Watch 长连接,集群中成百上千个控制器同时运行,APIServer 连接数会爆炸。SharedInformerFactory 用工厂模式统一管理:同一种资源全局只创建一个 Informer 实例,所有组件共用这一份本地缓存 + 一条 Watch 连接,内存占用低、APIServer 压力大降。

反向思考辅助记忆:假设 Informer 不共享会怎样?每个插件各自 Watch 自己那份资源 → 5000 节点集群每秒成百次 List 请求 → APIServer → ETCD 被打满 → 集群所有组件(kubectl、controller、其他 scheduler)全部卡死。这正是 SharedInformerFactory 解决的本质问题。

必记闭环逻辑(核心考点)

Informer 的三大机制形成闭环:

List 初始化本地镜像 → Watch 实时增量同步 → Handler 回调触发调度逻辑

这就是 kube-scheduler 实现"高性能 + 低延迟 + 事件驱动"的工程根基。面试被问到 Informer,直接背这 12 个字,比背 200 字定义更有用。

2.5 注册的 9 类资源

思考记忆提示9 类资源分类记忆:4 类写 Cache,1 类写 Queue,4 类做辅助

  • 写 Cache(事件驱动缓存更新):Node / PV / PVC / StorageClass / Service / CSINode / CSIDriver / VolumeAttachment
  • 写 Queue(Pod 调度决策):Pod(未调度)
  • 记忆技巧:调度决策相关 → Queue;资源感知相关 → Cache

addAllEventHandlers() 共注册了 9 类资源的 Informer Handler(行 498-666),以下是完整列表:

资源类型InformerHandler 回调写入目标
Pod CoreV1().Pods() addPod / updatePod / deletePod Queue 或 Cache
Node CoreV1().Nodes() addNodeToCache / updateNodeInCache / deleteNodeFromCache Cache + Queue
PersistentVolume CoreV1().PersistentVolumes() onPvAdd / onPvUpdate / onPvDelete VolumeBinding Cache
PersistentVolumeClaim CoreV1().PersistentVolumeClaims() onPvcAdd / onPvcUpdate / onPvcDelete VolumeBinding Cache
StorageClass StorageV1().StorageClasses() onStorageClassAdd VolumeBinding Cache
CSINode StorageV1().CSINodes() onCSINodeAdd Cache
CSIStorageCapacity StorageV1().CSIStorageCapacities() onCSIStorageCapacityAdd VolumeBinding Cache
VolumeAttachment StorageV1().VolumeAttachments() onVaAdd VolumeBinding Cache
PodGroup(可选) schedulingv1alpha1().PodGroups() onPodGroupAdd SchedulingQueue

小贴士关于 SharedInformer 的等待同步

kube-scheduler 启动时必须等待所有 Informer 同步完成informerFactory.WaitForCacheSync()),否则调度时会看到"过期"的集群状态——Pod 已在某节点运行,但 Cache 里还没有。这个等待发生在 sched.Run()pkg/scheduler/scheduler.go:545)启动主循环之前,是 bootstrap 阶段最耗时的步骤之一。

必记核心

9 类资源 Handler 的记忆口诀:"Pod 入 Queue,其余写 Cache"。Node 变化时,通过 Queue 的 MoveAllToActiveOrBackoffQueue() 触发相关未调度 Pod 重新评估。

2.6 常见误区:澄清 Informer 的边界

误区一:Informer 是独立缓存服务?

Informer 不是独立运行的缓存中间件(不是 Redis,不是本地代理),而是集成在调度器进程内的内存数据结构。它运行在 kube-scheduler 进程内,直接持有 Node/Pod/PV 的 Go 对象指针。

误区二:Watch 就不用 List 了?

Watch 只推送增量事件,但启动时本地缓存是空的,必须先 List 一次全量数据填充缓存,否则调度器拿到的是"无数据"。List 只在启动时执行一次,之后全程走增量 Watch。

误区三:SharedInformer 每个协程各创建一个?

SharedInformer 是单例——同一种资源(如 Pod)在整个调度器进程中只存在一个实例,所有插件(Filter、Score、Bind)共用同一个 Informer 实例 + 同一份本地缓存 + 同一条 Watch 连接。

误区四:Informer 是强一致性?

Informer 只保证最终一致性。Watch 长连接网络抖动时,增量事件可能短暂丢失,本地缓存会与集群真实状态出现几秒滞后。Resync 机制(默认 30s)会定时重新 List 全量数据、触发 Update 回调兜底修复,保证本地缓存最终与 ETCD 同步。

必记核心

Informer 四大边界:不是独立缓存服务(是进程内内存结构)→ 不是轮询替代品(启动必须 List)→ 不是多实例(全局单例)→ 不保证强一致(只保证最终一致)。记住这四句话,Informer 的核心概念就不再混淆。

三、Component 2:Scheduling Queue —— 调度任务的缓冲区

思考记忆提示本节是 Pod 调度的"缓冲层",关联 Informer 和主循环

  • Pod 调度失败为什么要等而不是直接报错?因为资源可能下一秒就释放了(节点污点撤销、Pod 删除腾出资源)
  • 三个子队列(activeQ / backoffQ / unschedulablePods)对应三种 Pod 状态:待调度 / 退避中 / 永久失败
  • 面试高频提问:调度失败的 Pod 去了哪里?Backoff 退避时间怎么算?

3.1 为什么需要调度队列?

Pod 调度不是"来一个调度一个"那么简单。调度失败时,Pod 需要等待条件变化后重试;节点变化时,未调度的 Pod 需要被触发重新评估。Scheduling Queue 就是管理这些"待调度 Pod"的缓冲区——它不是简单的 FIFO 队列,而是一个包含优先级排序、退避重试、条件触发的智能调度缓冲系统

3.2 核心结构:PriorityQueue

调度队列在 v1.36.1 中的实现是 PriorityQueuepkg/scheduler/backend/queue/scheduling_queue.go:172),对外暴露 SchedulingQueue 接口(行 96-153)。它内部维护三个子队列

// pkg/scheduler/backend/queue/scheduling_queue.go (行 60-84, k8s v1.36.1)
// 常量定义
const (
    DefaultPodMaxInUnschedulablePodsDuration time.Duration = 5 * time.Minute  // Pod 在不可调度队列中最多待 5 分钟
    DefaultPodInitialBackoffDuration time.Duration = 1 * time.Second           // 初始退避 1s
    DefaultPodMaxBackoffDuration time.Duration = 10 * time.Second             // 最大退避 10s
})

// pkg/scheduler/backend/queue/scheduling_queue.go (行 172-210, k8s v1.36.1)
// PriorityQueue — 调度队列的核心实现
type PriorityQueue struct {
    *nominator                  // 记录 Pod 的 nominatedNodeName(等待抢占的节点)
    stop  chan struct{}
    clock clock.WithTicker
    lock sync.RWMutex

    // 三个内部队列
    activeQ           activeQueuer    // 堆结构,待调度的 Pod 按优先级排序
    backoffQ          backoffQueuer  // 堆结构,调度失败后进入退避等待
    unschedulablePods *unschedulablePods  // map 结构,调度失败的 Pod

    moveRequestCycle int64
    // PreEnqueue 插件 map:哪个 Pod 满足哪个插件的前置条件
    preEnqueuePluginMap map[string]map[string]fwk.PreEnqueuePlugin
    // QueueingHintMap:插件注册的事件 → Pod 重排策略
    queueingHintMap    QueueingHintMapPerProfile
    // Pod 签名器:Opportunistic Batching 优化
    podSigners map[string]PodSigner
    // ...
}

小贴士关于 QueueingHint

v1.36.1 引入了 QueueingHint 机制(queueingHintMapscheduling_queue.go:193):每个插件声明"当 X 资源发生变化时,我的调度结果可能需要重新评估"。这让调度器能精确触发受影响的 Pod,而不是每次都把整个 activeQ 清空重排。这是一个 0(n) → 0(1) 的优化,在大集群中效果显著。

3.3 三个子队列的行为

队列底层数据结构进入条件离开条件关键方法
activeQ 堆(heap) Pod 新建、Backoff 结束、节点变化触发 Pop() 被调用 Add() / Pop()
backoffQ 堆(heap)+ 定时器 调度失败后进入,指数退避 退避计时器到期 → activeQ moveToBackoffQ()
unschedulablePods map + 堆(超时清理) 调度失败(不可重试/永久性原因) 5 分钟后超时 → backoffQ / 节点变化触发 AddUnschedulableIfNotPresent()

3.4 Pod 在三个队列之间的流转

// 场景 1: Pod 新建 → activeQ
// pkg/scheduler/backend/queue/scheduling_queue.go (行 739-750)
func (p *PriorityQueue) Add(ctx context.Context, pod *v1.Pod) {
    p.lock.Lock()
    defer p.lock.Unlock()
    if err := p.activeQ.Add(p.podInfo(pod)); err != nil {
        logger.Error(err, "Error adding pod to the active queue", "pod", klog.KObj(pod))
    }
}

// 场景 2: 调度失败 → backoffQ 或 unschedulablePods
// pkg/scheduler/backend/queue/scheduling_queue.go (行 901-965)
func (p *PriorityQueue) AddUnschedulableIfNotPresent(
    logger klog.Logger, podInfo *framework.QueuedPodInfo, podSchedulingCycle int64,
) error {
    // 判断是否值得等待(可重试 vs 永久性失败)
    if p.isPodWorthRequeuing(podInfo, frwk.Unschedulable) {
        // 永久性失败 → unschedulablePods(5 分钟后超时)
        p.unschedulablePods.Add(podInfo.Pod, p.moveRequestCycle)
    } else {
        // 可重试 → backoffQ(指数退避)
        p.podInfo(podInfo.Pod).UnschedulableTime = time.Now()
        if err := p.backoffQ.Add(p.podInfo(podInfo.Pod)); err != nil {
            logger.Error(err, "Error adding pod to the backoff queue", "pod", klog.KObj(podInfo.Pod))
        }
    }
}

// 场景 3: 节点变化 → 相关 Pod 从 unschedulablePods 移出
// pkg/scheduler/backend/queue/scheduling_queue.go (行 1269-1277)
func (p *PriorityQueue) MoveAllToActiveOrBackoffQueue(...) {
    for _, pInfo := range p.unschedulablePods.Pods() {
        if p.isPodWorthRequeuing(...) {
            p.moveToBackoffQ(pInfo)
        }
    }
}

设计精髓

PriorityQueue 的核心设计是"最小化重排":不是把所有 Pod 都推倒重来,而是通过 QueueingHint 精确找到"受事件影响"的 Pod。v1.36.1 之前,每次节点变化都会触发 MoveAllToActiveOrBackoffQueue 清空 unschedulablePods;现在,每个插件注册自己的 Hint("我关心 Node.Update" / "我关心 Pod.Delete"),只有相关 Pod 才被重新入队。在 5000 节点、10000 Pod 的集群中,这个优化可以将每次节点更新的处理时间从 O(n) 降到 O(1)

必记核心

Scheduling Queue 的三队列职责:activeQ(堆结构,Pod 按优先级排队等待调度)、backoffQ(指数退避,失败 Pod 冷却后再重试)、unschedulablePods(永久失败,5 分钟后超时转移)。v1.36.1 的 QueueingHint 机制把节点更新触发范围从 O(n) 降到 O(1)。

四、Component 3:Cache —— 节点与 Pod 的内存快照

思考记忆提示Cache 是调度器的"本地数据库",关联 Informer 和主循环

  • Cache 和 Informer 的 Indexer 区别:Indexer 存原始对象,Cache 存"调度语义加工"后的数据(如剩余 CPU)
  • Assumed 状态解决并发调度"超售"问题:Filter 前先预占资源,Bind 失败则回滚
  • 面试高频提问:并发调度时,两个 Pod 同时看到节点 X 有资源会发生什么?Assumed 如何解决?

4.1 Cache 的定位

Cache 是 kube-scheduler 的"数据库"——它存储了调度所需的所有节点和 Pod 的本地内存副本。与 apiserver 中的 etcd 不同,Cache 是纯内存结构,不做持久化;与 Informer 的本地 Indexer 不同,Cache 存储的是经过调度语义加工的数据(如"节点上还剩多少 CPU")。

4.2 核心接口与结构体

// pkg/scheduler/backend/cache/interface.go (行 56-127, k8s v1.36.1)
// Cache 接口 — 调度器只依赖这个抽象,不关心具体实现
type Cache interface {
    NodeCount() int
    PodCount() (int, error)
    GetNode(name string) (*framework.NodeInfo, error)

    // Pod 生命周期管理(Assumed = 乐观假设,Added = 确认绑定)
    AssumePod(logger klog.Logger, pod *v1.Pod) error   // 调度成功后"假设"Pod 已在节点
    ForgetPod(logger klog.Logger, pod *v1.Pod) error    // 假设失败,取消假设
    AddPod(logger klog.Logger, pod *v1.Pod) error       // 确认 Pod 已绑定
    UpdatePod(logger klog.Logger, oldPod, newPod *v1.Pod) error
    RemovePod(logger klog.Logger, pod *v1.Pod) error
    IsAssumedPod(pod *v1.Pod) (bool, error)            // 检查是否在 Assumed 状态

    // Node 管理
    AddNode(logger klog.Logger, node *v1.Node) *framework.NodeInfo
    UpdateNode(logger klog.Logger, oldNode, newNode *v1.Node) *framework.NodeInfo
    RemoveNode(logger klog.Logger, node *v1.Node) error

    // 快照:每个调度周期从 Cache 拿一份快照
    UpdateSnapshot(logger klog.Logger, nodeSnapshot *Snapshot) error

    // 调试与 PodGroup
    Dump() *Dump
    BindPod(binding *v1.Binding) (
// pkg/scheduler/backend/cache/cache.go (行 59-88, k8s v1.36.1)
// cacheImpl — Cache 的生产实现
type cacheImpl struct {
    stop   

4.3 Pod 的三态状态机

Cache 中的 Pod 遵循一个三态状态机(pkg/scheduler/backend/cache/cache.go:34-49):

// pkg/scheduler/backend/cache/cache.go (行 34-49, k8s v1.36.1)
// Pod 在 Cache 中的三种状态及其转换
//
//   Initial  ──────►  Assumed  ──────►  Added
//   (未在 Cache)         (已假设)          (已确认)
//                          │                 │
//                          ▼                 ▼
//                      Forgotten        Deleted
//                    (假设被取消)      (从 Cache 移除)
//
// Assumed 状态是"乐观预占":调度周期开始时,
// 在真正 Bind 之前,把 Pod 的资源算进节点的已分配量。

// AssumePod — 调度成功后,把 Pod "假设"到目标节点
// pkg/scheduler/backend/cache/cache.go (行 397-410)
func (cache *cacheImpl) AssumePod(logger klog.Logger, pod *v1.Pod) error {
    key, err := cache.keyFunc(pod)
    if err != nil { return err }

    cache.mu.Lock()
    defer cache.mu.Unlock()

    if _, ok := cache.podStates[key]; ok {
        return fmt.Errorf("pod %v is not assumed and thus cannot be assumed again", key)
    }

    cache.assumedPods.Insert(key)
    cache.podStates[key] = &podState{pod: pod}
    if err := cache.addPod(pod); err != nil {
        cache.assumedPods.Delete(key)
        delete(cache.podStates, key)
        return err
    }
    return nil
}

// ForgetPod — 假设失败或被取消,把 Pod 从 Cache 移除
// pkg/scheduler/backend/cache/cache.go (行 412-434)
func (cache *cacheImpl) ForgetPod(logger klog.Logger, pod *v1.Pod) error {
    key, err := cache.keyFunc(pod)
    if err != nil { return err }

    cache.mu.Lock()
    defer cache.mu.Unlock()

    if !cache.assumedPods.Has(key) {
        return fmt.Errorf("pod %v is not assumed, cannot be forgotten", key)
    }

    if err := cache.removePod(pod); err != nil { return err }
    cache.assumedPods.Delete(key)
    delete(cache.podStates, key)
    return nil
}

// AddPod — Bind 确认后,把 Pod 加入 Cache(或从 assumed 升级为 added)
// pkg/scheduler/backend/cache/cache.go (行 515-545)
func (cache *cacheImpl) AddPod(logger klog.Logger, pod *v1.Pod) error {
    key, err := cache.keyFunc(pod)
    if err != nil { return err }

    cache.mu.Lock()
    defer cache.mu.Unlock()

    if cache.assumedPods.Has(key) {
        // 从 Assumed → Added:Pod 已绑定,无需额外操作
        cache.assumedPods.Delete(key)
    } else {
        // 从外部进入(已在某节点运行):需要加入 Cache
        if err := cache.addPod(pod); err != nil { return err }
    }

    cache.podStates[key] = &podState{pod: pod}
    return nil
}

小贴士为什么需要 Assumed 状态?

调度是并发的——同一个集群可能有多个调度周期同时运行。如果没有 Assumed 机制:Pod A 和 Pod B 都在 Filter 阶段检查节点 X,X 的剩余资源同时满足两者,都选择了 X。但最终 Bind 只能成功一个,另一个的 Filter 结果就是"过时"的。Assumed 机制让调度器在 Filter 阶段就"预占"资源——Filter 看到的资源量已经扣除了 Assumed Pod,确保并发调度不会"超售"。

记忆思考重点Assumed 只存在调度器本地内存 Cache,不会写入 APIServer/ETCD,是临时虚拟占位

  • 核心解决两个痛点:多 Pod 并发调度资源争抢、同步 Bind 阻塞调度吞吐量
  • 必须联动 Reserve/Unreserve 插件,Bind 失败要回滚释放预占资源
  • ZoneSpreadScore 等拓扑均衡打分,统计数量会包含 Assumed Pod,保证分布均匀

无 Assumed 的并发冲突问题

节点 X 剩余 2C2G,Pod A、B 各需 1C1G 同时调度:

  1. 两者 Filter 都读取 Informer 集群真实状态,都判定节点 X 资源充足
  2. 两个 Pod 同时选中 X 发起 Bind,APIServer 仅能成功一个,另一个调度失败重排
  3. 批量扩容场景下会大量重复调度,性能极差

Assumed 核心逻辑(预占"未来资源")

Assumed Pod 是调度器 Cache 内的临时虚拟 Pod,执行时机在 Reserve 插件阶段:

  1. Pod A 选出节点 X 后,先在本地 Cache 创建 Assumed Pod,节点可用资源扣减 1C1G
  2. 后台异步发起 Bind 请求,调度器主线程立刻处理 Pod B
  3. Pod B Filter 读取混合了 Assumed 状态的 Cache,看到节点 X 只剩 1C1G,直接过滤,彻底避免争抢

两大核心价值

  1. 规避并发资源冲突:Informer 仅同步集群已落地的真实 Pod;Assumed 补充「正在调度、未绑定」的预占视图,插件打分过滤时能看见未来资源占用,防止双绑。
  2. 异步绑定提升调度吞吐:Bind 是远程网络操作,Assume 先内存预占,不用阻塞调度循环等待 APIServer 响应,调度器可并行处理队列中多个 Pod。

完整生命周期

  1. Assume(Reserve):本地 Cache 预占资源,生成虚拟 Assumed Pod
  2. 异步 Bind:后台协程向 APIServer 提交绑定
  3. 成功:Informer 同步到真实 Pod,Cache 清理 Assumed 临时数据
  4. 失败(Unreserve):删除 Assumed Pod,节点资源恢复,Pod 重新入队调度

核心总结

Assumed = 调度器本地乐观预占机制,给 Filter/Score 插件提供包含「待绑定 Pod」的未来资源视图,是调度器高并发、无冲突调度的底层保障。

4.4 Snapshot 机制

每个调度周期开始时,调度器从 Cache 提取一份只读快照nodeInfoSnapshot),Filter 和 Score 都在这个快照上做计算:

// pkg/scheduler/backend/cache/cache.go (行 184-296, k8s v1.36.1)
// UpdateSnapshot — 从 Cache 提取节点列表,构建只读快照
func (cache *cacheImpl) UpdateSnapshot(logger klog.Logger, nodeSnapshot *internalcache.Snapshot) error {
    cache.mu.RLock()
    defer cache.mu.RUnlock()

    // 按双向链表的 MRU 顺序遍历节点
    nodes := make([]*framework.NodeInfo, 0, len(cache.nodes))
    for item := cache.headNode; item != nil; item = item.next {
        nodes = append(nodes, item.info)
    }

    // 反转(因为链表是从 headNode → tailNode,但调度希望从尾部开始)
    for i, j := 0, len(nodes)-1; i < j; i, j = i+1, j-1 {
        nodes[i], nodes[j] = nodes[j], nodes[i]
    }

    nodeSnapshot.NodeInfoMap = make(map[string]*framework.NodeInfo, len(nodes))
    for _, nodeInfo := range nodes {
        nodeSnapshot.NodeInfoMap[nodeInfo.Node().Name] = nodeInfo
    }
    nodeSnapshot.Nodes = nodes
    return nil
}

4.5 双向链表的 MRU 设计

Cache 用双向链表管理节点(nodeInfoListItempkg/scheduler/backend/cache/cache.go:53-60),目的是维护MRU(Most Recently Used)顺序。节点被访问(Filter 阶段遍历)时会被移到链表头部,下次遍历优先使用——这是一种近似轮询的负载均衡策略。

// pkg/scheduler/backend/cache/cache.go (行 114-163, k8s v1.36.1)
// moveNodeInfoToHead — 节点被访问后移到链表头部(MRU)
func (cache *cacheImpl) moveNodeInfoToHead(logger klog.Logger, n *nodeInfoListItem) {
    if n == nil { return }
    if n == cache.headNode { return }  // 已在头部,无需移动

    // 从原位置摘下
    if n.prev != nil { n.prev.next = n.next }
    if n.next != nil { n.next.prev = n.prev }

    // 插入头部
    n.prev = nil
    n.next = cache.headNode
    cache.headNode.prev = n
    cache.headNode = n
}

// removeNodeInfoFromList — 节点删除时从链表摘下
func (cache *cacheImpl) removeNodeInfoFromList(n *nodeInfoListItem) {
    if n == nil { return }
    if n.prev != nil { n.prev.next = n.next }
    if n.next != nil { n.next.prev = n.prev }
    if n == cache.headNode { cache.headNode = n.next }
}

设计精髓

Cache 的双向链表 + Map 混合结构(O(1) 插入、删除、移动;O(n) 遍历)是一种经典的"缓存友好"设计。相比纯 Map,它保留了遍历顺序(MRU);相比纯链表,它提供了 O(1) 随机访问。这个选择在调度场景下是合理的:节点数量(千级)远小于 Pod 数量,但遍历次数极多(每次调度遍历所有节点),MRU 顺序能带来更好的 CPU cache locality。

必记核心

Cache 是调度器的"语义加工层":Assumed 机制解决并发超售(三态:未绑定 → Assumed → 真正 Bind 或回滚),双向链表 + Map 兼顾 O(1) 随机访问和 MRU 遍历顺序,Snapshot 机制保证整个 schedulingCycle 看到一致的资源视图。

五、Component 4:Scheduling Framework —— 插件化的调度流水线

思考记忆提示本节是调度器的"执行层",关联前面三章的感知/缓冲/快照

  • Informer 提供集群感知 → Cache 提供调度快照 → Scheduling Queue 缓冲任务 → Scheduling Framework 执行流水线
  • 9 个扩展点(PreFilter → Filter → PreScore → Score → Reserve → Permit → PreBind → Bind → PostBind)串成一个 Pod 的完整生命周期
  • 面试高频提问:Filter 和 Score 的区别是什么?Reserve 和 PreBind 的区别是什么?

5.1 为什么需要插件化?

Scheduler 面对的调度规则五花八门:节点资源、节点亲和、Pod 间亲和、污点容忍、拓扑打散、卷绑定……每一条都是一个独立的业务规则。如果全量硬编码进调度主循环,schedule_one.go 会膨胀到几万行;新增一条规则就要改核心代码。Scheduling Framework 就是把调度规则全部抽象成扩展点接口,每条规则单独实现成插件

5.2 扩展点全景

Scheduling Framework 定义了 13 个扩展点(对应 13 个 Go 接口),按执行顺序排列:

扩展点Go 接口阶段作用
PreEnqueue PreEnqueuePlugin 入队前 判断 Pod 是否满足前置条件(如 SchedulingGates 已全部移除)
QueueSort QueueSortPlugin 队列排序 决定 activeQ 中 Pod 的优先级(由 PrioritySort 实现)
PreFilter PreFilterPlugin 过滤前预处理 预处理(如算好资源请求),结果写入 CycleState
Filter FilterPlugin 过滤 逐个 Node 检查硬约束(资源、亲和、污点等)
PostFilter PostFilterPlugin 过滤后 过滤失败时执行(典型:DefaultPreemption 抢占)
PreScore PreScorePlugin 打分前预处理 预计算 Pod 特征向量,供 Score 阶段使用
Score ScorePlugin 打分 为每个 Feasible Node 打 0~100 分
Reserve ReservePlugin 预约 Bind 前"占座"(典型:VolumeBinding 占用 PV)
Permit PermitPlugin 批准 异步等待外部信号(GangScheduling 等齐兄弟 Pod)
PreBind PreBindPlugin 绑定前 真正创建 PV 绑定等预处理
Bind BindPlugin 绑定 向 apiserver 提交 Binding 对象(由 DefaultBinder 完成)
PostBind PostBindPlugin 绑定后 Bind 成功后清理(释放资源)
Sign SignPlugin 签名 Opportunistic Batching:给 Pod 生成签名,复用调度结果

5.3 框架运行时:frameworkImpl

// pkg/scheduler/framework/runtime/framework.go (行 58-112, k8s v1.36.1)
// frameworkImpl — 框架运行时的核心结构体
type frameworkImpl struct {
    // ---- 13 类插件切片 ----
    registry                  runtime.Registry
    snapshotSharedLister      fwk.SharedLister  // NodeInfo 快照(从 Cache.UpdateSnapshot 传入)

    // 队列相关
    waitingPods               *waitingPodsMap     // 等待 Permit 批准的 Pod
    podsInPreBind             *podsInPreBindMap  // 正在 PreBind 的 Pod
    queueSortPlugins         []fwk.QueueSortPlugin

    // 调度流水线
    preEnqueuePlugins        []fwk.PreEnqueuePlugin
    enqueueExtensions        []fwk.EnqueueExtensions
    preFilterPlugins         []fwk.PreFilterPlugin
    filterPlugins            []fwk.FilterPlugin
    postFilterPlugins        []fwk.PostFilterPlugin
    preScorePlugins          []fwk.PreScorePlugin
    scorePlugins             []fwk.ScorePlugin
    reservePlugins           []fwk.ReservePlugin
    preBindPlugins           []fwk.PreBindPlugin
    bindPlugins              []fwk.BindPlugin
    postBindPlugins          []fwk.PostBindPlugin
    permitPlugins            []fwk.PermitPlugin

    // 插件权重(Score 时加权)
    scorePluginWeight         map[string]int

    // 签名与批处理
    batchablePlugins          []fwk.SignPlugin
    batch                    *OpportunisticBatch

    // DRA / CSI / PodGroup
    sharedDRAManager         fwk.SharedDRAManager
    sharedCSIManager         fwk.CSIManager
    podGroupManager          fwk.PodGroupManager

    // 并行化器
    parallelizer             fwk.Parallelizer

    // 辅助工具
    clientSet                clientset.Interface
    informerFactory          informers.SharedInformerFactory
    apiDispatcher            *apidispatcher.APIDispatcher
    logger                   klog.Logger
    metricsRecorder          *metrics.MetricAsyncRecorder
    profileName              string
}

5.4 插件注册中心:NewInTreeRegistry

// pkg/scheduler/framework/plugins/registry.go (行 50-79, k8s v1.36.1)
// NewInTreeRegistry — 23 个内置插件的注册中心
func NewInTreeRegistry() runtime.Registry {
    fts := plfeature.NewSchedulerFeaturesFromGates(feature.DefaultFeatureGate)
    registry := runtime.Registry{
        // ---- Filter/PreFilter 插件 ----
        noderesources.Name:               runtime.FactoryAdapter(fts, noderesources.NewFit),
        tainttoleration.Name:            runtime.FactoryAdapter(fts, tainttoleration.New),
        nodeaffinity.Name:               runtime.FactoryAdapter(fts, nodeaffinity.New),
        nodename.Name:                   runtime.FactoryAdapter(fts, nodename.New),
        nodeports.Name:                  runtime.FactoryAdapter(fts, nodeports.New),
        interpodaffinity.Name:           runtime.FactoryAdapter(fts, interpodaffinity.New),
        podtopologyspread.Name:         runtime.FactoryAdapter(fts, podtopologyspread.New),
        nodeunschedulable.Name:         runtime.FactoryAdapter(fts, nodeunschedulable.New),
        nodedeclaredfeatures.Name:      runtime.FactoryAdapter(fts, nodedeclaredfeatures.New),
        nodevolumelimits.CSIName:      runtime.FactoryAdapter(fts, nodevolumelimits.NewCSI),

        // ---- 资源打分插件 ----
        noderesources.BalancedAllocationName: runtime.FactoryAdapter(fts, noderesources.NewBalancedAllocation),
        imagelocality.Name:                   runtime.FactoryAdapter(fts, imagelocality.New),

        // ---- 卷相关插件 ----
        volumebinding.Name:              runtime.FactoryAdapter(fts, volumebinding.New),
        volumerestrictions.Name:        runtime.FactoryAdapter(fts, volumerestrictions.New),
        volumezone.Name:                runtime.FactoryAdapter(fts, volumezone.New),

        // ---- 抢占 & 队列 ----
        defaultpreemption.Name:          runtime.FactoryAdapter(fts, defaultpreemption.New),
        queuesort.Name:                 queuesort.New,            // 无需 FeatureGate,直接注册
        defaultbinder.Name:             defaultbinder.New,        // 无需 FeatureGate,直接注册

        // ---- 高级特性 ----
        dynamicresources.Name:           runtime.FactoryAdapter(fts, dynamicresources.New),
        gangscheduling.Name:           runtime.FactoryAdapter(fts, gangscheduling.New),
        schedulinggates.Name:          runtime.FactoryAdapter(fts, schedulinggates.New),
        topologyaware.Name:            runtime.FactoryAdapter(fts, topologyaware.New),
        podgrouppodscount.Name:        runtime.FactoryAdapter(fts, podgrouppodscount.New),
    }
    return registry
}

注意

所有通过 runtime.FactoryAdapter 包装的插件都能接收 feature.Featurespkg/scheduler/framework/runtime/registry.go:38),这使得插件在 v1.36.1 中可以感知 DynamicResourceAllocationNodeInclusionPolicyInPodTopologySpread 等特性门控。queuesortdefaultbinder 不需要特性门控,直接以裸 PluginFactory 注册。

必记核心

Scheduling Framework 的核心价值:把调度规则全部抽象成扩展点接口(9 个),插件通过编译期接口断言注册到对应扩展点。Filter(硬约束)→ Score(软权重)→ Reserve(占座防超售)→ Bind(写入 apiserver),四大阶段各司其职。

六、调度主循环:四组件如何串联

思考记忆提示本节是全篇的"总装图",把前五章的组件串成一个完整调度流程

  • 两阶段设计:schedulingCycle(同步,Filter → Score → Reserve)和 bindingCycle(异步,Permit → PreBind → Bind → PostBind)
  • schedulingCycle 失败 → Pod 回退到 SchedulingQueue;bindingCycle 失败 → Assumed 资源回滚
  • 面试高频提问:schedulingCycle 和 bindingCycle 的关系是什么?为什么分开?

6.1 ScheduleOne 的两阶段设计

ScheduleOne()pkg/scheduler/schedule_one.go:67)是 kube-scheduler 的主循环入口,每次处理一个 Pod。它分为同步的 schedulingCycle 和异步的 bindingCycle 两阶段:

// pkg/scheduler/schedule_one.go (行 67-148, k8s v1.36.1)
// ScheduleOne — 一次调度一个 Pod 的主循环
func (sched *Scheduler) ScheduleOne(ctx context.Context) {
    // ① 从 SchedulingQueue 拿出下一个待调度的 Pod(阻塞)
    podInfo, err := sched.NextPod(logger)
    if err != nil { return }
    if podInfo == nil || podInfo.Pod == nil { return }

    if sched.genericWorkloadEnabled && podInfo.Pod.Spec.SchedulingGroup != nil {
        // PodGroup 调度:批量调度一族相关的 Pod
        podGroupInfo, err := sched.podGroupInfoForPod(ctx, podInfo)
        sched.scheduleOnePodGroup(ctx, podGroupInfo)
    } else {
        // 普通 Pod 调度
        sched.scheduleOnePod(ctx, podInfo)
    }
}

// pkg/scheduler/schedule_one.go (行 99-148, k8s v1.36.1)
// scheduleOnePod — 单个 Pod 的调度流程
func (sched *Scheduler) scheduleOnePod(ctx context.Context, podInfo *fwk.QueuedPodInfo) {
    // 获取当前调度周期的 framework(根据 Pod 的 schedulerName 选择 Profile)
    fwk, state := sched.getFrameworkAndState(ctx, podInfo)
    if fwk == nil { return }

    // 同步阶段:schedulingCycle
    scheduleResult, facStatus := sched.schedulingCycle(ctx, fwk, state, podInfo, logger)

    if facStatus != nil {
        sched.handleSchedulingFailure(ctx, fwk, podInfo, facStatus, ...)
        return
    }

    // 异步阶段:bindingCycle(在 goroutine 中执行)
    go func() {
        defer func() {
            if !podInfo.DeferAfter(fwk) {
                sched.SchedulingQueue.Done(podInfo)
            }
        }()
        sched.bindingCycle(ctx, fwk, state, podInfo, scheduleResult, logger)
    }()
}

6.2 schedulingCycle:同步调度阶段

// pkg/scheduler/schedule_one.go (行 175-198, k8s v1.36.1)
// schedulingCycle — 同步调度阶段
func (sched *Scheduler) schedulingCycle(
    ctx context.Context, fwk fwk.Framework, state *fwk.CycleState,
    podInfo *fwk.QueuedPodInfo, logger klog.Logger,
) (ScheduleResult, *fwk.Status) {

    // ① 从 Cache 提取节点快照(所有 Filter/Score 在这个快照上运行)
    if err := sched.Cache.UpdateSnapshot(logger, sched.nodeInfoSnapshot); err != nil {
        return ScheduleResult{}, fwk.AsStatus(err)
    }

    // ② 调用 Framework 执行调度算法
    scheduleResult, schedStatus := sched.SchedulePod(ctx, fwk, state, podInfo)
    if schedStatus != nil {
        return ScheduleResult{}, schedStatus
    }

    // ③ 假设 Pod 已调度 + 执行 Reserve 插件("占座")
    assumeFinish, err := sched.assumeAndReserve(ctx, fwk, state, podInfo, scheduleResult.SuggestedHost)
    if err != nil {
        return ScheduleResult{}, fwk.AsStatus(err)
    }

    // ④ 执行 Permit 插件(等待批准,可能返回 Wait)
    permitStatus := sched.prepareForBindingCycle(ctx, fwk, state, podInfo)
    return scheduleResult, permitStatus
}

// pkg/scheduler/schedule_one.go (行 570-718, k8s v1.36.1)
// SchedulePod — 找节点 + 打分,核心调度算法
func (sched *Scheduler) SchedulePod(
    ctx context.Context, fwk fwk.Framework, state fwk.CycleState,
    podInfo *fwk.QueuedPodInfo,
) (ScheduleResult, *fwk.Status) {

    // ① PreFilter + Filter:找出所有满足硬约束的节点
    feasibleNodes, filteredNodesStatuses, fitErr := sched.findNodesThatFitPod(ctx, fwk, state, pod)
    if fitErr != nil {
        return ScheduleResult{}, fwk.AsStatus(fitErr)
    }

    // ② 如果没有可行节点 → 运行 PostFilter(DefaultPreemption 抢占)
    if len(feasibleNodes) == 0 {
        status := fwk.RunPostFilterPlugins(ctx, state, pod, filteredNodesStatuses)
        if !status.IsSuccess() && status.Requeue() { /* 抢占成功,等待 nominatedNode */ }
        return ScheduleResult{}, status
    }

    // ③ PreScore + Score:为所有可行节点打分
    priorityList, scoreErr := sched.prioritizeNodes(ctx, fwk, state, pod, feasibleNodes)
    if scoreErr != nil {
        return ScheduleResult{}, fwk.AsStatus(scoreErr)
    }

    // ④ 选择得分最高的节点
    host, err := selectHost(logger, priorityList)
    return ScheduleResult{
        SuggestedHost: host,
        EvaluatedNodes: len(feasibleNodes),
        FeasibleNodes: len(feasibleNodes),
    }, nil
}

// pkg/scheduler/schedule_one.go (行 628-718)
// findNodesThatFitPod — Filter 阶段的实现
func (sched *Scheduler) findNodesThatFitPod(
    ctx context.Context, fwk fwk.Framework, state fwk.CycleState, pod *v1.Pod,
) ([]*v1.Node, *fwk.NodeToStatus, error) {

    // ① PreFilter 插件(预处理)
    _, s := fwk.RunPreFilterPlugins(ctx, state, pod)
    if !s.IsSuccess() { return nil, nil, s.AsError() }

    // ② Filter 插件(并行遍历所有节点)
    nodes, statuses := fwk.RunFilterPluginsWithNominatedPods(ctx, state, pod, sched.nodeInfoSnapshot)
    return nodes, statuses, nil
}

// pkg/scheduler/schedule_one.go (行 938-1054)
// prioritizeNodes — Score 阶段的实现
func (sched *Scheduler) prioritizeNodes(
    ctx context.Context, fwk fwk.Framework, state fwk.CycleState,
    pod *v1.Pod, feasibleNodes []*v1.Node,
) (framework.NodeScoreList, error) {

    fwk.RunPreScorePlugins(ctx, state, pod)         // ① PreScore
    scores, s := fwk.RunScorePlugins(ctx, state, pod)  // ② Score 插件(并行)
    if !s.IsSuccess() { return nil, s.AsError() }

    // ③ Extender 扩展(如果有外部调度器)
    if len(sched.Extenders) > 0 { /* 调用外部 Extender */ }

    // ④ 加权求和,返回排序后的节点列表
    result := fwk.ScoreNormalizedNodes(ctx, pod, scores)
    return result, nil
}

6.3 bindingCycle:异步绑定阶段

// pkg/scheduler/schedule_one.go (行 397-503, k8s v1.36.1)
// bindingCycle — 异步绑定阶段(在 goroutine 中运行)
func (sched *Scheduler) bindingCycle(
    ctx context.Context, fwk fwk.Framework, state *fwk.CycleState,
    podInfo *fwk.QueuedPodInfo, scheduleResult ScheduleResult, logger klog.Logger,
) {
    // ① PreBind PreFlight:检查前提条件(如 Pod 是否还在)
    if !fwk.RunPreBindPreFlights(ctx, podInfo.Pod) {
        sched.handleBindingCycleError(ctx, fwk, podInfo, scheduleResult, ...)
        return
    }

    // ② 等待 Permit 批准(如果 Permit 返回了 Wait)
    waitStatus := fwk.WaitOnPermit(ctx, podInfo.Pod)
    if !waitStatus.IsSuccess() {
        sched.handleBindingCycleError(ctx, fwk, podInfo, scheduleResult, ...)
        return
    }

    // ③ PreBind 插件(真正创建 PV 绑定等)
    if !fwk.RunPreBindPlugins(ctx, state, podInfo.Pod, scheduleResult.SuggestedHost).IsSuccess() {
        sched.handleBindingCycleError(ctx, fwk, podInfo, scheduleResult, ...)
        return
    }

    // ④ Bind 插件(向 apiserver 提交 Binding 对象)
    bindStatus := fwk.RunBindPlugins(ctx, state, podInfo.Pod, scheduleResult.SuggestedHost)
    if !bindStatus.IsSuccess() {
        sched.handleBindingCycleError(ctx, fwk, podInfo, scheduleResult, ...)
        return
    }

    // ⑤ PostBind 插件(清理)
    fwk.RunPostBindPlugins(ctx, podInfo.Pod, scheduleResult.SuggestedHost, scheduleResult.EvaluatedNodes)

    // ⑥ 通知 SchedulingQueue:该 Pod 调度完成
    sched.SchedulingQueue.Done(podInfo)
}

6.4 四组件的完整时序图

┌──────────────────────────────────────────────────────────────────────────────────┐
│                           ScheduleOne(ctx)                                        │
│                     pkg/scheduler/schedule_one.go:67                               │
│                                                                                   │
│  ┌──────────────────────────────────────────────────────────────────────────┐   │
│  │  阶段一:schedulingCycle(同步)                                           │   │
│  │                                                                              │   │
│  │  ① Cache.UpdateSnapshot()                                                  │   │
│  │     └─ 从 Cache 的双向链表提取 NodeInfo 快照                                │   │
│  │                                                                           │   │
│  │  ② Framework.RunPreFilterPlugins()                                         │   │
│  │     └─ PreFilter:预处理(NodeResourcesFit 算资源)                         │   │
│  │                                                                           │   │
│  │  ③ Framework.RunFilterPluginsWithNominatedPods()                            │   │
│  │     └─ Filter:并行遍历所有节点,Filter 插件链过滤                          │   │
│  │        └─ NodeResourcesFit / NodeAffinity / TaintToleration / ...         │   │
│  │                                                                           │   │
│  │  ④ [无可行节点] → RunPostFilterPlugins()                                   │   │
│  │     └─ DefaultPreemption:尝试抢占低优先级 Pod                              │   │
│  │                                                                           │   │
│  │  ⑤ Framework.RunScorePlugins()                                             │   │
│  │     └─ Score:并行对可行节点打分                                            │   │
│  │        └─ NodeResourcesFit / InterPodAffinity / PodTopologySpread / ...     │   │
│  │                                                                           │   │
│  │  ⑥ assumeAndReserve()                                                     │   │
│  │     ├─ Cache.AssumePod()     → 把 Pod "假设"到目标节点                    │   │
│  │     └─ RunReservePluginsReserve() → Reserve:VolumeBinding 占 PV           │   │
│  │                                                                           │   │
│  │  ⑦ RunPermitPlugins()                                                     │   │
│  │     └─ Permit:GangScheduling 等齐(可能返回 Wait)                        │   │
│  └──────────────────────────────────────────────────────────────────────────┘   │
│                                    │                                              │
│                                    │ permitStatus(Wait 或 Success)              │
│                                    ▼                                              │
│  ┌──────────────────────────────────────────────────────────────────────────┐   │
│  │  阶段二:bindingCycle(异步 goroutine)                                     │   │
│  │                                                                              │   │
│  │  ① RunPreBindPreFlights()                                                  │   │
│  │     └─ 检查 Pod 是否仍然存在、未被删除                                      │   │
│  │                                                                           │   │
│  │  ② WaitOnPermit()                                                         │   │
│  │     └─ 等待 Permit 批准(GangScheduling 等齐)                            │   │
│  │                                                                           │   │
│  │  ③ RunPreBindPlugins()                                                     │   │
│  │     └─ PreBind:VolumeBinding 真正创建 PV 绑定                           │   │
│  │                                                                           │   │
│  │  ④ RunBindPlugins()                                                        │   │
│  │     └─ Bind:DefaultBinder 提交 Binding 到 apiserver                      │   │
│  │                                                                           │   │
│  │  ⑤ RunPostBindPlugins()                                                   │   │
│  │     └─ PostBind:清理                                                      │   │
│  │                                                                           │   │
│  │  ⑥ SchedulingQueue.Done()                                                │   │
│  │     └─ 从 activeQ 标记该 Pod 已完成                                        │   │
│  └──────────────────────────────────────────────────────────────────────────┘   │
│                                                                                  │
│  ════════════════════════════════════════════════════════════════════════════════ │
│                                                                                  │
│  ┌──────────────────────────────────────────────────────────────────────────┐   │
│  │  Informer 事件回调(与调度主循环并发运行)                                  │   │
│  │                                                                              │   │
│  │  Node Updated → Cache.UpdateNode() + MoveAllToActiveOrBackoffQueue()       │   │
│  │     └─ 触发 unschedulableQ 中的相关 Pod 重新入队                           │   │
│  │                                                                              │   │
│  │  Pod Deleted (scheduled) → Cache.RemovePod()                              │   │
│  │     └─ 释放该节点资源                                                      │   │
│  │                                                                              │   │
│  │  Pod Created → addPodToSchedulingQueue() → activeQ.Add()                  │   │
│  │     └─ Pod 进入队列,等待 NextPod() 被调用                                  │   │
│  └──────────────────────────────────────────────────────────────────────────┘   │
└──────────────────────────────────────────────────────────────────────────────────┘

七、FAQ:常见疑问

思考记忆提示FAQ 是全篇的"临考前速背"模块,建议通读全篇后再来看这 20 题

  • Q1-Q6 围绕 Informer:Watch 断线 Resync 兜底、List 初始化、Indexer vs Cache 区别
  • Q7-Q13 围绕 Scheduling Queue:三队列流转、QueueingHint、Backoff、Pod 三态
  • Q14-Q20 围绕 Cache + 主循环:Assumed、Snapshot、schedulingCycle vs bindingCycle

Q1. 为什么不直接调 apiserver 查集群状态,要用 Informer?

直接调 apiserver = 每次调度都访问 etcd,数千节点的集群每秒数万次 List 请求,apiserver 直接被打满。用 Informer 后:启动做一次 List 初始化本地缓存,之后全部读本地内存(微秒级),APIServer 只在 Watch 事件到来时推送增量变更。APIServer 压力从 O(n) 降到 O(1)(只处理变更推送)。

Q2. Informer 的 Indexer 和 Scheduler 的 Cache 有什么区别?

Indexer 是 Informer 的本地缓存,存的是原始 Go 对象(如完整的 Node/Pod 结构体)。Cache 是 Scheduler 在 Indexer 基础上加工的调度语义数据(如"节点上还剩多少 CPU/内存"、"该节点上已有几个 Pod")。Cache 用 Indexer 快照初始化,再由 Informer 事件实时更新。

Q3. Informer 的 Watch 长连接断了会怎样?

Watch 断线后,Informer 会在本地缓存和真实集群状态之间出现几秒不一致(最终一致性)。Resync 机制(默认 30s)会定时重新 List 全量数据,触发 Update 回调修复本地缓存,保证最终同步。生产环境中 Watch 断线一般不超过几秒,对调度影响有限。

Q4. 为什么 scheduler 启动时要等 Informer 同步完成?

如果不等同步就调度,调度器看到的是"不完整"的集群状态——Pod 已在某节点运行,但 Cache 里还没有,Filter 可能选出一台实际已满的节点。informerFactory.WaitForCacheSync()pkg/scheduler/scheduler.go:545)是启动阶段最耗时的步骤之一,大集群可能需要数十秒。

Q5. Scheduling Queue 的三个子队列分别什么时候用?

activeQ:Pod 新建、Backoff 结束、事件触发后进入,堆结构按优先级排队,等 Pop() 被调度。backoffQ:调度失败(可重试)进入,指数退避等待(初始 10s,最长 5min),到期后转回 activeQ。unschedulablePods:永久失败(不可重试原因)进入,5 分钟后超时才允许重新入队。

Q6. QueueingHint 机制解决了什么问题?

v1.36.1 之前,节点变化会触发 MoveAllToActiveOrBackoffQueue 把所有 unschedulable Pod 全部清空重新入队,在 5000 节点、10000 Pod 集群里这是 O(n) 的操作。QueueingHint 让每个插件声明"我关心 X 资源的变化"(如 VolumeBinding 说"我关心 PVC.Delete"),只有相关 Pod 才被重新入队,变成 O(1)。

Q7. Pod 从创建到调度成功,在三个队列之间是怎么流转的?

Pod 创建 → addPod()(NodeName 为空)→ activeQ.Add()。
调度失败(可重试)→ backoffQ.Add()(指数退避)→ Backoff 到期 → activeQ.Add()。
调度失败(永久)→ unschedulablePods.Add() → 5 分钟超时 → backoffQ → activeQ → 重新调度。
调度成功 → deletePodFromSchedulingQueue()(移出 Queue)→ addAssignedPodToCache()(进入 Cache)。

Q8. Pod 在调度器里有哪三态?

未绑定(Unscheduled):NodeName 为空,Pod 在 SchedulingQueue 的 activeQ 中等待调度。已预占(Assumed):Filter 通过后,Cache 记录 Pod 预占节点资源,但 apiserver 的 Binding 还没提交,防止并发超售。已绑定(Bound):bindingCycle 完成,Pod.Spec.NodeName 被 apiserver 写入 etcd,调度完成。

Q9. 两个 Pod 同时调度到同一节点会怎样?Cache 如何防止并发超售?

如果没有 Assumed 机制,Pod A 和 Pod B 在 Filter 阶段都看到节点 X 有资源,都选择 X,但 Bind 只能成功一个。Assumed 机制在 Filter 前就"预占"资源:Pod A Filter 时把节点 X 的可用资源减去 A 的请求量,Pod B Filter 时只能看到扣减后的资源,从而不会选 X。Assumed 是 Filter 看到"未来"资源状态的关键。

Q10. bindingCycle 失败了会怎样?

bindingCycle 失败时,RunBindingPlugins 返回错误,FinishBindingpkg/scheduler/schedule_one.go:370)会执行 Assumed 回滚:Cache 中该 Pod 对应的 Assumed 资源被释放,节点 X 的资源恢复到调度前状态,Pod 回到 unschedulableQ 等待重新调度。整个过程对调度器透明,不影响其他 Pod 的调度。

Q11. Cache Snapshot 机制为什么重要?

一个 Pod 的整个 schedulingCycle(Filter → Score → Reserve)期间,所有节点的资源视图必须是一致的。如果 Filter 阶段 Node A 有 4 核可用,Score 阶段变成 2 核(另一个 Pod 在此期间被调度),打分和 Bind 就会出错。Cache.UpdateSnapshot() 在 schedulingCycle 开始前提取一份"冻结"的 NodeInfo 快照,所有插件共享这份快照,保证调度语义的一致性。

Q12. schedulingCycle 和 bindingCycle 为什么要分开?

schedulingCycle 是同步的:Filter/Score/Reserve 必须全部完成才能进入下一阶段,目的是锁定资源、保证一致性。
bindingCycle 是异步的:Bind 调 apiserver 写 etcd,网络延迟不确定,可能需要几秒。如果 bindingCycle 同步阻塞,调度器主循环就会卡住,其他 Pod 全部等待。异步解绑了调度吞吐量和 Bind 网络延迟的依赖关系。

Q13. nominatedNodeName 是干什么用的?

抢占者(Preemptor)Pod 抢占成功后,调度器在 status.nominatedNodeName 字段写入目标节点名。它的作用是告诉调度器"请等这个节点"——即使节点当前还不满足条件(如节点污点还未撤销),调度器也会给这个 Pod 一定的时间窗口等待,而不是立刻把它扔回 unschedulableQ。这样可以避免抢占者和被抢占者都卡住的"双输"局面。

Q14. 节点变成 Unschedulable 时,调度器会发生什么?

节点 Unschedulable 变化触发 updateNodeInCachepkg/scheduler/eventhandlers.go:53)→ Cache 更新节点信息 → QueueingHint 判断 → MoveAllToActiveOrBackoffQueue 把所有受影响 Pod 移回 activeQ → 这些 Pod 在下一个调度周期重新 Filter。该节点已调度的 Pod 不受影响,已 Bind 的 Pod 不会被驱逐(除非节点变为 NotReady)。

Q15. 节点删除时,调度器会发生什么?

节点 Delete 触发 deleteNodeFromCachepkg/scheduler/eventhandlers.go:67):Cache 中该节点被移除,所有分配给该节点的 Pod 被标记为"未调度",通过 sched.addPodToSchedulingQueue 进入 activeQ 重新调度。这些 Pod 的 spec.nodeName 被清空,状态变为 Pending,等待重新调度。

Q16. 调度失败后的 Pod 退避(Backoff)时间是怎么算的?

指数退避策略(pkg/scheduler/internal/queue/backoff_utils.go):初始 BackoffDuration = 10ms,每次失败后乘以 2(指数增长),最大不超过 5min。具体时间还受 Pod 优先级影响:高优先级 Pod 的 Backoff 更短(更急着想重试),低优先级 Pod 的 Backoff 更长(让高优先级先跑)。

Q17. Cache 里已完成(Completed/Deleted)的 Pod 什么时候被清理?

Cache 的 RemovePod 由 Pod Informer 的 Delete 事件触发(pkg/scheduler/eventhandlers.go:204):Pod 删除事件到达 → deleteAssignedPodFromCache → 从双向链表中摘除,释放对应节点的资源。没有独立的 GC 线程——删除即清理,因为 Informer 的 Watch 实时推送变更,Cache 始终与 etcd 保持最终一致。

Q18. 多个 Scheduler Profile 之间会冲突吗?

不会冲突。每个 Profile 有独立的 frameworkImpl、独立的插件注册表和独立的 SchedulingQueue。但 Cache 是共享的pkg/scheduler/scheduler.go:200)——所有 Profile 共用同一个 Cache 实例,因为 Cache 只存节点和已调度 Pod 的资源信息,不区分由哪个 Profile 调度。这保证了同一个节点不会被两个 Profile 同时 Bind 到不同 Pod。

Q19. 为什么 SchedulingQueue 和 Cache 都需要加锁?

因为它们都是读写并发的:
SchedulingQueue 被 Informer 回调(写)和 ScheduleOne.Pop()(读)并发访问。
Cache 被 Informer 回调(写)和 Filter/Score(读)并发访问。
两者都使用 Go 的 sync.RWMutex:写操作用 Lock(互斥),读操作用 RLock(读多写少时并发性能好)。读操作为什么不用原子操作?因为需要批量读取节点快照(一次性拿到所有节点信息),RWMutex 是最佳选择。

Q20. 调度器的"读写分离"设计和 k8s 其他组件有什么关系?

kube-controller-manager、kubelet、operator 都遵循同样的"读 informer cache、写 apiserver"模式。调度器的特殊性在于它的"读"是高频批量读(一次调度要读数千节点),所以在 informer cache 之上又加了一层 Cache(调度语义加工)和 Snapshot(一致性冻结)。理解了这个模式,就理解了 k8s 控制平面的所有读写设计。

全篇必记总纲

kube-scheduler 四大组件的职责闭环:Informer 感知集群变化驱动 Scheduling Queue 缓冲待调度 PodScheduleOne 从 Queue 拿 Pod 走 Cache 快照执行 Scheduling Framework 插件流水线,Bind 结果写回 apiserver 触发 Informer 回调,再驱动新一轮调度。全篇围绕一条主线:读本地内存快照,写单点 apiserver

七·1、对比与关联:四组件的数据流

思考记忆提示本节是全篇的"全景地图",用数据流把四大组件串起来

  • 读路径:Informer → Cache → Snapshot → Filter/Score(全程读内存)
  • 写路径:Bind → APIServer(单点写入 etcd)
  • 面试高频提问:调度器什么时候访问 apiserver?什么时候只读本地内存?

7.1 数据流总图

┌──────────────┐    事件回调    ┌──────────────────┐    AssumePod     ┌───────────┐
│   Informer   │──────────────►│  SchedulingQueue │                │   Cache    │
│  (apiserver) │               │  (activeQ 等)   │◄─────────────│  (NodeInfo │
│              │◄──────────────│                 │   AddPod       │   快照)   │
│  Watch 事件  │  NextPod()   │                 │─────────────►│            │
└──────────────┘               └────────┬─────────┘  UpdateSnapshot│           │
                                        │                         │           │
                              Pop()      │                         │           │
                                        ▼                         │           │
┌──────────────┐               ┌──────────────────┐               │           │
│   kube-apiserver              │  ScheduleOne()   │               │           │
│              │◄──────────────│                  │               │           │
│  Binding     │   Bind 提交   │  Filter + Score  │◄──────────────┘           │
│  写入        │               │  (Framework)    │  NodeInfoSnapshot          │
└──────────────┘               └──────────────────┘                            │
                                 │                                                   │
                                 │ Pod 解绑                                          │
                                 ▼                                                   │
                        ┌──────────────────┐                                       │
                        │  回到 SchedulingQueue                                     │
                        │  (backoffQ 或 unschedulablePods)                          │
                        └───────────────────────────────────────────────────────────┘

7.2 四组件职责矩阵

组件源码路径核心结构体数据类型线程安全与 apiserver 的关系
Informer eventhandlers.go SharedInformerFactory Raw API Objects 线程安全 Watch + List
SchedulingQueue backend/queue/ PriorityQueue QueuedPodInfo 需要锁(lock) 无直接交互
Cache backend/cache/ cacheImpl NodeInfo(含聚合资源) 需要锁(mu) Bind 时写入 Binding
Scheduling Framework framework/runtime/ frameworkImpl CycleState 无状态(只读快照) 无直接交互

小贴士为什么 SchedulingQueue 和 Cache 都要锁?

两个组件虽然都在内存中,但访问模式不同导致都需要加锁:SchedulingQueue 被 Informer 回调(写)和 ScheduleOnePop()(读)并发访问;Cache 被 Informer 回调(写)和 Filter/Score(读)并发访问。两者都使用 Go 的 sync.RWMutex:读多写少时用 RLock,并发性能好。

必记核心

四组件数据流的核心是读写分离:读全部走本地内存(Informer Cache → SchedulingQueue 快照 → Filter/Score),写只走 apiserver(Bind 提交 Binding)。这种设计是 kube-scheduler 高性能调度的工程根基,也是理解整个 k8s 控制平面架构的钥匙。

八、Roadmap:调度专题后续预告

本篇覆盖了 kube-scheduler 的内部架构骨架——四大组件的定位、数据结构和协作方式。后续篇章会逐一深入每个组件的细节:

  • 专题二(已发布):Scheduling Framework 6 大内置插件精讲——NodeResourcesFit / NodeAffinity / TaintToleration / InterPodAffinity / PodTopologySpread / VolumeBinding 的源码行号 + 接口断言
  • 专题三:Scheduling Framework 扩展点源码精讲——framework/runtime/framework.go 的 13 个 RunXxxPlugins 方法,CycleState 如何在扩展点之间传值
  • 专题四:DefaultPreemption 抢占机制——当 Filter 阶段没有可行节点时,调度器如何通过抢占低优先级 Pod 来创造空间
  • 专题五:SchedulingQueue 深度解析——QueueingHint 机制、PriorityQueue 的三个子队列如何协同、Backoff 算法的指数退避策略
  • 专题六:Out-of-Tree 插件实战——用 RegisterPluginBuilder 注册自定义调度插件

调度专题的目标读者是资深运维开发:能读 Go 源码、有集群运维经验、对 k8s 整体架构已有认知。下一篇我会从 Scheduling Framework 的扩展点源码切入,把框架运行时的"骨架"补全。

全篇必记总纲

kube-scheduler 四大组件的职责闭环:Informer 感知集群变化驱动 Scheduling Queue 缓冲待调度 PodScheduleOne 从 Queue 拿 Pod 走 Cache 快照执行 Scheduling Framework 插件流水线,Bind 结果写回 apiserver 触发 Informer 回调,再驱动新一轮调度。全篇围绕一条主线:读本地内存快照,写单点 apiserver


本文参考与源码链接:
  • scheduler.go · Scheduler struct
  • eventhandlers.go · Informer 注册
  • scheduling_queue.go · PriorityQueue
  • cache.go · cacheImpl
  • framework.go · frameworkImpl
  • registry.go · NewInTreeRegistry
  • schedule_one.go · ScheduleOne 入口

posted @ 2026-06-21 16:54  左扬  阅读(6)  评论(0)    收藏  举报