k8s-scheduler的工作流程源码解析

129 阅读15分钟

项目代码:kubernetes

198fed9f-1f59-4ebc-972e-3615b723e8c0.png

核心职责

kube-scheduler 是 Kubernetes 中负责节点调度的核心组件,其主要职责是:

  • 监听 apiserver 中未调度的 Pod(spec.nodeName 为空)
  • 为每个待调度 Pod 选择最佳的节点
  • 将调度结果通过 apiserver 写入 Pod 的 spec.nodeName 字段

调度流程

第一阶段:预选(Predicates)/ Filter

作用:过滤不符合要求的节点

主要策略(Hard Constraints):

PodFitsResources:资源(CPU、内存、GPU)满足要求
PodFitsHostPorts:端口不冲突(HostNetwork 或 HostPort 场景)
MatchNodeSelector:匹配 NodeSelector 标签选择器
NodeAffinity:节点亲和性匹配
PodAffinity/PodAntiAffinity:Pod 间亲和/反亲和
NoVolumeZoneConflict:卷的区域不冲突(PV/PVC 约束)
NodeUnschedulable:排除不可调度节点

结果:选出符合所有硬约束的节点列表

第二阶段:优选(Priorities)/ Score

作用:对预选通过的节点排序,选出最佳节点

主要策略(Soft Constraints):

LeastRequestedPriority:资源剩余最多的节点(常用)
MostRequestedPriority:资源使用最多的节点(适合高密度部署)
BalancedResourceAllocation:资源分配最均衡的节点
NodeAffinityPriority:节点亲和性权重
PodAffinityPriority:Pod 亲和性权重
ImageLocalityPriority:镜像本地缓存策略(减少下载时间)
TaintTolerationPriority:容忍度权重
SelectorSpreadPriority:Pod 跨节点、跨区域分散部署

结果:为每个节点计算分数,得分最高者获胜

第三阶段:绑定(Bind)

作用:将调度结果写入 apiserver

  • 向 apiserver 发送请求,更新 Pod 的 spec.nodeName 字段
  • 只有绑定成功后,Pod 才会进入 Pending->Running 阶段

关键组件和概念

调度器框架(Scheduler Framework,1.19+)

Kubernetes 1.19 引入了新的调度器框架,更易于扩展:

  • Extenders 调度器扩展(out-of-tree):独立 HTTP 服务,适合复杂业务逻辑或与外部系统联动;
  • Plugins 内置调度插件(in-tree):直接编译进 kube-scheduler,性能最好,适合通用策略,分为多个扩展点:PreFilter、Filter、PostFilter、Score、PreBind、Bind 等;

调度策略配置

  • kube-scheduler 支持通过配置文件(--config)自定义调度策略
  • 可以禁用默认插件,启用自定义插件
  • 支持插件级别的配置(如调整权重)

源码解析

程序入口

kube-scheduler启动入口: cmd/kube-scheduler/scheduler.go,用cobra命令行工具启动。

kube-scheduler功能实现: 具体功能实现位于pkg/scheduler

调度器工作流程

  1. 数据结构

type Scheduler struct {
    // It is expected that changes made via Cache will be observed
    // by NodeLister and Algorithm.
    Cache internalcache.Cache

    Extenders []fwk.Extender

    // NextPod should be a function that blocks until the next pod
    // is available. We don't use a channel for this, because scheduling
    // a pod may take some amount of time and we don't want pods to get
    // stale while they sit in a channel.
    NextPod func(logger klog.Logger) (*framework.QueuedPodInfo, error)

    // FailureHandler is called upon a scheduling failure.
    FailureHandler FailureHandlerFn

    // SchedulePod tries to schedule the given pod to one of the nodes in the node list.
    // Return a struct of ScheduleResult with the name of suggested host on success,
    // otherwise will return a FitError with reasons.
    SchedulePod func(ctx context.Context, fwk framework.Framework, state fwk.CycleState, podInfo *framework.QueuedPodInfo) (ScheduleResult, error)

    // Close this to shut down the scheduler.
    StopEverything <-chan struct{}

    // SchedulingQueue holds pods to be scheduled
    SchedulingQueue internalqueue.SchedulingQueue

    // If possible, indirect operation on APIDispatcher, e.g. through SchedulingQueue, is preferred.
    // Is nil iff SchedulerAsyncAPICalls feature gate is disabled.
    // Adding a call to APIDispatcher should not be done directly by in-tree usages.
    // framework.APICache should be used instead.
    APIDispatcher *apidispatcher.APIDispatcher

    // Profiles are the scheduling profiles.
    Profiles profile.Map

    client clientset.Interface

    nodeInfoSnapshot *internalcache.Snapshot

    percentageOfNodesToScore int32

    nextStartNodeIndex int

    // logger *must* be initialized when creating a Scheduler,
    // otherwise logging functions will access a nil sink and
    // panic.
    logger klog.Logger

    podGroupLister schedulinglisters.PodGroupLister

    // registeredHandlers contains the registrations of all handlers. It's used to check if all handlers have finished syncing before the scheduling cycles start.
    registeredHandlers []cache.ResourceEventHandlerRegistration

    nominatedNodeNameForExpectationEnabled bool
    genericWorkloadEnabled                 bool
    workloadAwarePreemptionEnabled         bool
}

