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

0 阅读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)
  • Plugins:内置调度插件(in-tree)
    • 分为多个扩展点: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