当前位置: 首页 > news >正文

Kubernetes 源码 / Operator 专题【左扬精讲】——kube-scheduler(调度专题):初识调度模型、内部架构与事件驱动机制

Kubernetes 源码 / Operator 专题【左扬精讲】——kube-scheduler(调度专题):初识调度模型、内部架构与事件驱动机制

当你 kubectl apply 一个 Pod 之后,Kubernetes 内部发生了什么事?Pod 是怎么"被分配"到某个节点上的?为什么同样一份 Deployment,有时会调度到这台机器,有时又跑到另一台?这背后默默工作的核心组件,就是 kube-scheduler

对很多初学者来说,kube-scheduler 是个"熟悉的陌生人":知道它是调度器、知道它选节点、但不知道它具体怎么选。本文作为调度专题的开篇,目标不是带你读完所有源码,而是建立一个总览心智模型。读完你应该能回答三个问题:kube-scheduler 的调度模型是什么?它内部架构由哪些模块组成?它如何靠事件驱动持续工作?至于 Scheduling Framework 各扩展点的细节、调度器插件开发、抢占算法、Scheduler Profile 等进阶内容,会在后续文章里展开。

本文源码分析基于 k8s v1.36.1,全部以 cmd/kube-scheduler/pkg/scheduler/ 目录下的实际代码为依据。

Kubernetes Scheduler Scheduling Framework Informer 事件驱动 Go k8s v1.36.1

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

★ 重点掌握(必须)
   • 两阶段调度模型:Filtering(过滤)→ Scoring(打分)→ Binding(绑定),三步走通杀一个 Pod
   • 核心 struct 与目录组织:Scheduler(pkg/scheduler/scheduler.go) / SchedulingQueue(backend/queue) / Cache(backend/cache) / Profiles(profile)
   • 事件驱动三件套:SharedInformer → ResourceEventHandler → SchedulingQueue,靠 Watch 增量同步 + Queue 异步处理

☆ 次重点(了解即可)
   • Scheduling Framework 的扩展点(PreFilter/Filter/Score/Reserve/Bind 等)有哪些,调用顺序如何
   • Assume(假定缓存)是什么、为什么需要它、Bind 是怎么把结果写回 apiserver 的
   • cmd/kube-scheduler/app/server.go 的 Run 流程(启动入口)


📋 文章目录

  1. 一、Why:为什么需要 kube-scheduler
  2. 二、What:kube-scheduler 的调度模型
  3. 三、How:kube-scheduler 的内部架构
  4. 四、Detail:事件驱动机制深度解析
  5. 五、Roadmap:后续专题预告与学习路径

一、Why:为什么需要 kube-scheduler

在 k8s 集群中,kube-scheduler 是控制平面的核心组件之一。它唯一的工作职责,就是为每一个新创建的 Pod 挑选一个最合适的 Node,并把这个决定以 Binding 对象的形式写回 apiserver。一旦 Binding 成功,对应节点上的 kubelet 就会"看到"这个 Pod 并开始创建容器。

它的工作流程可以用一句话概括:监听 → 入队 → 选节点 → 绑定。但这四个动作背后,隐藏着 k8s 集群里最复杂的并发模型之一——成百上千个 Pod 同时涌入、成百上千个 Node 不断变化、还要兼顾资源、亲和性、污点、抢占、本地卷、动态资源分配等几十种约束条件。所以 kube-scheduler 的设计目标就是:在高并发场景下,依然能可扩展地完成 Pod ↔ Node 的最优匹配。

理解 kube-scheduler 不仅是面试常考点,更是深入 k8s 的必经之路:它把 client-goapimachinery、informer、workqueue、controller pattern 几乎所有核心概念都串了起来——可以说,读懂 kube-scheduler,等于读懂半个 k8s 核心代码

🚀 小贴士  — 集群里可以同时跑多个 kube-scheduler 实例,通过 Leader Election 选举出一个 Leader 真正工作,其他作为备份(v1.36.1 还新增了 Coordinated LeaderElection,更进一步支持多 scheduler 协调)。
这意味着 kube-scheduler 本身也是一个"高可用"组件,但同一时刻只有一个实例在执行调度决策,避免并发冲突。


二、What:kube-scheduler 的调度模型

2.1 调度问题的本质

从算法角度看,kube-scheduler 解决的是一个典型的多约束配对问题:给定一个待调度的 Pod 和一组 Node,找出"最合适"的那个 Node。这里的"最合适"由一系列过滤(Filter)+打分(Score)规则共同决定。Filter 回答"能不能",Score 回答"好不好"。

社区里很多人把这套模型叫两阶段调度(Two-Stage Scheduling)

Text
┌──────────────────────────────────────────────────────────────┐
│  Stage 1: Filtering(过滤 / 预选)                              │
│    输入:Pod 描述 + 全部 Node 列表                              │
│    操作:依次执行若干 Filter 插件,每条规则淘汰不满足的 Node     │
│    输出:Feasible Nodes(可行节点集合)                          │
├──────────────────────────────────────────────────────────────┤
│  Stage 2: Scoring(打分 / 优选)                                │
│    输入:Feasible Nodes                                         │
│    操作:每个 Score 插件为每个 Node 打一个分数(0~100)          │
│    输出:每个插件的得分加权求和,得到最终排名                    │
├──────────────────────────────────────────────────────────────┤
│  Stage 3: Binding(绑定)                                       │
│    输入:排名第一的 Node                                        │
│    操作:调用 Bind 插件把 Pod.Spec.NodeName 写入并提交 apiserver  │
│    输出:apiserver 创建 Binding 对象                            │
└──────────────────────────────────────────────────────────────┘