我们重点关注以下Scheduler字段

  • Cache:缓存现在集群调度的状态,会保留集群中所有schedulerd pod/node 的状态和 assumed pod ,防止 pod 被重新调度。
  • SchedulingQueue:保留准备调度的 pod 的队列,这是一个Interface接口类型(能力集),实际实现是PriorityQueue
  1. SetUp 初始化分析

启动入口cmd/kube-scheduler/scheduler.go开始,然后依赖于cmd/kube-scheduler/app/server.go 里 runCommand 运行 SetUp 初始化后的真正启动命令。

// Setup creates a completed config and a scheduler based on the command args and options
func Setup(ctx context.Context, opts *options.Options, outOfTreeRegistryOptions ...Option) (*schedulerserverconfig.CompletedConfig, *scheduler.Scheduler, error) {
    // ... something before ...

    // 1. 返回 Config,包含所有Scheduler运行需要的Context上下文
    c, err := opts.Config(ctx)
    if err != nil {
        return nil, nil, err
    }
    // 2. Get the completed config
    cc := c.Complete()

    outOfTreeRegistry := make(runtime.Registry)
    for _, option := range outOfTreeRegistryOptions {
       if err := option(outOfTreeRegistry); err != nil {
          return nil, nil, err
       }
    }

    recorderFactory := getRecorderFactory(&cc)
    completedProfiles := make([]kubeschedulerconfig.KubeSchedulerProfile, 0)
    // 3. Create the scheduler.
    sched, err := scheduler.New(ctx,
       cc.Client,
       cc.InformerFactory,
       cc.DynInformerFactory,
       recorderFactory,
       scheduler.WithComponentConfigVersion(cc.ComponentConfig.TypeMeta.APIVersion),
       scheduler.WithKubeConfig(cc.KubeConfig),
       scheduler.WithProfiles(cc.ComponentConfig.Profiles...),
       scheduler.WithPercentageOfNodesToScore(cc.ComponentConfig.PercentageOfNodesToScore),
       scheduler.WithFrameworkOutOfTreeRegistry(outOfTreeRegistry),
       scheduler.WithPodMaxBackoffSeconds(cc.ComponentConfig.PodMaxBackoffSeconds),
       scheduler.WithPodInitialBackoffSeconds(cc.ComponentConfig.PodInitialBackoffSeconds),
       scheduler.WithPodMaxInUnschedulablePodsDuration(cc.PodMaxInUnschedulablePodsDuration),
       scheduler.WithExtenders(cc.ComponentConfig.Extenders...),
       scheduler.WithParallelism(cc.ComponentConfig.Parallelism),
       scheduler.WithBuildFrameworkCapturer(func(profile kubeschedulerconfig.KubeSchedulerProfile) {
          // Profiles are processed during Framework instantiation to set default plugins and configurations. Capturing them for logging
          completedProfiles = append(completedProfiles, profile)
       }),
    )
    
    // ... something after

    return &cc, sched, nil
}
Part I:配置初始化阶段 opts.Config(ctx)

返回一个Config,其包含所有Scheduler运行需要的Context上下文

