项目代码:kubernetes
核心职责
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
调度器工作流程
分析完细节后总结流程图和概要说明在这里
-
数据结构
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
-
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()
- 语义标记:表示配置已经完成,不应该再被修改
- 类型封装(
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事件处理器
- 将调度过有NodeName标记的Assigned Pod执行addAssignedPodToCache送入SchedulerCache更新记录(先remove再add一次)
- 将没调度过但指定当前调度器的Responsible Pod,执行addPodToSchedulingQueue送入SchedulingQueue(底层有activeQ,backoffQ)
- 用于提供调度器调度进程来不断从activeQ里Pop Pod来进行调度。
事件处理器和调度器的协同流程图
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:
| 资源类型 | 作用 | 相关调度插件 |
|---|---|---|
| CSINode | CSI 节点信息变化 | Volume 相关插件 |
| CSIDriver | CSI 驱动变化 | Volume 相关插件 |
| CSIStorageCapacity | 存储容量变化 | Volume 相关插件 |
| PersistentVolume | PV 变化 | Volume 绑定、计数 |
| PersistentVolumeClaim | PVC 变化 | Volume 绑定 |
| StorageClass | 存储类变化 | Volume 绑定 |
| VolumeAttachment | 卷挂载变化 | Volume 相关插件 |
| ResourceClaim | 资源申领变化 | DRA 动态资源分配 |
| ResourceSlice | 资源切片变化 | DRA 动态资源分配 |
| DeviceClass | 设备类变化 | DRA 动态资源分配 |
| PodGroup | Pod 组变化 | Gang 调度 |
这部分资源事件处理器主要都是做以下的工作:
- 事件捕获 → Informer 发现变化
- 事件分类 → 确定是什么类型的变化
- 遍历不可调度 Pod → 看看谁可能受益
- QueueingHint 判断 → 这个变化能帮到这个 Pod 吗?
- 移动队列 → 从 unschedulablePods → ActiveQ/BackoffQ
- 唤醒调度器 → 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:没有间隔,执行完一个立即执行下一个
- 原理分析
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:正常运行
─────────────────────────────────────────────────────────────────
┌─────────────────────────────────────────────────────────────┐
│ 主 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:完成关闭
─────────────────────────────────────────────────────────────────
所有资源正确关闭,程序安全退出!
- 关键点总结
职责分离:
- 主 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重启
接下来,我们来回头看看这三个场景的退出信号捕获定义在何处。
- 手动 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
}
- 失去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")
}
- 健康检查失败,kubelet出手重启pod
在cmd运行框架侧去调用pkg实现真正调度流程前,我们已经定义各种健康检查,例如下面
| 端点 | 用途 | 对应探针类型 | 失败结果 |
|---|---|---|---|
| /healthz | 传统健康检查(已弃用) | - | - |
| /livez | 存活检查 | livenessProbe | kubelet 会重启容器 |
| /readyz | 就绪检查 | readinessProbe | 从 Service 停止发流量 |
在Yaml部署指定健康检查端点后,kubelet将会接管定期check