这套模型自 kube-scheduler v1.0 起就没有变过——变的只是每阶段的插件实现、扩展点和性能优化。

2.2 一个 Pod 的"一生"

理解了"两阶段"还不够,kube-scheduler 真正的复杂度在于它不止处理新 Pod。下面这些场景全部要触发调度:

  • 新建 Pod:spec.nodeName 为空,Pod 处于 Pending 状态,需要被调度到某个 Node
  • 已经调度的 Pod:Node 上有变化(资源变化、label 改变、污点改变),需要重新评估是否仍然"合适"
  • 被抢占的 Pod:集群资源紧张时,某个高优先级 Pod 抢占低优先级 Pod,被抢占的 Pod 需要重新调度
  • unschedulable Pod:之前调度失败(如资源不足)的 Pod,当相关资源释放后需要重新尝试

这就需要 kube-scheduler 维护一个待调度 Pod 队列,并对外监听多种资源变化。这正是后面要讲的"事件驱动"机制的核心价值。

2.3 Scheduling Framework:插件化的两阶段

自 k8s 1.19 起,kube-scheduler 全面迁移到了 Scheduling Framework 架构(v1.36.1 中位于 staging/src/k8s.io/kube-scheduler/framework/)。它的核心思想:把"调度一个 Pod"拆成若干扩展点(Extension Point),每个扩展点是一个 Go interface,开发者可以注册任意多个 plugin 来实现。Framework 负责把这些 plugin 按固定顺序串成一个调度流水线。

下表列出 v1.36.1 中的所有扩展点(按调度流水线顺序排列)。注意:初学者只需要了解前 5 个就够用了,其余会在后续 Scheduling Framework 专题里详解。