type Config struct {
    // Flagz is the Reader interface to get flags for flagz page.
    Flagz flagz.Reader

    // ComponentConfig is the scheduler server's configuration object.
    ComponentConfig kubeschedulerconfig.KubeSchedulerConfiguration

    Authentication apiserver.AuthenticationInfo
    Authorization  apiserver.AuthorizationInfo
    SecureServing  *apiserver.SecureServingInfo

    Client             clientset.Interface
    KubeConfig         *restclient.Config
    InformerFactory    informers.SharedInformerFactory
    DynInformerFactory dynamicinformer.DynamicSharedInformerFactory

    //nolint:staticcheck // SA1019 this deprecated field still needs to be used for now. It will be removed once the migration is done.
    EventBroadcaster events.EventBroadcasterAdapter

    // LeaderElection is optional.
    LeaderElection *leaderelection.LeaderElectionConfig

    // PodMaxInUnschedulablePodsDuration is the maximum time a pod can stay in
    // unschedulablePods. If a pod stays in unschedulablePods for longer than this
    // value, the pod will be moved from unschedulablePods to backoffQ or activeQ.
    // If this value is empty, the default value (5min) will be used.
    PodMaxInUnschedulablePodsDuration time.Duration

    // ComponentGlobalsRegistry is the registry where the effective versions and feature gates for all components are stored.
    ComponentGlobalsRegistry basecompatibility.ComponentGlobalsRegistry
}
  • Client:查询 Pod/Node、创建 Binding 等调度核心操作
  • EventBroadcaster:本质是一个EventClient,会构造Event事件,发送给K8s API Server的 /events接口,然后持久化到etcd。后续可以供运维人员直接通过kubectl get events查看或供其他系统(如监控工具、日志系统)收集和分析。
c.EventBroadcaster = events.NewEventBroadcasterAdapterWithContext(ctx, eventClient)
  • InformerFactory:实际是PodInformer
func NewInformerFactory(cs clientset.Interface, resyncPeriod time.Duration) informers.SharedInformerFactory {
    informerFactory := informers.NewSharedInformerFactory(cs, resyncPeriod)
    informerFactory.InformerFor(&v1.Pod{}, newPodInformer)
    return informerFactory
}
  • DynInformerFactory:动态资源的 Informer 工厂
Part II:配置完成阶段 c.Complete()
  1. 语义标记:表示配置已经完成,不应该再被修改
  2. 类型封装(config.go:77-82):
func (c *Config) Complete() CompletedConfig {
    cc := completedConfig{c}
    return CompletedConfig{&cc}
}

把可变的 Config 包装成 CompletedConfig,通过私有结构体 completedConfig 防止外部直接修改

只是一种设计模式,用来区分"配置中"和"配置完成"两个阶段,确保配置在使用时不会被意外修改。

Part III:调度器实例化 scheduler.New(...)
// New returns a Scheduler
func New(ctx context.Context,
    client clientset.Interface,
    informerFactory informers.SharedInformerFactory,
    dynInformerFactory dynamicinformer.DynamicSharedInformerFactory,
    recorderFactory profile.RecorderFactory,
    opts ...Option) (*Scheduler, error) {

    // ...
}
  • 初始化调度器需要的组件
1. Cache (schedulerCache)
功能:维护调度器的Node和 Pod 信息的缓存
依赖:
通过 Informer 机制从 apiserver 同步节点和 pod 信息
依赖节点状态、资源信息、调度约束等
用途:快速查询和更新节点的可调度性状态,避免频繁访问 apiserver
核心作用:调度过程中快速获取节点资源使用情况

2. Client
功能:与 Kubernetes API Server 通信的客户端
依赖:KubeConfig 配置
用途:
获取调度所需的资源信息
更新 Pod 的调度状态
与其他 Kubernetes 组件通信
核心作用:调度器与 apiserver 的通信接口


3. SchedulingQueue (podQueue)
功能:调度队列,管理待调度的 Pod
依赖:
PodLister(用于获取待调度 Pod)
Informer 机制同步 pod 信息
用途:
管理待调度 Pod 的优先级和调度顺序
实现调度队列的入队和出队策略
处理调度失败的 Pod 重试
核心作用:调度工作流程的入口

8. Profiles
功能:调度器配置文件
依赖:框架插件配置
用途:
配置调度器的插件链
定义调度器的行为和策略
支持多调度器配置
核心作用:调度器的配置中心

// else 。。。
  • 注册了各种EventHandler事件处理器
  1. 将调度过有NodeName标记的Assigned Pod执行addAssignedPodToCache送入SchedulerCache更新记录(先remove再add一次)
  2. 将没调度过但指定当前调度器的Responsible Pod,执行addPodToSchedulingQueue送入SchedulingQueue(底层有activeQ,backoffQ)
  3. 用于提供调度器调度进程来不断从activeQ里Pop Pod来进行调度。

事件处理器和调度器的协同流程图

Mermaid (1).jpg

1. Pod 事件处理器 (addPod, updatePod, deletePod)

功能:处理 Pod 的添加、更新和删除事件

处理逻辑:

  • unAssigned Pod(无 nodeName):加入SchedulingQueue
  • Assigned Pod(有 nodeName):更新到SchedulerCache
  • Binding 绑定过程中的 Pod:从队列中移除

关键依赖:

  • 依赖调度队列(SchedulingQueue)管理待调度 Pod
  • 依赖调度器缓存(Cache)跟踪已调度 Pod

2. Node 事件处理器 (addNodeToCache, updateNodeInCache****, deleteNodeFromCache****)

功能:处理节点的添加、更新和删除事件

处理逻辑:

  • 添加节点:更新缓存,唤醒待调度 Pod 重新尝试调度
  • 更新节点:根据节点属性变化(如资源、标签、污点),选择性唤醒 Pod
  • 删除节点:从缓存移除,清理相关状态

关键依赖:

  • 依赖节点信息(资源、标签、污点、亲和性等)
  • 依赖 NodeSchedulingPropertiesChange 判断哪些变化会影响调度

3. 其他资源事件处理器(通过 buildEvtResHandler 动态构建)

这些资源变化时,会唤醒待调度 Pod:

资源类型作用相关调度插件
CSINodeCSI 节点信息变化Volume 相关插件
CSIDriverCSI 驱动变化Volume 相关插件
CSIStorageCapacity存储容量变化Volume 相关插件
PersistentVolumePV 变化Volume 绑定、计数
PersistentVolumeClaimPVC 变化Volume 绑定
StorageClass存储类变化Volume 绑定
VolumeAttachment卷挂载变化Volume 相关插件
ResourceClaim资源申领变化DRA 动态资源分配
ResourceSlice资源切片变化DRA 动态资源分配
DeviceClass设备类变化DRA 动态资源分配
PodGroupPod 组变化Gang 调度

这部分资源事件处理器主要都是做以下的工作:

  1. 事件捕获 → Informer 发现变化
  2. 事件分类 → 确定是什么类型的变化
  3. 遍历不可调度 Pod → 看看谁可能受益
  4. QueueingHint 判断 → 这个变化能帮到这个 Pod 吗?
  5. 移动队列 → 从 unschedulablePods → ActiveQ/BackoffQ
  6. 唤醒调度器 → broadcast() 让调度器立即处理

4. 关键机制:MoveAllToActiveOrBackoffQueue

sched.SchedulingQueue.MoveAllToActiveOrBackoffQueue(logger, evt, oldNode, newNode, preCheck)

作用:处理有可能让unscheduled pod加入调度周期的事件变化,将 unschedulableQ(不可调度队列)中的 Pod 移动到 activeQ(活跃队列)或 backoffQ(退避队列),让它们有机会重新调度。

触发时机:

MoveAllToActiveOrBackoffQueue 触发时机
├─ 1. 节点事件
│  ├─ Node Add
│  ├─ Node Update
│  └─ Node Delete
├─ 2. Pod 事件
│  ├─ Assigned Pod Add ()
│  ├─ Assigned Pod Update
│  ├─ Assigned Pod Delete
│  ├─ Assumed Pod Delete
│  ├─ Unscheduled Pod (nominatedNodeName changed)
│  ├─ Unscheduled Pod Delete (with nominatedNodeName)
│  └─ New Pod + GangScheduling enabled (特例)
└─ 3. 其他资源事件
   ├─ Storage (PV/PVC/StorageClass/CSI)
   ├─ DRA (ResourceClaim/ResourceSlice/DeviceClass)
   ├─ PodGroup
   └─ 自定义资源

思考:新版调度器在事件处理器中如何处理Gang Scheduling Pod

关注下Pod Informer里的addPod方法

  • 非Gang Scheduling Pod
addPodToSchedulingQueue(pod)
    ↓
sched.Cache.AddPodGroupMember(pod)  // (即使没有 Gang 也调用,做个记录)
    ↓
sched.SchedulingQueue.Add(pod)  // ← 关键在这里!
    ├─ 创建 pInfo
    ├─ moveToActiveQ(pInfo)  → 直接加入 ActiveQ
    └─ activeQ.broadcast()    → 立即唤醒调度器处理
    ↓