扩展点对应的 Go Interface所属阶段作用
PreEnqueue PreEnqueuePlugin 入队前 Pod 进队列前做快速检查(如 schedulingGates
PreFilter PreFilterPlugin 过滤前预处理 预处理(如把资源请求算好、提前算好 inter-pod affinity),写 CycleState
Filter FilterPlugin 过滤(PreSelection) 逐个 Node 检查是否满足硬约束(资源、亲和、污点、端口等)
PostFilter PostFilterPlugin 过滤失败后 尝试抢占(默认实现是 defaultpreemption)
PreScore PreScorePlugin 打分前预处理 为 Score 阶段做轻量预处理(如批量算亲和性)
Score ScorePlugin 打分(Scoring) 给每个 Feasible Node 打分(资源均衡、亲和权重、镜像本地性等)
NormalizeScore ScoreExtensions 分数归一化 把不同 plugin 的分数归一化到 [0, 100]
Reserve ReservePlugin 假定(Assume) 在 cache 中"假定"这个 Pod 已经分配到该 Node(关键设计,下文详述)
Permit PermitPlugin 同步等待 可阻塞等待外部批准(默认 none,所有 Pod 立即放行)
PreBind PreBindPlugin 绑定前 绑定前执行(如 VolumeBinding 预留 PV/PVC)
Bind BindPlugin 绑定 把 Pod 的 nodeName 写回 apiserver(默认实现是 DefaultBinder)
PostBind PostBindPlugin 绑定后 绑定后的清理(默认 none)

一个 Pod 走完这个流水线,就是一次完整的 scheduling cycle。如果失败(Filter 全部淘汰),会进入 PostFilter / 抢占流程

2.4 Assume(假定缓存):两阶段解耦的关键设计

初学者最应该理解的设计是 Assume(假定缓存)。它解决了一个核心问题:

在 k8s v1.0 时代,调度器选完节点后,必须等 Binding 写回 apiserver 完成(可能耗时几十毫秒~几秒),才能认为这个 Node 的资源被占用了。这就导致:

  • 如果两个 Pod 几乎同时调度,第二个 Pod 可能把第一个 Pod"还没写入"的资源也算进来 → 过载(overbooking)
  • 高并发场景下,Binding 网络往返成为瓶颈

Assume 机制巧妙地解决了两者:调度器一旦决定把 Pod 放到 Node X,立刻在本地 cache 里把 Pod 标记为"假定已分配"。之后调度别的 Pod 时,cache 会把这个假定的 Pod 算进 Node X 的资源占用里。

后续如果 Binding 成功,cache 里的假定状态会被"确认"为正式状态;如果 Binding 失败(如 apiserver 拒绝),cache 会调用 ForgetPod 撤销假定,把资源"还回去"。这就是为什么 pkg/scheduler/backend/cache/cache.goAssumePodForgetPod 这对方法在 v1.36.1 中依然存在(位置:pkg/scheduler/backend/cache/cache.go:397)。

💡 注意
在 v1.36.1 中,Assume 机制对应的源码方法是 Scheduler.Cache.AssumePod(),由 pkg/scheduler/schedule_one.goschedulingCycle 成功结束后立即调用。后续 bind 阶段无论成功失败,对应的 cache 状态都已经准备好了,不会出现"先来后到"问题。


三、How:kube-scheduler 的内部架构

我们把视线从"模型"切到"代码",看 kube-scheduler 的内部模块。k8s 1.36.1 的 kube-scheduler 实现主要在两个目录:

  • cmd/kube-scheduler/:二进制入口(main.go / app/server.go
  • pkg/scheduler/:核心实现(含 scheduler.goschedule_one.goeventhandlers.gobackend/queue/backend/cache/framework/
  • staging/src/k8s.io/kube-scheduler/:对外发布的 Scheduling Framework 接口库(独立 module)

3.1 入口:cmd/kube-scheduler/app/server.go

二进制的启动入口遵循 k8s 组件的"标准模板":cobra 命令 → flags 解析 → runCommandSetupRun。我们重点看 Run 函数:

// cmd/kube-scheduler/app/server.go (行 173-310, k8s v1.36.1)

Go
// Run executes the scheduler based on the given configuration. It only returns on error or when context is done.
func Run(ctx context.Context, cc *schedulerserverconfig.CompletedConfig, sched *scheduler.Scheduler) error {logger := klog.FromContext(ctx)logger.Info("Starting Kubernetes Scheduler", "version", utilversion.Get())// Configz registration.if cz, err := configz.New("componentconfig"); err != nil { ... }// Start events processing pipeline.cc.EventBroadcaster.StartRecordingToSink(ctx.Done())defer cc.EventBroadcaster.Shutdown()// Setup healthz checks.readyzChecks = append(readyzChecks, healthz.NewShutdownHealthz(ctx.Done()))// 启动健康检查 endpointif cc.SecureServing != nil { ... }// 核心:启动 Informer(Pod / Node / PV / PVC / CSINode ...)并等待同步startInformersAndWaitForSync := func(ctx context.Context) {cc.InformerFactory.Start(ctx.Done())if cc.DynInformerFactory != nil {cc.DynInformerFactory.Start(ctx.Done())}cc.InformerFactory.WaitForCacheSync(ctx.Done())// 关键:等待所有事件 handler 完成首次同步if err := sched.WaitForHandlersSync(ctx); err != nil {logger.Error(err, "handlers are not fully synchronized")}close(handlerSyncReadyCh)}// 启动 Leader Election(如启用)if cc.LeaderElection != nil { ... }// 启动调度器主循环sched.Run(ctx)return nil
}

可以看到,Run 函数的逻辑非常克制:它只负责搭建"骨架"——健康检查、配置注册、Informer 启动、Leader 选举——然后调用 sched.Run(ctx) 把自己交给核心循环阻塞等待。所有真正的调度逻辑都在 pkg/scheduler 里。

3.2 核心 struct:Scheduler

整个 kube-scheduler 的"灵魂"是 pkg/scheduler/scheduler.go 中的 Scheduler struct(v1.36.1 定义于行 68)。我们可以把它拆成 5 个核心字段:

// pkg/scheduler/scheduler.go (行 68-125, k8s v1.36.1)

Go
type Scheduler struct {// 1. 本地缓存:NodeInfo、PVC、PV、ResourceClaim 等//    Cache 决定了 Filter / Score 阶段能"看到"什么数据Cache internalcache.Cache// 2. 调度队列:待调度的 Pod 在这里排队SchedulingQueue internalqueue.SchedulingQueue// 3. Scheduling Framework 句柄:含全部 Profile 和 PluginProfiles profile.Map// 4. 客户端:与 apiserver 交互client clientset.Interface// 5. 闭包函数:可被外部替换为测试用的 fakeNextPod         func(logger klog.Logger) (*framework.QueuedPodInfo, error)SchedulePod     func(ctx context.Context, fwk framework.Framework, ...) (ScheduleResult, error)FailureHandler  FailureHandlerFnregisteredHandlers []cache.ResourceEventHandlerRegistration  // 已注册的事件 handler
}

一个很巧妙的设计是 NextPod / SchedulePod / FailureHandler 都是函数字段(闭包),默认实现由 applyDefaultHandlers() 注入(行 127),但测试时可以用 fake 实现替换。这是典型的"依赖注入"模式,让 Scheduler 易于单测。

3.3 主循环:Scheduler.Run

看核心的 Run 函数(pkg/scheduler/scheduler.go:546):

// pkg/scheduler/scheduler.go (行 545-573, k8s v1.36.1)

Go
// Run begins watching and scheduling. It starts scheduling and blocked until the context is done.
func (sched *Scheduler) Run(ctx context.Context) {logger := klog.FromContext(ctx)sched.SchedulingQueue.Run(logger)              // 1. 启动队列(维护 activeQ/unschedulableQ/backoffQ)if sched.APIDispatcher != nil {sched.APIDispatcher.Run(logger)            // 2. 启动异步 API 调用派发器}// 3. 关键:启动 scheduleOne 循环,0 表示不间隔go wait.UntilWithContext(ctx, sched.ScheduleOne, 0)<-ctx.Done()                                   // 4. 阻塞到 ctx 取消if sched.APIDispatcher != nil {sched.APIDispatcher.Close()}sched.SchedulingQueue.Close()                  // 5. 关闭队列err := sched.Profiles.Close()                  // 6. 关闭 Profiles(释放 plugin 资源)
}

整个 Scheduler.Run 极其简洁:3 行核心逻辑,1 行阻塞等待。真正的"调度"行为发生在 scheduleOne 这个循环函数里(下一个 goroutine 启动)。

🌟 设计精髓
为什么用 go wait.UntilWithContext(ctx, sched.ScheduleOne, 0) 启动新 goroutine?注释说得很清楚:scheduleOne 会阻塞在 NextPod 上等下一个 Pod。如果在主 goroutine 里跑,关闭队列时就会死锁(没人调 Pop,主 goroutine 永远不退出)。所以单独开一个 goroutine 跑 scheduleOne,主 goroutine 专门负责监听 ctx 取消和清理。

3.4 核心模块全景图

把上面的信息拼起来,kube-scheduler 内部可以拆成 6 大模块,它们各司其职、相互协作:

Text
┌────────────────────────────────────────────────────────────────────┐
│              kube-scheduler (k8s v1.36.1) 内部模块全景                 │
└────────────────────────────────────────────────────────────────────┘│┌──────────────────────┐    ┌──────────────────────┐                ││  1. cmd/kube-scheduler│   │  2. Scheduler struct  │               ││  ──────────────────  │    │  ──────────────────  │                ││  main.go             │    │  pkg/scheduler/       │                ││  app/server.go       │    │  scheduler.go         │                ││  ─ Run/Setup/flags   │    │  ─ Run/ScheduleOne    │                ││  ─ healthz/metrics   │    │  ─ Cache/Queue/Profile│                ││  ─ Leader Election   │    │  ─ NextPod/SchedulePod│                │└──────────┬───────────┘    └──────────┬───────────┘                ││                           │                            ││  启动入口                  │  核心调度循环               │└─────────┬─────────────────┘                            │▼                                              │┌─────────────────────────────────────────────────┐                ││  3. eventhandlers.go                              │                ││  ─────────────────────────────────────────────   │                ││  addAllEventHandlers:                            │                ││    注册 Pod/Node/PV/PVC/CSINode/Service 等        │                ││    informer 的 ResourceEventHandler              │                ││  ─ 把集群事件"翻译"成 SchedulingQueue 的操作      │                ││    (addPod/updatePod/deletePod/...)            │                │└──────────┬──────────────────────────────────────┘                ││                                                        │▼                                                        │┌──────────────────────────────────────────────────┐               ││  4. backend/queue/scheduling_queue.go              │               ││  ─ PriorityQueue 实现                              │               ││  ─ 三段队列: activeQ / unschedulableQ / backoffQ  │               ││  ─ Pop() / Add() / AddIfNotPresent() / MoveAll...  │               ││  ─ 维护 inFlightPods(QHint 特性)                │               │└──────────┬───────────────────────────────────────┘               ││ Pop() 返回 QueuedPodInfo                               │▼                                                        │┌──────────────────────────────────────────────────┐               ││  5. schedule_one.go (ScheduleOne / schedulePod)   │               ││  ──────────────────────────────────────────────  │               ││  ① 选 Framework(按 pod.Spec.SchedulerName)      │               ││  ② schedulingCycle:                              │               ││     PreFilter → Filter → PostFilter               │               ││     → PreScore → Score → Reserve (AssumePod)      │               ││  ③ bindingCycle:                                  │               ││     Permit → PreBind → Bind (写 apiserver)        │               ││  ④ 失败 → handleSchedulingFailure                 │               │└──────────┬───────────────────────────────────────┘               ││ 调用 Filter / Score                                    │▼                                                        │┌──────────────────────────────────────────────────┐               ││  6. framework/ (Scheduling Framework 核心)         │               ││  ──────────────────────────────────────────────  │               ││  framework.go        ─ Framework interface 实现  │               ││  plugins/            ─ 内置插件(NodeResourcesFit,│               ││                        NodeAffinity, TaintToleration,│             ││                        VolumeBinding, PodTopologySpread, ...)│   ││  cycle_state.go      ─ CycleState(plugin 间传数据)│              ││  registry.go         ─ Plugin Registry            │               │└──────────┬───────────────────────────────────────┘               ││                                                        │▼                                                        │┌──────────────────────────────────────────────────┐               ││  7. backend/cache/cache.go (Cache)                 │               ││  ──────────────────────────────────────────────  │               ││  AddPod/UpdatePod/RemovePod/AddNode/...          │               ││  AssumePod/ForgetPod(假定缓存,调度即占用资源)   │               ││  UpdateSnapshot(生成 snapshot 给 Score 阶段用)   │               │└──────────────────────────────────────────────────┘               │

记住这个全景图:事件驱动模块(3)把集群变化翻译为队列操作 → 队列模块(4)管理待调度 Pod → 调度循环(5)从队列取 Pod 跑调度流水线 → Framework(6)执行具体的 Filter / Score 逻辑 → Cache(7)为整个过程提供"实时"集群状态视图。

3.5 scheduleOne 一次循环做了什么

代码位于 pkg/scheduler/schedule_one.go(v1.36.1 行 67)。它的核心骨架如下:

// pkg/scheduler/schedule_one.go(k8s v1.36.1 关键流程简化)

Go
func (sched *Scheduler) ScheduleOne(ctx context.Context) {// 1. 从 SchedulingQueue 阻塞获取下一个待调度 PodpInfo, err := sched.NextPod(logger)if pInfo == nil { return }// 2. 根据 pod.Spec.SchedulerName 选择对应的 Framework(profile)fwk, err := sched.frameworkForPod(pInfo.Pod)// 3. 判断是否跳过(如 pod 已被 nominated 或被替换)if skip, status := sched.skipPodSchedule(ctx, fwk, pInfo.Pod); skip { return }// 4. scheduling cycle:跑 Filter + Scorestate := framework.NewCycleState()scheduleResult, status := sched.schedulingCycle(ctx, fwk, state, pInfo)// 5. 调度成功 → binding cycleif status.IsSuccess() {status = sched.bindingCycle(ctx, fwk, state, scheduleResult, pInfo, start, podsToActivate)}// 6. 失败 → handleSchedulingFailureif !status.IsSuccess() {sched.FailureHandler(ctx, fwk, pInfo, status, clearNominatedNode, start)}
}

注意 schedulingCycle 和 bindingCycle 是分开的。schedulingCycle 只在内存里"算"出最合适的 Node 并假定(Assume),bindingCycle 才真正把 Binding 对象写到 apiserver。中间用 CycleStatepkg/scheduler/framework/cycle_state.go)传数据——这是一个基于 sync.Map 的"写一次读多次"容器,让 PreFilter 算出的中间结果在 Filter 阶段被复用。

3.6 SchedulingQueue:三段优先级队列

实现位于 pkg/scheduler/backend/queue/scheduling_queue.go,核心是 PriorityQueue。它内部维护三个堆/队列:

队列存放什么何时出队
activeQ 新 Pod / 重新激活的 Pod 按 priority + 时间排序,由 scheduleOne Pop() 取出
unschedulableQ 调度失败的 Pod(含失败原因) 等 backoff timer 到期 或 相关事件触发 MoveAllToActiveOrBackoffQueue
backoffQ 被 backoff 限流的 Pod 指数退避计时器到期后移入 unschedulableQ
inFlightPods(v1.36.1) 正在被 scheduleOne 处理的 Pod 处理完成后调 Done(uid) 移除;用于 QueueingHint 特性

为什么要分这么多队列?防止"抖动"和"惊群"。比如一个 Pod 调度失败,盲目立刻重试没有意义(资源还没释放),所以先放 unschedulableQ 等事件触发;又比如大量 Pod 同时涌入,先在 backoffQ 限流避免 apiserver 过载。

🚀 v1.36.1 新增
v1.36.1 引入了 QueueingHint 特性:插件可以告诉队列"某种事件会让某些 Pod 重新可调度",队列据此精准地把 Pod 从 unschedulableQ 移到 activeQ,避免全量重排。代码位置:pkg/scheduler/framework/plugins/schedulinggates/scheduling_gates.go:69(SchedulingGates 插件已率先支持)。


四、Detail:事件驱动机制深度解析

到这里你可能有个疑问:调度器怎么知道有新的 Pod?Node 资源变化时它怎么知道要重排队列?答案就是本节要讲的事件驱动机制。它的核心是 k8s 所有组件都在用的标准模式:List + Watch + Local Cache + EventHandler

4.1 整体流程图

Text
              ┌────────────────────┐│    kube-apiserver   ││   (etcd 真实存储)   │└─────────┬──────────┘│   ▲List 拉全量│   │Watch 推增量(HTTP long-poll)▼   │┌──────────────────────────────┐│      SharedInformerFactory     ││      (client-go 提供)          ││  ─ PodInformer / NodeInformer  ││  ─ PVInformer / PVCInformer    ││  ─ CSINodeInformer / ...       │└─────────────┬────────────────┘│ Reflector 把事件分发给▼┌──────────────────────────────┐│   ResourceEventHandlerFuncs   ││   (注册在 informer 上)        ││  ─ AddFunc / UpdateFunc       ││  ─ DeleteFunc                 │└─────────────┬────────────────┘│ 由 Scheduler 在 addAllEventHandlers│ 中注册,桥接到 SchedulingQueue▼┌──────────────────────────────┐│   SchedulingQueue             ││   (PriorityQueue 三段队列)    ││  ─ Add / AddIfNotPresent      ││  ─ MoveAllToActiveOrBackoffQ  │└─────────────┬────────────────┘│ Pop▼┌──────────────────────────────┐│   ScheduleOne 循环            ││   走 Filter / Score / Bind    │└──────────────────────────────┘

这是 k8s 所有控制面组件的通用范式:从 apiserver 拉数据到本地缓存 → 通过事件回调通知业务 → 业务把事件转化为工作队列。kube-scheduler 也不例外。

4.2 起点:SharedInformerFactory + 多资源 Informer

kube-scheduler 启动时,通过 staging/src/k8s.io/client-go/informers 包下的 SharedInformerFactory 同时启动多个 Informer:

// cmd/kube-scheduler/app/server.go(v1.36.1)

Go
// 用 client-go 提供的 factory 启动所有内置 Informer
cc.InformerFactory.Start(ctx.Done())
if cc.DynInformerFactory != nil {cc.DynInformerFactory.Start(ctx.Done())
}
// 阻塞等待所有缓存完成首次 List 同步
cc.InformerFactory.WaitForCacheSync(ctx.Done())
// 关键:还要等所有注册的 EventHandler 处理完首次 List 的事件
if err := sched.WaitForHandlersSync(ctx); err != nil {logger.Error(err, "handlers are not fully synchronized")
}

在 v1.36.1 中,kube-scheduler 至少需要监听 以下资源类型

资源来源作用
Pod informerFactory.Core().V1().Pods() 新 Pod 入队、已调度的 Pod 重排
Node informerFactory.Core().V1().Nodes() 资源/标签/污点变化,触发 unschedulable Pod 重排
PersistentVolume informerFactory.Core().V1().PersistentVolumes() 新增 PV 后,等 PV 的 Pod 可重新调度
PersistentVolumeClaim informerFactory.Core().V1().PersistentVolumeClaims() PVC Bound 后让依赖 Pod 可调度
CSINode / CSIDriver informerFactory.Storage().V1().CSINodes() VolumeBinding 插件需要感知存储拓扑
StorageClass informerFactory.Storage().V1().StorageClasses() 新建 StorageClass 后影响未绑定 PVC 的 Pod
Service informerFactory.Core().V1().Services() 用于某些 plugin(如 NodeAffinity)做 Service 拓扑感知
PodGroup (v1.36.1) informerFactory.Scheduling().V1alpha2().PodGroups() Gang Scheduling(需开启 GenericWorkload feature gate)

4.3 核心枢纽:pkg/scheduler/eventhandlers.go

所有"事件 → 队列"的翻译工作,都集中在 pkg/scheduler/eventhandlers.go 中。其中 addAllEventHandlers 函数是入口(v1.36.1 行 481):

// pkg/scheduler/eventhandlers.go (行 481-507, k8s v1.36.1)

Go
func addAllEventHandlers(sched *Scheduler,informerFactory informers.SharedInformerFactory,dynInformerFactory dynamicinformer.DynamicSharedInformerFactory,...
) error {// 1. Pod Informer:调度器最关心的事件源informerFactory.Core().V1().Pods().Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{AddFunc:    sched.addPod,    // 新 Pod → 入队UpdateFunc: sched.updatePod, // Pod 更新 → 重新入队或忽略DeleteFunc: sched.deletePod, // Pod 删除 → ForgetPod})// 2. Node Informer:节点变化触发 unschedulable Pod 重排informerFactory.Core().V1().Nodes().Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{AddFunc:    sched.addNodeToCache,UpdateFunc: sched.updateNodeInCache,DeleteFunc: sched.deleteNodeFromCache,},)// 3. 其他资源:CSINode / PV / PVC / StorageClass / Service / PodGroup ...//    通过 buildEvtResHandler 工厂方法动态注册for gvk, at := range gvkMap {switch gvk {case fwk.CSINode:           /* ... */case fwk.PersistentVolume:  /* ... */case fwk.PersistentVolumeClaim: /* ... */case fwk.PodGroup:          /* ... */default:                    /* 用 dynInformerFactory 处理 CRD */}}
}

我们以 addPod / addNodeToCache 为例,看具体怎么"翻译"。先看 addNodeToCache 的完整实现(pkg/scheduler/eventhandlers.go:53):

// pkg/scheduler/eventhandlers.go (行 53-66, k8s v1.36.1)

Go
func (sched *Scheduler) addNodeToCache(obj interface{}) {evt := fwk.ClusterEvent{Resource: fwk.Node, ActionType: fwk.Add}defer metrics.EventHandlingLatency.ObserveSince(time.Now(), evt.Label())()logger := sched.loggernode, ok := obj.(*v1.Node)if !ok {utilruntime.HandleErrorWithLogger(logger, nil, "Cannot convert to *v1.Node", "obj", obj)return}logger.V(3).Info("Add event for node", "node", klog.KObj(node))// 步骤 1:把 Node 加入本地 CachenodeInfo := sched.Cache.AddNode(logger, node)// 步骤 2:把所有 unschedulable Pod 重新移到 activeQ 重新评估sched.SchedulingQueue.MoveAllToActiveOrBackoffQueue(logger, evt, nil, node, preCheckForNode(logger, nodeInfo))
}

一个看似简单的"新增节点"事件,触发了两个动作:

  • 更新 Cachesched.Cache.AddNode() 把 Node 加入本地缓存,下一次 Score 就能看到这个 Node
  • 重排队列MoveAllToActiveOrBackoffQueue() 把所有当前被"判 unschedulable"的 Pod 重新激活,加入下一轮调度

这就是"事件驱动"的核心价值:不需要轮询,集群一有变化就被推过来,业务侧只需把事件翻译成工作队列操作

4.4 Pod 事件:addPod / updatePod / deletePod

Pod 是最复杂的事件源,调度器需要做精细判断(不是所有 Pod Update 都要重排)。典型的 addPod 处理逻辑(v1.36.1)会做:

  1. 判断是否需要调度:spec.nodeName 为空、spec.schedulerName 匹配、Pod 未被删除(deletionTimestamp 为空)
  2. 调用 runPreEnqueuePlugins:每个 profile 可以注册 PreEnqueue plugin 做"快速门禁"(如 SchedulingGates 检查)
  3. 加入 SchedulingQueue.Add(已存在则用 AddIfNotPresent 跳过)

updatePod 更复杂:

  • 如果 Pod 已经被 nominated(被预选为某个 Node)、或新加入了 SchedulingGates、或关键 label 改变——需要重新调度
  • 如果只是 status 字段更新(如容器启动状态变化),调度器不关心,直接 return

这种"事件过滤"非常重要,否则 apiserver 的每一个事件都会冲击调度队列,导致大量无效工作。

4.5 关键:WaitForHandlersSync 是什么?

很多新手会把 WaitForCacheSyncWaitForHandlersSync 混为一谈,其实它们是两件事:

方法在哪等等什么
WaitForCacheSync Informer 内部 Reflector 拉完第一次 List,本地 Indexer 填好
WaitForHandlersSync Scheduler 层 所有已注册 EventHandler 把 List 中的每个对象都处理完(都调用了一次 AddFunc/UpdateFunc)

为什么要等第二层?因为 WaitForCacheSync 完成后,本地缓存虽然填好了,但 List 阶段产生的事件还没被 EventHandler 处理(这些事件是异步分发的)。如果此时 scheduleOne 开始取 Pod,可能错过"先创建后又被删"的 Pod,或漏算某些已被假定但未进 cache 的 Pod。

v1.36.1 的 server.go 专门为它加了 healthz check:

// cmd/kube-scheduler/app/server.go (行 220-227, k8s v1.36.1)

Go
handlerSyncReadyCh := make(chan struct{})
handlerSyncCheck := healthz.NamedCheck("sched-handler-sync", func(_ *http.Request) error {select {case <-handlerSyncReadyCh:return nildefault:}return fmt.Errorf("handlers are not fully synchronized")
})
readyzChecks = append(readyzChecks, handlerSyncCheck)

也就是说,/readyz 返回 200 之前,kube-scheduler 不会真正开始调度。这是生产环境排障的常用入口:如果你看到 kube-scheduler 一直没 Ready,第一反应就是看是不是 handler 还没同步完。

4.6 整体时序图

Text
用户         kubectl         kube-apiserver      SharedInformer       ResourceEventHandler     SchedulingQueue       ScheduleOne│             │                  │                    │                    │                      │                    ││ apply Pod ──┼─────────────────►│                    │                    │                      │                    ││             │                  │ 写入 etcd           │                    │                      │                    ││             │                  │ 触发 Watch 事件 ───►│ 收到 ADD 事件       │                      │                    ││             │                  │                    │ 调用 addPod handler│                      │                    ││             │                  │                    │ ──────────────────►│                      │                    ││             │                  │                    │                    │ Cache.AddPod         │                    ││             │                  │                    │                    │ Queue.Add(pod) ─────►│                    ││             │                  │                    │                    │                      │ Pop() 阻塞          ││             │                  │                    │                    │                      │ ───────────────────►││             │                  │                    │                    │                      │                    │ 取到 Pod│             │                  │                    │                    │                      │                    │ schedulingCycle│             │                  │                    │                    │                      │                    │ Filter/Score│             │                  │                    │                    │                      │                    │ AssumePod│             │                  │                    │                    │                      │                    │ bindingCycle│             │                  │◄───────────────────┼────────────────────┼──────────────────────┼────────────────────│ POST /bindings│             │                  │ 写入 Pod.Spec.NodeName                                  │                    ││             │                  │ ────► Node 端 kubelet 监听到                          │                    ││             │                  │      开始创建容器                                     │                    ││             │                  │                    │                    │                      │                    │ Done(uid)│             │                  │                    │                    │                      │                    │ 循环取下一个│             │                  │                    │                    │                      │                    │

4.7 一个小例子:Node 增加资源后会发生什么?

把上述知识串成一个具体场景:

  1. 运维给 Node worker-1 扩容 32Gi 内存(kubectl edit node 或 ccm 自动调整)
  2. apiserver 把 Node 对象更新到 etcd
  3. kube-scheduler 的 NodeInformer 通过 Watch 收到 Update 事件
  4. updateNodeInCache 被调用:sched.Cache.UpdateNode() 更新本地 cache
  5. 计算 NodeSchedulingPropertiesChange,识别出"capacity 变化"等关键属性
  6. 调用 MoveAllToActiveOrBackoffQueue(),把所有之前因"内存不足"被标 unschedulable 的 Pod 移到 activeQ
  7. scheduleOne 从 activeQ 取出这些 Pod 重新跑调度,可能命中新 Node

整个过程无需重启 scheduler、无需轮询,纯粹由事件驱动。这就是 k8s 控制面的优雅之处。

💡 注意
有些事件会触发"全量重排"(MoveAllToActiveOrBackoffQueue),这在超大集群(>5000 节点)上可能成为性能瓶颈。v1.36.1 引入的 QueueingHint 特性正是为了解决这个问题:插件能告诉队列"只有 X 类 Pod 受影响",避免无差别重排。


五、Roadmap:后续专题预告与学习路径

本文是调度专题的开篇,只建立总览心智模型。后续会按下面顺序逐篇展开,感兴趣的读者可以先标记:

  1. Scheduling Framework 深度解析:逐个讲解 PreFilter/Filter/Score/Reserve/Bind 等扩展点的源码、调用顺序、数据如何在 CycleState 中传递
  2. 内置插件逐个精读:NodeResourcesFit、NodeAffinity、TaintToleration、PodTopologySpread、VolumeBinding、InterPodAffinity 等
  3. 抢占(Preemption)算法剖析:defaultpreemption 插件如何选择 victim、PodDisruptionBudget 如何约束
  4. SchedulingQueue 与 QueueingHint:三段队列的细节、v1.36 新引入的 QueueingHint 工作机制
  5. Scheduler Profile 与多调度器:如何配置多个 profile 实现多租户、Coordinated LeaderElection
  6. 自定义插件开发实战:手写一个 Score 插件并注册到集群

5.1 给初学者的源码阅读建议

如果你想直接啃源码,推荐按下面的顺序读,每一步都建立在上一部的概念上:

  1. cmd/kube-scheduler/app/server.go — 看 Run 函数,理解启动流程(约 100 行)
  2. pkg/scheduler/scheduler.go — 看 Scheduler struct 定义和 Run 方法(约 100 行)
  3. pkg/scheduler/eventhandlers.go — 看 addAllEventHandlers(约 200 行)
  4. pkg/scheduler/schedule_one.go — 看 ScheduleOne / schedulingCycle / bindingCycle(约 400 行)
  5. pkg/scheduler/framework/framework.go — 看 RunPreFilterPlugins / RunFilterPlugins / RunScorePlugins 等 framework 核心方法
  6. pkg/scheduler/framework/plugins/noderesources/fit.go — 看一个具体 plugin 的完整实现,作为范例

读完这些,你就能把本文的"心智模型"对到具体代码上。

5.2 一句话总结

kube-scheduler 是个"事件驱动的两阶段调度器":通过 Informer 监听 Pod/Node/各种资源变化,把事件翻译成 SchedulingQueue 的入队操作;调度循环从队列里取 Pod,先用一系列 Filter 插件过滤,再用 Score 插件打分排名,最后 Bind 插件把结果写回 apiserver。Assume 缓存机制让调度决策和资源占用"瞬时一致",避免并发过载;Scheduling Framework 把所有这些规则抽象成可插拔的扩展点,让调度行为可以灵活定制。

记住这三个关键词:两阶段(Filter → Score)插件化(Framework)事件驱动(Informer → Queue)。下次有人问你 kube-scheduler 怎么工作,就可以用这三点展开。


本文参考与源码链接:
   • cmd/kube-scheduler/ 入口
   • pkg/scheduler/ 核心实现
   • staging/src/k8s.io/kube-scheduler/framework 扩展点定义
   • Kubernetes 官方文档:调度与驱逐
   • Scheduling Framework 官方文档

Kubernetes 调度专题【左扬精讲】—— 初识 kube-scheduler:调度模型、内部架构与事件驱动机制 · 来源:k8s 源码 v1.36.1 深度分析

D:\worker-go\kubernetes-1.36.1\blog-output\kubernetes-1.36.1-kube-scheduler-intro-cnblogs.html
http://www.jsqmd.com/news/1031284/

相关文章:

  • TextIn xParse + Codex 实操:把复杂 PDF 表格解析成 Agent 可用数据
  • 2026长沙回收手表全科普,教你辨别正规门店,卖劳力士欧米茄不亏价 - 名奢变现站
  • 2026 连云港防水补漏TOP5实测:外墙地下室屋顶厨卫漏水,本地靠谱服务商怎么选 - 防水空鼓维修家
  • 租车平台客服哪家响应快?从服务机制到实测体验,神州租车才是真靠谱 - 科技焦点
  • 从单进程到多进程:USDPAA SDK 1.2资源管理架构演进与实战
  • 告别淘汰!3步让你的老Mac免费升级到最新macOS系统
  • USDPAA LPM IPFwd:用户空间高性能IPv4转发实现与优化
  • 广州花都驾培市场深度盘点报告:拨开学车乱象,筛选本地靠谱驾培机构 - GrowthUME
  • 汕头工厂、学校食堂承包与快餐配送,值得关注的本地服务商 - 品牌推荐大师1
  • HoRain云--React 路由
  • ZigBee Light Link (ZLL) 智能照明开发实战:基于NXP JN516x的协议栈解析与工程实践
  • KVM/QEMU虚拟化实战:设备直通与性能调优深度解析
  • 升级:推荐一家广东成型线厂家 - 品牌推广大师
  • 2026广州迪奥回收实测|本地实体上门回收,Dior包包高价变现攻略 - 奢侈品回收评测
  • 卡地亚手表维修保养攻略|2026官方售后网点、400热线及常见问题解答 - 资讯快报
  • 16-1 Lambda表达式
  • 2026年免费去水印小程序避坑实测:这5款小红书图片视频解析工具千万别乱用,内附靠谱榜单 - 互联网科技品牌测评
  • AI编程-Vibe coding(大厂常问问题)
  • OEE设备综合效率分析——半导体FAB的「利润放大镜」完整指南
  • NXP DPA Offloading配置实战:从设备树编译到应用部署全解析
  • 用什么设备涂覆导热硅脂? - 资讯快报
  • 告别启动等待:在Vscode中构建高效Matlab脚本工作流
  • 企业级自动化测试平台:扬帆测试平台分钟级部署与高可用架构实践指南
  • 从入门到精通:利用GPSTest解锁Android手机GNSS定位性能全解析
  • 带着爱马仕、LV、迪奥、香奈儿去回收:石家庄各区奢品回收店横向测评优选榜单 - 名奢变现站
  • 合肥市巢湖市 厨房改造・卫生间翻新|维小达|厨房改造、卫生间翻新、防水整改、水电升级、瓷砖铺贴、适老化改造服务 - 维小达科技
  • 职场人必看的MBA书籍推荐
  • LXC容器技术解析:从命名空间、cgroups到嵌入式网络实战
  • 别墅地下室防水品牌推荐:结构型防水、渗透型防水、负压防水与防水堵漏品牌选择指南 - 资讯快报
  • 2026石家庄回收商家测评排名,禹竞鉴定准、报价高、到账快 - 名奢变现站