结束!(不需要 MoveAllToActiveOrBackoffQueue)
  • Gang Scheduling Pod
addPodToSchedulingQueue(pod)
    ↓
sched.Cache.AddPodGroupMember(pod)  // ← 记录到 PodGroup
    ↓
sched.SchedulingQueue.Add(pod)       // ← 当前 Pod 加入 ActiveQ
    ├─ moveToActiveQ(pInfo)
    └─ activeQ.broadcast()
    ↓
MoveAllToActiveOrBackoffQueue(...)    // ← 额外这一步!
    └─ 遍历 unschedulablePods,找到同一个 PodGroup 的其他成员
       └─ 让它们也有机会重新调度

思考:调度器主进程启动后如何感知到有变化要去进行调度

调度器启动后等待,直到有SchedulingQueue的activeQ里有要调度的pod

调度器主循环
    ↓
scheduleOne()
    ↓
SchedulingQueue.Pop()
    ↓
activeQueue.pop()
    ├─ 检查 ActiveQ 是否为空
    └─ aq.cond.Wait()  // 阻塞!调度器睡眠

... 时间流逝 ...

Pod 创建事件
    ↓
eventHandler 处理
    ↓
addPodToSchedulingQueue()
    ↓
SchedulingQueue.Add()
    ├─ moveToActiveQ()  // 加入 ActiveQ
    └─ activeQ.broadcast()  // 唤醒调度器!

调度器主循环
    ↓
被唤醒,继续执行 pop()
    ├─ 从 ActiveQ 取出 Pod
    └─ scheduleOne() 继续处理

Run 分析

启动入口cmd/kube-scheduler/scheduler.go开始,然后依赖于cmd/kube-scheduler/app/server.go 里 runCommand 运行 SetUp 初始化后的真正启动命令。

当前文SetUp初始化调度器后,开始走到Run启动全流程。

这部分的代码我们分以下几部分拆解

Part I:常规初始化
  • 日志配置
  • EventBroadCaster事件处理器启动
  • 健康检查设置
Kubernetes kubelet 会定期调用这些端点来监控调度器状态
如果 /readyz 失败,调度器不会接收新的调度请求
如果 /livez 失败,kubelet 可能会重启调度器
  • 选主LeaderElection准备
  • Informer启动和同步
  • 完成选主LeaderElection
Part II:启动调度器主调度进程

执行sched.Run(ctx),跳转到pkg调度器功能实现的Run函数,pkg/scheduler/scheduler.go:Run()

func (sched *Scheduler) Run(ctx context.Context) {
    logger := klog.FromContext(ctx)
    sched.SchedulingQueue.Run(logger)

    if sched.APIDispatcher != nil {
       sched.APIDispatcher.Run(logger)
    }

    // We need to start scheduleOne loop in a dedicated goroutine,
    // because scheduleOne function hangs on getting the next item
    // from the SchedulingQueue.
    // If there are no new pods to schedule, it will be hanging there
    // and if done in this goroutine it will be blocking closing
    // SchedulingQueue, in effect causing a deadlock on shutdown.
    go wait.UntilWithContext(ctx, sched.ScheduleOne, 0)

    <-ctx.Done()
    if sched.APIDispatcher != nil {
       sched.APIDispatcher.Close()
    }
    sched.SchedulingQueue.Close()

    // If the plugins satisfy the io.Closer interface, they are closed.
    err := sched.Profiles.Close()
    if err != nil {
       logger.Error(err, "Failed to close plugins")
    }
}

这部分我们三个部分来分析

  • sched.SchedulingQueue.Run(logger):为调度工作准备好数据源,
  • go wait.UntilWithContext(ctx, sched.ScheduleOne, 0):精巧实现避免主goroutine被阻塞Hang住无法优雅关闭
  • SchedulerOne:展示所谓的“调度”,具体是在干什么。
sched.SchedulingQueue.Run(logger)
func (p *PriorityQueue) Run(logger klog.Logger) {
    // 1. 处理 backoffQ 的 Pod
    go p.backoffQ.waitUntilAlignedWithOrderingWindow(func() {
        p.flushBackoffQCompleted(logger)
    }, p.stop)
    
    // 2. 定期清理不可调度 Pod
    go wait.Until(func() {
        p.flushUnschedulablePodsLeftover(logger)
    }, 30*time.Second, p.stop)
}

启动两个后台 goroutine 构成了SchedulingQueue的数据源

  • 一个处理backoffQ的Pod推到activeQ
  • 一个每间隔30s处理unscheduledPods中等待5分钟以上的Pod再次推入activeQ
go wait.UntilWithContext(ctx, sched.ScheduleOne, 0)

这里的参数:

  • ctx:共享的上下文,用于取消
  • sched.ScheduleOne:要循环执行的函数(调度一个 Pod)
  • 0:没有间隔,执行完一个立即执行下一个
  1. 原理分析
func (sched *Scheduler) Run(ctx context.Context) {
    logger := klog.FromContext(ctx)
    sched.SchedulingQueue.Run(logger)

    if sched.APIDispatcher != nil {
        sched.APIDispatcher.Run(logger)
    }

    // ✅ 关键:在独立的 goroutine 中运行调度循环go wait.UntilWithContext(ctx, sched.ScheduleOne, 0)

    // 主 goroutine 在这里等待关闭信号
    <-ctx.Done()
    
    // 然后执行优雅关闭if sched.APIDispatcher != nil {
        sched.APIDispatcher.Close()
    }
    sched.SchedulingQueue.Close()
    
    err := sched.Profiles.Close()
    if err != nil {
        logger.Error(err, "Failed to close plugins")
    }
}

如果不使用 goroutine,直接在主 goroutine 中运行调度循环:

// ❌ 错误的做法func (sched *Scheduler) Run(ctx context.Context) {
    sched.SchedulingQueue.Run(logger)
    // 直接在这里运行,会阻塞!
    wait.UntilWithContext(ctx, sched.ScheduleOne, 0)  // ← 会一直等在这里!// 下面的关闭代码永远不会执行!
    sched.SchedulingQueue.Close()
}

问题:

  • ScheduleOne 会在 SchedulingQueue.Pop() 处阻塞等待新 Pod
  • 如果没有新 Pod,会一直 hang 在那里
  • 当需要关闭时,无法执行到关闭代码
  1. 完整工作流程
─────────────────────────────────────────────────────────────────
阶段 1:正常运行
─────────────────────────────────────────────────────────────────

┌─────────────────────────────────────────────────────────────┐
│ 主 goroutine (Run 函数)                              
│  - 启动调度队列                                      
│  - 启动 API 调度器 (如果有)                            
│  - 等待 <-ctx.Done()                               
│  - 不会阻塞!                                       
└─────────────────────────────────────────────────────────────┘
                           │
                           ▼
┌─────────────────────────────────────────────────────────────┐
│ 调度 goroutine (独立启动)                            
│  wait.UntilWithContext(ctx, ScheduleOne, 0)             
│  ┌───────────────────────────────────────────────────┐  │
│  │ 循环:                                    
│  │  ├─ ScheduleOne()                           
│  │  ├─ Pop() → aq.cond.Wait() ← 可能阻塞等待新 Pod   
│  │  └─ 调度成功,继续下一个                       
│  └───────────────────────────────────────────────────┘  │
│ 同时监听 ctx.Done(),准备随时退出                
└─────────────────────────────────────────────────────────────┘


─────────────────────────────────────────────────────────────────
阶段 2:关闭触发
─────────────────────────────────────────────────────────────────

触发方式:
  1. Pod 被 kill → SIGTERM 信号
  2. 失去领导者选举
  3. 手动停止
  
     ↓
ctx.Done() 被触发!


─────────────────────────────────────────────────────────────────
阶段 3:同时响应关闭
─────────────────────────────────────────────────────────────────

两个 goroutine 同时收到信号:

┌─────────────────────────────────────────────────────────────┐
│ 主 goroutine                                       
│  ← ctx.Done() 解除阻塞                            
│  开始执行优雅关闭:                                
│  ├─ sched.APIDispatcher.Close()                    
│  ├─ sched.SchedulingQueue.Close()                 
│  └─ sched.Profiles.Close()                         
└─────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│ 调度 goroutine                                      
│  wait.UntilWithContext 检测到 ctx 取消:              
│  ├─ 停止循环                                       
│  └─ goroutine 结束                                  
└─────────────────────────────────────────────────────────────┘


─────────────────────────────────────────────────────────────────
阶段 4:完成关闭
─────────────────────────────────────────────────────────────────

所有资源正确关闭,程序安全退出!
  1. 关键点总结

职责分离:

  • 主 goroutine:管理生命周期、等待关闭、执行优雅关闭
  • 调度 goroutine:执行调度循环、可以安全地阻塞

避免死锁:

  • 调度 goroutine 在 Pop() 时阻塞不会影响主 goroutine
  • 主 goroutine 可以自由执行关闭代码

context 的作用:

  • 两个 goroutine 共享同一个 context
  • 当 context 取消时,两个 goroutine 都会收到通知
  • 调度 goroutine 中的 wait.UntilWithContext 会检测到 context 取消并退出

这就是经典的 Go 语言并发模式:一个 goroutine 处理主要业务逻辑,另一个 goroutine 管理生命周期。

说到这里,情不自禁的想到MIT 6.824 分布式课程第二节课Threads And RPC里的经典Vote案例,介绍了条件变量和Chanel两种Coordination方式解决Threads Challengs,贴个代码大家有兴趣可以看看。

// 条件变量例子
package main

import "sync"
import "time"
import "math/rand"

func main() {
    rand.Seed(time.Now().UnixNano())

    count := 0
    finished := 0
    var mu sync.Mutex
    cond := sync.NewCond(&mu)

    for i := 0; i < 10; i++ {
       go func() {
          vote := requestVote()
          mu.Lock()
          defer mu.Unlock()
          if vote {
             count++
          }
          finished++
          //// 等待一段时间再检查,避免 CPU 一直忙等待
          //time.Sleep(50 * time.Millisecond)

          // 优化Sleep,不需要人为猜想or设定sleep多久
          cond.Broadcast() // 自动释放锁,让子goroutine继续执行
       }()
    }

    mu.Lock()
    for count < 5 && finished != 10 {
       cond.Wait()
    }
    if count >= 5 {
       println("received 5+ votes!")
    } else {
       println("lost")
    }
    mu.Unlock()
}

// 通道例子
package main

import "time"
import "math/rand"

func main() {
    rand.Seed(time.Now().UnixNano())

    count := 0
    finished := 0
    ch := make(chan bool)

    for i := 0; i < 10; i++ {
       go func() {
          ch <- requestVote()
       }()
    }

    for count < 5 && finished != 10 {
       vote := <-ch // 从 channel 读取(如果没有数据会自动阻塞,不占用 CPU)
       if vote {
          count++
       }
       finished++
    }
    // TODO: 例如前5个vote true后,ch没有接受者,剩余子goroutine继续试图写入会被blocked(此时无人read from channel)

    if count >= 5 {
       println("received 5+ votes!")
    } else {
       println("lost")
    }

}

SchedulerOne

根据K8s Scheduling FrameWork展开,分为Scheduling Cycle(Filter & Score)和Binding Cycle。这里整理一个工作流程,具体的内容在K8s Scheduling FrameWork里展开

ScheduleOne (主循环)
    │
    ├─ NextPod() ← 从队列获取 Pod
    │
    ├─ 判断是否是 PodGroup
    │   ├─ 是 → scheduleOnePodGroup()
    │   └─ 否 → scheduleOnePod()
    │
    └─ scheduleOnePod()
        │
        ├─ frameworkForPod() ← 获取调度框架
        │
        ├─ skipPodSchedule() ← 检查是否跳过
        │
        ├─ schedulingCycle() (同步)
        │   │
        │   ├─ UpdateSnapshot() ← 更新节点快照
        │   │
        │   ├─ schedulingAlgorithm()
        │   │   │
        │   │   ├─ SchedulePod() ← 过滤 + 评分
        │   │   │
        │   │   └─ 失败 → RunPostFilterPlugins() ← 抢占
        │   │
        │   └─ prepareForBindingCycle()
        │       │
        │       ├─ assumeAndReserve() ← 假设绑定 + 预留资源
        │       │
        │       └─ RunPermitPlugins() ← 许可插件
        │
        └─ 成功 → runBindingCycle() (异步 goroutine)
            │
            └─ bindingCycle() ← 实际绑定到 APIServer

思考:调度器主进程启动后,何时触发关闭呢?

调度器关闭依赖捕获退出信号,具体关闭有很多场景,比较常见的是

  • 手动 kill pod
  • 选主场景下失去Leader地位
  • 健康检查失败触发Pod重启

接下来,我们来回头看看这三个场景的退出信号捕获定义在何处。

  1. 手动 Pod Kill
flowchart LR
    A[Kubernetes 删除调度器 Pod] --> B[kubelet 发送 SIGTERM 信号给进程]
    B --> C[SetupSignalHandler 捕获信号]
    C --> D[stopCh 关闭]
    D --> E[cancel 被调用]
    E --> F[ctx.Done]

    style A fill:#fff,color:#000,stroke:#333,stroke-width:1px
    style B fill:#fff,color:#000,stroke:#333,stroke-width:1px
    style C fill:#fff,color:#000,stroke:#333,stroke-width:1px
    style D fill:#fff,color:#000,stroke:#333,stroke-width:1px
    style E fill:#fff,color:#000,stroke:#333,stroke-width:1px
    style F fill:#fff,color:#000,stroke:#333,stroke-width:1px
// runCommand runs the scheduler.
func runCommand(cmd *cobra.Command, opts *options.Options, registryOptions ...Option) error {
    // ...
    // 退出信号捕获
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()
    go func() {
       stopCh := server.SetupSignalHandler()
       <-stopCh
       cancel()
    }()

    // SetUp 初始化
    
    // Run 启动
    return Run(ctx, cc, sched)
}


// 上门调用的SetupSignalHandler具体实现如下,真优雅啊
func SetupSignalHandler() <-chan struct{} {
    return SetupSignalContext().Done()
}

func SetupSignalContext() context.Context {
    close(onlyOneSignalHandler)
    
    shutdownHandler = make(chan os.Signal, 2)
    ctx, cancel := context.WithCancel(context.Background())
    signal.Notify(shutdownHandler, shutdownSignals...)  // 监听信号
    go func() {
        <-shutdownHandler  // 第一次信号:优雅关闭
        cancel()
        <-shutdownHandler  // 第二次信号:强制退出
        os.Exit(1)
    }()
    return ctx
}
  1. 失去Leader地位

回调 OnStoppedLeading 函数

flowchart LR
    A[调度器失去主地位] --> B[OnStoppedLeading 被调用]
    B --> C[os.Exit 1 退出]

    style A fill:#fff,color:#000,stroke:#333,stroke-width:1px
    style B fill:#fff,color:#000,stroke:#333,stroke-width:1px
    style C fill:#fff,color:#000,stroke:#333,stroke-width:1px
func Run(ctx context.Context, cc *schedulerserverconfig.CompletedConfig, sched *scheduler.Scheduler) error {
    // ... something before
    
    // If leader election is enabled, runCommand via LeaderElector until done and exit.
    if cc.LeaderElection != nil {
       if utilfeature.DefaultFeatureGate.Enabled(kubefeatures.CoordinatedLeaderElection) {
          cc.LeaderElection.Coordinated = true
       }
       cc.LeaderElection.Callbacks = leaderelection.LeaderCallbacks{
          OnStartedLeading: func(ctx context.Context) {
             close(waitingForLeader)
             if cc.ComponentConfig.DelayCacheUntilActive {
                logger.Info("Starting informers and waiting for sync...")
                startInformersAndWaitForSync(ctx)
                logger.Info("Sync completed")
             }
             sched.Run(ctx)
          },
          // 
          OnStoppedLeading: func() {
             gracefulShutdownSecureServer()
             select {
             case <-ctx.Done():
                // We were asked to terminate. Exit 0.
                logger.Info("Requested to terminate, exiting")
                os.Exit(0)
             default:
                // We lost the lock.
                logger.Error(nil, "Leaderelection lost")
                klog.FlushAndExit(klog.ExitFlushTimeout, 1)
             }
          },
       }
       leaderElector, err := leaderelection.NewLeaderElector(*cc.LeaderElection)
       if err != nil {
          return fmt.Errorf("couldn't create leader elector: %v", err)
       }

       leaderElector.Run(ctx)

       return fmt.Errorf("lost lease")
    }

    // Leader election is disabled, so runCommand inline until done.
    close(waitingForLeader)
    sched.Run(ctx)
    gracefulShutdownSecureServer()
    return fmt.Errorf("finished without leader elect")
}

  1. 健康检查失败,kubelet出手重启pod

在cmd运行框架侧去调用pkg实现真正调度流程前,我们已经定义各种健康检查,例如下面

端点用途对应探针类型失败结果
/healthz传统健康检查(已弃用)--
/livez存活检查livenessProbekubelet 会重启容器
/readyz就绪检查readinessProbe从 Service 停止发流量

在Yaml部署指定健康检查端点后,kubelet将会接管定期check