Part 5: kube-scheduler API类型、配置、工具与Binder超深度分析


一、模块定位

1.1 总体架构定位

kube-scheduler的API类型体系、配置系统、Binder插件和QueueSort插件,共同构成了调度器的"神经中枢"——它们定义了调度器是谁(配置身份)、如何行为(插件编排)、如何执行(绑定动作)和如何排队(优先级排序)四大核心问题。

模块 源码路径 业务职责
API类型(内部版本) pkg/scheduler/apis/config/types.go + types_pluginargs.go + legacy_types.go 定义调度器配置的权威内部类型,是所有版本转换的锚点
API类型(v1版本) staging/src/k8s.io/kube-scheduler/config/v1/types.go 旧版Policy策略的稳定API版本,用于向后兼容
API类型(v1beta1版本) staging/src/k8s.io/kube-scheduler/config/v1beta1/types.go + types_pluginargs.go ComponentConfig的当前推荐版本,支持Profiles+Plugins范式
配置验证 pkg/scheduler/apis/config/validation/validation.go + validation_pluginargs.go 对配置进行全量校验,防止非法配置进入运行时
配置Scheme pkg/scheduler/apis/config/scheme/scheme.go 统一注册所有版本类型,提供编解码能力
版本转换 pkg/scheduler/apis/config/v1/conversion.go + v1beta1/conversion.go 处理v1/v1beta1→内部版本的类型转换与PluginConfig Args的编解码
默认值 pkg/scheduler/apis/config/v1beta1/defaults.go 为v1beta1配置填充默认值(并行度16、端口10251、QPS 50等)
DefaultBinder pkg/scheduler/framework/plugins/defaultbinder/default_binder.go 框架默认的Bind插件,通过API Server执行Pod→Node绑定
PrioritySort(QueueSort) pkg/scheduler/framework/plugins/queuesort/priority_sort.go 调度队列排序插件,基于Pod优先级+时间戳的堆排序
工具函数 pkg/scheduler/util/utils.go + non_zero.go + clock.go Pod全名生成、优先级比较、资源请求计算、状态补丁等通用工具
Metrics pkg/scheduler/metrics/metrics.go + metric_recorder.go + profile_metrics.go + resources/resources.go Prometheus指标体系——调度延迟、尝试次数、队列深度、插件耗时等
Extender API staging/src/k8s.io/kube-scheduler/extender/v1/types.go 调度器扩展器的请求/响应类型定义

1.2 核心设计哲学

  1. 内部版本(Internal Version)为真理源:所有外部版本(v1、v1beta1)的配置最终都转换到内部版本,内部版本是运行时的唯一工作形态。
  2. Profiles+Plugins范式:抛弃旧版Policy的"谓词+优先级函数"硬编码模型,转向"调度Profile + 扩展点插件列表"的声明式模型,一个调度器可同时服务多个调度Profile。
  3. PluginConfig Args的嵌套编解码:PluginConfig.Args在v1beta1中以runtime.RawExtension存储(原始JSON/YAML字节),解码时按插件名+Args的GVK进行反序列化,对未知的外部插件优雅跳过。
  4. QueueSort全局一致性:所有Profile必须使用相同的QueueSort插件和配置,因为调度队列是全局共享的。
  5. 默认值与验证分离:defaults.go负责填充零值默认,validation.go负责检查语义合法性,两者互补。

二、模块整体结构

2.1 KubeSchedulerConfiguration完整字段详解

// 内部版本(types.go)
type KubeSchedulerConfiguration struct {
    metav1.TypeMeta

    Parallelism                 int32                                  // 调度算法并行度,必须>0,默认16
    AlgorithmSource             SchedulerAlgorithmSource               // [已废弃] 旧版算法源,仅内部版本保留
    LeaderElection              componentbaseconfig.LeaderElectionConfiguration  // 领导选举配置
    ClientConnection            componentbaseconfig.ClientConnectionConfiguration // API Server连接配置
    HealthzBindAddress          string                                 // 健康检查地址,默认0.0.0.0:10251
    MetricsBindAddress          string                                 // 指标服务地址,默认0.0.0.0:10251
    DebuggingConfiguration      componentbaseconfig.DebuggingConfiguration  // 调试配置(内联)
    PercentageOfNodesToScore    int32                                  // 评分节点百分比,0=自适应,范围[0,100]
    PodInitialBackoffSeconds    int64                                  // 不可调度Pod初始退避秒数,默认1
    PodMaxBackoffSeconds        int64                                  // 不可调度Pod最大退避秒数,默认10
    Profiles                    []KubeSchedulerProfile                 // 调度Profile列表
    extenders                   []Extender                             // 调度扩展器列表(跨Profile共享)
}

逐字段深入分析:

Parallelism(并行度)
  • 控制调度算法中同时评估的Pod数量
  • v1beta1中使用*int32(指针),允许区分"未设置"和"设为0"
  • 默认值16,表示最多16个goroutine并发执行调度算法
  • 验证规则:> 0,否则报field.Invalid
AlgorithmSource(算法源)——已废弃
  • 旧版调度器支持三种算法来源:Provider名称、Policy文件、Policy ConfigMap
  • 在v1beta1中已移除此字段,仅内部版本保留用于向后兼容转换
  • 转换时自动设置out.AlgorithmSource.Provider = pointer.StringPtr("DefaultProvider")
LeaderElection(领导选举)
type LeaderElectionConfiguration struct {
    LeaderElect       bool    // 是否启用领导选举,多实例部署时必须启用
    ResourceLock      string  // 资源锁类型,默认"leases"(1.20+从EndpointsLease迁移)
    ResourceName      string  // 锁对象名称,默认"kube-scheduler"
    ResourceNamespace string  // 锁对象命名空间,默认"kube-system"
    LeaseDuration     metav1.Duration  // 选举租约时长
    RenewDeadline     metav1.Duration  // 续约截止时间
    RetryPeriod       metav1.Duration  // 重试间隔
}
ClientConnection(客户端连接)
type ClientConnectionConfiguration struct {
    ContentType string   // 请求内容类型,默认"application/vnd.kubernetes.protobuf"
    QPS         float32  // QPS限制,默认50(覆盖component-base的通用默认值)
    Burst       int32    // 突发限制,默认100
}
  • Scheduler对QPS/Burst有自己的见解,不使用component-base的通用默认值(QPS=5, Burst=10)
PercentageOfNodesToScore(评分节点百分比)
  • 值为0时:自适应模式,根据集群规模计算5%~50%的动态百分比
  • 值为30且集群500节点时:找到150个可行节点后停止搜索
  • 底层保证minFeasibleNodesToFind:无论百分比多低,至少搜索足够节点
PodInitialBackoffSeconds / PodMaxBackoffSeconds
  • 不可调度Pod的退避重试机制:backoff = min(PodMaxBackoffSeconds, PodInitialBackoffSeconds * 2^n)
  • 初始1秒,最大10秒,指数退避
  • 验证:PodInitialBackoffSeconds > 0PodMaxBackoffSeconds >= PodInitialBackoffSeconds
Profiles(调度Profile列表)
  • 每个Profile定义一个独立的调度器"人格"
  • Pod通过spec.schedulerName匹配对应的Profile
  • 未指定schedulerName的Pod匹配"default-scheduler"Profile
  • 验证规则:至少1个Profile;SchedulerName不能重复;所有Profile的QueueSort必须一致
Extenders(调度扩展器)
  • 跨所有Profile共享
  • 最多1个Extender实现Bind操作
  • 支持Filter/Prioritize/Preempt/Bind四种HTTP动词
  • 支持TLS配置、超时设置、节点缓存能力声明

2.2 KubeSchedulerProfile结构

type KubeSchedulerProfile struct {
    SchedulerName string        // 调度器名称,匹配pod.spec.schedulerName
    Plugins       *Plugins      // 插件启用/禁用配置
    PluginConfig  []PluginConfig // 插件参数配置
}

设计要点:

  • SchedulerName是Profile的唯一标识和Pod路由键
  • Plugins为nil时使用默认插件集
  • PluginConfigname键索引(v1beta1标注+listType=map, +listMapKey=name
  • 省略PluginConfig的插件等同于使用该插件的默认配置

2.3 Plugins配置结构

type Plugins struct {
    QueueSort  PluginSet  // 队列排序扩展点
    PreFilter  PluginSet  // 预过滤扩展点
    Filter     PluginSet  // 过滤扩展点
    PostFilter PluginSet  // 后过滤扩展点(抢占相关)
    PreScore   PluginSet  // 预评分扩展点
    Score      PluginSet  // 评分扩展点
    Reserve    PluginSet  // 预留扩展点
    Permit     PluginSet  // 许可扩展点
    PreBind    PluginSet  // 预绑定扩展点
    Bind       PluginSet  // 绑定扩展点
    PostBind   PluginSet  // 后绑定扩展点
}

PluginSet详解:

type PluginSet struct {
    Enabled  []Plugin   // 额外启用的插件(在默认插件之后调用)
    Disabled []Plugin   // 禁用的默认插件("*"禁用所有默认插件)
}

type Plugin struct {
    Name   string  // 插件名称
    Weight int32   // 插件权重(仅Score扩展点使用)
}

核心语义规则:

  1. Enabled列表中的插件在默认插件之后按声明顺序调用
  2. 如果需要在默认插件之前调用,必须先Disabled默认插件,再Enabled重新排列
  3. Disabled中使用*表示禁用该扩展点的所有默认插件
  4. PluginSet为nil/空时,使用该扩展点的默认插件集
  5. Weight仅对Score扩展点有意义,其他扩展点忽略

Plugins.Apply方法——合并逻辑:

func (p *Plugins) Apply(customPlugins *Plugins) {
    // 对每个扩展点调用mergePluginSets
    p.QueueSort = mergePluginSets(p.QueueSort, customPlugins.QueueSort)
    // ... 其他10个扩展点同理
}

func mergePluginSets(defaultPluginSet, customPluginSet PluginSet) PluginSet {
    // 1. 收集customPluginSet.Disabled中的插件名
    disabledPlugins := sets.NewString()
    for _, disabledPlugin := range customPluginSet.Disabled {
        disabledPlugins.Insert(disabledPlugin.Name)
    }

    var enabledPlugins []Plugin
    // 2. 如果Disabled中没有"*",从默认插件中过滤掉被禁用的
    if !disabledPlugins.Has("*") {
        for _, defaultEnabledPlugin := range defaultPluginSet.Enabled {
            if disabledPlugins.Has(defaultEnabledPlugin.Name) {
                continue  // 跳过被禁用的默认插件
            }
            enabledPlugins = append(enabledPlugins, defaultEnabledPlugin)
        }
    }
    // 3. 追加自定义启用的插件
    enabledPlugins = append(enabledPlugins, customPluginSet.Enabled...)

    return PluginSet{Enabled: enabledPlugins}
}

逐行解析Apply:

  • 第一步:构建禁用集合——遍历custom.Disabled,将插件名(可能是*)插入Set
  • 第二步:如果存在*,则清空所有默认插件(相当于白名单模式:只有Enabled列表中的插件生效)
  • 第三步:如果无*,则从默认插件中筛掉被显式禁用的(相当于黑名单模式)
  • 第四步:将自定义启用的插件追加到末尾(顺序很重要——Score插件按声明顺序加权,Bind插件按声明顺序短路)

2.4 PluginConfig配置

// 内部版本
type PluginConfig struct {
    Name string          // 插件名称
    Args runtime.Object  // 插件参数(强类型内部对象)
}

// v1beta1版本
type PluginConfig struct {
    Name string                 // 插件名称
    Args runtime.RawExtension   // 插件参数(原始JSON/YAML字节 + 解码后的Object)
}

v1beta1的解码流程(DecodeNestedObjects):

func (c *PluginConfig) decodeNestedObjects(d runtime.Decoder) error {
    // 1. 构造GVK:Group="kubescheduler.config.k8s.io", Kind=插件名+"Args"
    gvk := SchemeGroupVersion.WithKind(c.Name + "Args")
    
    // 2. 干跑检测:空字节解码,判断是否是已注册的内部类型
    if _, _, err := d.Decode(nil, &gvk, nil); runtime.IsNotRegisteredError(err) {
        return nil  // 外部(out-of-tree)插件,跳过解码
    }
    
    // 3. 正式解码:从Raw字节反序列化为强类型对象
    obj, parsedGvk, err := d.Decode(c.Args.Raw, &gvk, nil)
    if err != nil {
        return fmt.Errorf("decoding args for plugin %s: %w", c.Name, err)
    }
    
    // 4. GVK一致性校验
    if parsedGvk.GroupKind() != gvk.GroupKind() {
        return fmt.Errorf("args for plugin %s were not of type %s, got %s", ...)
    }
    c.Args.Object = obj
    return nil
}

v1beta1的编码流程(EncodeNestedObjects):

func (c *PluginConfig) encodeNestedObjects(e runtime.Encoder) error {
    if c.Args.Object == nil { return nil }
    
    // 1. 将Object编码到buffer
    var buf bytes.Buffer
    err := e.Encode(c.Args.Object, &buf)
    
    // 2. YAML→JSON转换(因为编码器可能是YAML编码器,但外层期望JSON)
    json, err := yaml.YAMLToJSON(buf.Bytes())
    c.Args.Raw = json
    return nil
}

关键设计决策:

  • 外部插件(未注册到Scheme的类型)不会被报错,而是被优雅跳过——这是可扩展性的核心保障
  • 解码使用GVK推导(插件名+“Args”),而非显式声明——减少了配置冗余
  • YAML→JSON转换处理了kube-scheduler配置文件常用YAML但API Server期望JSON的现实

2.5 插件参数类型全景图

参数类型 对应插件 关键字段 默认值
DefaultPreemptionArgs DefaultPreemption MinCandidateNodesPercentage, MinCandidateNodesAbsolute 10%, 100
InterPodAffinityArgs InterPodAffinity HardPodAffinityWeight 1
NodeLabelArgs NodeLabel PresentLabels, AbsentLabels, PresentLabelsPreference, AbsentLabelsPreference
NodeResourcesFitArgs NodeResourcesFit IgnoredResources, IgnoredResourceGroups
PodTopologySpreadArgs PodTopologySpread DefaultConstraints, DefaultingType System/List
RequestedToCapacityRatioArgs RequestedToCapacityRatio Shape, Resources cpu+memory权重1
NodeResourcesLeastAllocatedArgs NodeResourcesLeastAllocated Resources cpu+memory权重1
NodeResourcesMostAllocatedArgs NodeResourcesMostAllocated Resources cpu+memory权重1
ServiceAffinityArgs ServiceAffinity AffinityLabels, AntiAffinityLabelsPreference
VolumeBindingArgs VolumeBinding BindTimeoutSeconds 600
NodeAffinityArgs NodeAffinity AddedAffinity nil

DefaultPreemptionArgs详细分析:

type DefaultPreemptionArgs struct {
    metav1.TypeMeta
    MinCandidateNodesPercentage int32  // 候选节点百分比,范围[0,100]
    MinCandidateNodesAbsolute    int32  // 候选节点绝对数,范围[0,∞)
}

候选节点数计算公式:numCandidates = max(numNodes * minCandidateNodesPercentage/100, minCandidateNodesAbsolute)

验证规则:

  • 两者不能同时为0
  • 百分比范围[0,100]
  • 绝对数范围[0,∞)

PodTopologySpreadArgs详细分析:

type PodTopologySpreadArgs struct {
    metav1.TypeMeta
    DefaultConstraints []v1.TopologySpreadConstraint  // 默认拓扑扩散约束
    DefaultingType     PodTopologySpreadConstraintsDefaulting  // "System"或"List"
}
  • SystemDefaulting:使用Kubernetes内置默认(跨Node和Zone扩散),此时DefaultConstraints必须为空
  • ListDefaulting:使用DefaultConstraints中用户定义的约束
  • FeatureGate DefaultPodTopologySpread 启用时默认为System,禁用时默认为List
  • DefaultConstraints的LabelSelector必须为nil(由Pod所属Service/RS自动推导)

2.6 DefaultBinder类结构

const Name = "DefaultBinder"

type DefaultBinder struct {
    handle framework.Handle  // 框架句柄,提供ClientSet等能力
}

// 实现framework.BindPlugin接口
var _ framework.BindPlugin = &DefaultBinder{}

func New(_ runtime.Object, handle framework.Handle) (framework.Plugin, error) {
    return &DefaultBinder{handle: handle}, nil
}

func (b DefaultBinder) Name() string { return Name }

func (b DefaultBinder) Bind(ctx context.Context, state *framework.CycleState, 
    p *v1.Pod, nodeName string) *framework.Status {
    klog.V(3).InfoS("Attempting to bind pod to node", "pod", klog.KObj(p), "node", nodeName)
    binding := &v1.Binding{
        ObjectMeta: metav1.ObjectMeta{Namespace: p.Namespace, Name: p.Name, UID: p.UID},
        Target:     v1.ObjectReference{Kind: "Node", Name: nodeName},
    }
    err := b.handle.ClientSet().CoreV1().Pods(binding.Namespace).Bind(ctx, binding, metav1.CreateOptions{})
    if err != nil {
        return framework.AsStatus(err)
    }
    return nil
}

DefaultBinder设计要点:

  1. 极简主义:整个Bind操作仅构造一个Binding对象并调用API Server
  2. Binding子资源:使用Pod的/binding子资源,而非直接更新Pod的nodeName——这确保了准入控制器的参与
  3. UID传递:Binding携带Pod的UID,防止并发冲突(如果Pod被重建,UID不匹配会导致绑定失败)
  4. 错误转换framework.AsStatus(err)将API错误包装为framework.Status,统一错误处理
  5. 无状态:DefaultBinder本身不维护任何状态,所有操作通过handle委托

2.7 QueueSort——PrioritySort排序逻辑

const Name = "PrioritySort"

type PrioritySort struct{}

var _ framework.QueueSortPlugin = &PrioritySort{}

func (pl *PrioritySort) Less(pInfo1, pInfo2 *framework.QueuedPodInfo) bool {
    p1 := corev1helpers.PodPriority(pInfo1.Pod)
    p2 := corev1helpers.PodPriority(pInfo2.Pod)
    return (p1 > p2) || (p1 == p2 && pInfo1.Timestamp.Before(pInfo2.Timestamp))
}

排序算法解析:

Less函数用于活跃队列(activeQ)的堆排序,决定了Pod出队的优先级:

  1. 第一排序键——优先级(Priority)p1 > p2

    • 优先级数值越大,Pod越重要,越先调度
    • corev1helpers.PodPrioritypod.Spec.Priority读取,nil时默认为0
  2. 第二排序键——入队时间戳(Timestamp)pInfo1.Timestamp.Before(pInfo2.Timestamp)

    • 同优先级时,先入队的Pod先调度(FIFO)
    • Timestamp是Pod进入调度队列的时间

注意: 与util.MoreImportantPod不同,PrioritySort.Less不比较Pod启动时间(StartTime),而是比较入队时间戳。这是因为在队列排序阶段,Pod尚未启动,StartTime不可靠。

2.8 工具函数库

utils.go
函数 功能 调用场景
GetPodFullName(pod) 返回Name_Namespace 日志和缓存键
GetPodStartTime(pod) 返回StartTime或当前时间 抢占受害者排序
GetEarliestPodStartTime(victims) 找最高优先级中最早启动的Pod 抢占策略
MoreImportantPod(pod1, pod2) 优先级→StartTime比较 抢占受害者选择
PatchPodStatus(cs, old, newStatus) StrategicMergePatch更新Pod状态 清除NominatedNodeName
DeletePod(cs, pod) 删除Pod 抢占执行
ClearNominatedNodeName(cs, pods...) 批量清除提名节点 调度失败后清理
IsScalarResourceName(name) 判断是否为标量资源 资源计算
non_zero.go
const (
    DefaultMilliCPURequest int64 = 100           // 0.1核
    DefaultMemoryRequest   int64 = 200 * 1024 * 1024  // 200MB
)

func GetNonzeroRequests(requests *v1.ResourceList) (int64, int64)  // 返回(CPU, Memory)
func GetNonzeroRequestForResource(resource v1.ResourceName, requests *v1.ResourceList) int64

设计意图:

  • 未显式请求CPU的Pod,视为请求100m(0.1核)
  • 未显式请求Memory的Pod,视为请求200MB
  • 这些默认值源自集群addon Pod的资源请求(#10653)
  • 目的:防止零请求Pod全部堆到同一节点,以及防止普通Pod误判零请求Pod不消耗资源
  • CPU和Memory是特殊处理(有默认值),其他资源未设置则返回0
  • EphemeralStorage受FeatureGate LocalStorageCapacityIsolation 控制
clock.go
type Clock interface { Now() time.Time }
type RealClock struct{}
func (RealClock) Now() time.Time { return time.Now() }

可测试的时钟抽象,用于注入伪时钟。


三、核心业务逻辑深度解析

3.1 Scheduler配置文件解析与验证流程

读取

识别GVK

v1beta1

v1

SetDefaults

DecodeNestedObjects

否 out-of-tree

convertToInternalPluginConfigArgs

YAML/JSON配置文件

scheme.Scheme.Decode

版本判断

KubeSchedulerConfiguration v1beta1

Policy v1

默认值填充

Parallelism=16

HealthzBindAddress=0.0.0.0:10251

MetricsBindAddress=0.0.0.0:10251

PercentageOfNodesToScore=0

PodInitialBackoffSeconds=1

PodMaxBackoffSeconds=10

LeaderElection.ResourceLock=leases

ClientConnection.QPS=50/Burst=100

EnableProfiling=true

PluginConfig Args解码

GVK是否注册?

Decode为强类型对象

跳过 保持RawExtension

转换到内部版本

PluginConfig.Args→runtime.Object

ValidateKubeSchedulerConfiguration

Parallelism>0?

Profiles非空?

SchedulerName唯一?

QueueSort跨Profile一致?

HealthzBindAddress合法?

PercentageOfNodesToScore∈0,100?

PodInitialBackoffSeconds>0?

PodMaxBackoffSeconds≥PodInitialBackoffSeconds?

Extenders合法?

有错误?

返回field.ErrorList 拒绝启动

配置生效 继续初始化

验证函数逐行解析——ValidateKubeSchedulerConfiguration:

func ValidateKubeSchedulerConfiguration(cc *config.KubeSchedulerConfiguration) field.ErrorList {
    allErrs := field.ErrorList{}
    
    // 1. 委托验证ClientConnection
    allErrs = append(allErrs, componentbasevalidation.ValidateClientConnectionConfiguration(
        &cc.ClientConnection, field.NewPath("clientConnection"))...)
    
    // 2. 委托验证LeaderElection
    allErrs = append(allErrs, componentbasevalidation.ValidateLeaderElectionConfiguration(
        &cc.LeaderElection, field.NewPath("leaderElection"))...)

    // 3. 并行度校验
    if cc.Parallelism <= 0 {
        allErrs = append(allErrs, field.Invalid(
            field.NewPath("parallelism"), cc.Parallelism,
            "should be an integer value greater than zero"))
    }

    // 4. Profile列表校验
    if len(cc.Profiles) == 0 {
        allErrs = append(allErrs, field.Required(profilesPath, ""))
    } else {
        existingProfiles := make(map[string]int, len(cc.Profiles))
        for i := range cc.Profiles {
            // 4a. 单Profile校验(SchedulerName非空)
            allErrs = append(allErrs, validateKubeSchedulerProfile(path, profile)...)
            // 4b. SchedulerName唯一性校验
            if idx, ok := existingProfiles[profile.SchedulerName]; ok {
                allErrs = append(allErrs, field.Duplicate(...))
            }
            existingProfiles[profile.SchedulerName] = i
        }
        // 4c. QueueSort跨Profile一致性校验
        allErrs = append(allErrs, validateCommonQueueSort(profilesPath, cc.Profiles)...)
    }

    // 5. 地址合法性校验
    for _, msg := range validation.IsValidSocketAddr(cc.HealthzBindAddress) { ... }
    for _, msg := range validation.IsValidSocketAddr(cc.MetricsBindAddress) { ... }

    // 6. 数值范围校验
    if cc.PercentageOfNodesToScore < 0 || cc.PercentageOfNodesToScore > 100 { ... }
    if cc.PodInitialBackoffSeconds <= 0 { ... }
    if cc.PodMaxBackoffSeconds < cc.PodInitialBackoffSeconds { ... }

    // 7. Extender校验
    allErrs = append(allErrs, validateExtenders(field.NewPath("extenders"), cc.Extenders)...)
    return allErrs
}

Extender验证规则:

  • 有PrioritizeVerb的Extender必须有正数Weight
  • 最多1个Extender实现BindVerb
  • ManagedResources名称必须符合扩展资源命名规范(域名/资源名格式)
  • ManagedResources名称不能跨Extender重复

3.2 配置版本转换(v1→v1beta1→Internal)

内部版本

外部版本

Convert_v1_LegacyExtender_To_config_Extender

Convert_v1beta1_KubeSchedulerConfiguration_To_config_KubeSchedulerConfiguration

Convert_config_KubeSchedulerConfiguration_To_v1beta1_KubeSchedulerConfiguration

Convert_config_Extender_To_v1_LegacyExtender

PluginConfig Args: RawExtension → Object

PluginConfig Args: Object → RawExtension

v1 (Policy)

v1beta1 (KubeSchedulerConfiguration)

Internal (config.KubeSchedulerConfiguration)

v1beta1→Internal转换核心逻辑:

func Convert_v1beta1_KubeSchedulerConfiguration_To_config_KubeSchedulerConfiguration(
    in *v1beta1.KubeSchedulerConfiguration, out *config.KubeSchedulerConfiguration, s conversion.Scope) error {
    // 1. 自动转换基础字段(指针→值类型、类型映射等)
    if err := autoConvert_v1beta1_KubeSchedulerConfiguration_To_config_KubeSchedulerConfiguration(in, out, s); err != nil {
        return err
    }
    // 2. 硬编码DefaultProvider(v1beta1不再支持AlgorithmSource)
    out.AlgorithmSource.Provider = pointer.StringPtr(v1beta1.SchedulerDefaultProviderName)
    // 3. 转换PluginConfig Args为内部类型
    return convertToInternalPluginConfigArgs(out)
}

convertToInternalPluginConfigArgs关键步骤:

func convertToInternalPluginConfigArgs(out *config.KubeSchedulerConfiguration) error {
    scheme := getPluginArgConversionScheme()  // 延迟初始化的Scheme
    for i := range out.Profiles {
        for j := range out.Profiles[i].PluginConfig {
            args := out.Profiles[i].PluginConfig[j].Args
            if args == nil { continue }
            if _, isUnknown := args.(*runtime.Unknown); isUnknown { continue }
            
            // 1. 应用默认值
            scheme.Default(args)
            // 2. 转换到内部版本
            internalArgs, err := scheme.ConvertToVersion(args, config.SchemeGroupVersion)
            if err != nil {
                return fmt.Errorf("converting .Profiles[%d].PluginConfig[%d].Args into internal type: %w", i, j, err)
            }
            out.Profiles[i].PluginConfig[j].Args = internalArgs
        }
    }
    return nil
}

版本间的关键差异:

特性 v1 (Policy) v1beta1 (ComponentConfig) Internal
配置范式 Predicates + Priorities Profiles + Plugins Profiles + Plugins
PluginConfig.Args N/A runtime.RawExtension runtime.Object
字段类型 值类型 指针类型(区分未设置) 值类型
AlgorithmSource 支持 移除 保留(废弃)
默认值 无Scheme默认 SetDefaults_*函数 由v1beta1转换时填充
Extender LegacyExtender Extender Extender

3.3 DefaultBinder Bind操作完整逻辑

etcd API Server DefaultBinder Framework etcd API Server DefaultBinder Framework Binding{ ObjectMeta: {Namespace, Name, UID}, Target: {Kind:"Node", Name: nodeName}} Bind扩展点特性: 多个Bind插件按声明顺序调用 任一返回成功则短路(跳过后续) Bind(ctx, state, pod, nodeName) klog.V(3) 日志记录 构造Binding对象 Pods(namespace).Bind(ctx, binding, CreateOptions{}) 准入控制器校验 写入Pod的nodeName 确认 err or nil nil (成功) 或 framework.AsStatus(err)

逐行解析Bind方法:

func (b DefaultBinder) Bind(ctx context.Context, state *framework.CycleState, 
    p *v1.Pod, nodeName string) *framework.Status {
    
    // 第1行:日志——V(3)级别,避免生产环境日志爆炸
    klog.V(3).InfoS("Attempting to bind pod to node", "pod", klog.KObj(p), "node", nodeName)
    
    // 第2-5行:构造Binding对象
    // - Namespace/Name/UID:精确标识Pod,UID防并发冲突
    // - Target.Kind="Node":绑定目标类型
    // - Target.Name=nodeName:目标节点名称
    binding := &v1.Binding{
        ObjectMeta: metav1.ObjectMeta{Namespace: p.Namespace, Name: p.Name, UID: p.UID},
        Target:     v1.ObjectReference{Kind: "Node", Name: nodeName},
    }
    
    // 第6行:调用API Server的Bind子资源
    // - 使用CreateOptions(而非UpdateOptions),因为Binding是创建操作
    // - ctx传播超时和取消信号
    err := b.handle.ClientSet().CoreV1().Pods(binding.Namespace).Bind(ctx, binding, metav1.CreateOptions{})
    
    // 第7-9行:错误处理
    // - framework.AsStatus将error封装为*framework.Status
    // - Status包含Code(HTTP状态码)、Message、Reason等
    if err != nil {
        return framework.AsStatus(err)
    }
    return nil
}

Binding子资源vs直接更新Pod的差异:

  • Binding子资源触发准入控制链(包括PodNodeSelector准入控制器)
  • 直接更新Pod的spec.nodeName绕过部分准入控制
  • Binding是幂等的——重复绑定同一节点不会报错
  • Binding携带UID确保操作的是同一个Pod实例

Bind扩展点的短路语义:

  • 多个Bind插件按Plugins.Bind.Enabled的声明顺序调用
  • 任一插件返回成功(nil),后续插件全部跳过
  • 所有Bind插件都返回不成功时,调度失败
  • DefaultBinder通常排在Bind列表最后,作为兜底

3.4 QueueSort Pod优先级排序算法

否 p1 < p2

Less(pInfo1, pInfo2)

p1 > p2?

return true — pInfo1更优先

p1 == p2?

return false — pInfo2更优先

pInfo1.Timestamp < pInfo2.Timestamp?

return true — pInfo1先入队 更优先

return false — pInfo2先入队 更优先

排序决策树深度分析:

比较层级1: Pod优先级 (Priority)
├── p1 > p2 → pInfo1更优先(高优先级Pod先调度)
├── p1 < p2 → pInfo2更优先
└── p1 == p2 → 进入层级2

比较层级2: 入队时间戳 (Timestamp)
├── pInfo1.Timestamp更早 → pInfo1更优先(同优先级FIFO)
└── pInfo2.Timestamp更早 → pInfo2更优先

corev1helpers.PodPriority实现:

// 位置: k8s.io/component-helpers/scheduling/corev1
func PodPriority(pod *v1.Pod) int32 {
    if pod.Spec.Priority != nil {
        return *pod.Spec.Priority
    }
    // 未设置优先级时,返回0
    // 当PodPriority功能门禁用时,所有Pod优先级为0
    return 0
}

与util.MoreImportantPod的对比:

特性 PrioritySort.Less MoreImportantPod
使用场景 activeQ堆排序 抢占受害者选择
第一排序键 Pod优先级 Pod优先级
第二排序键 入队时间戳(Timestamp) 启动时间(StartTime)
第三排序键
返回类型 bool bool
操作对象 QueuedPodInfo *v1.Pod

为什么排序键不同?

  • 队列排序时Pod尚未运行,StartTime不可靠,用入队时间戳更公平
  • 抢占选择时Pod已在运行,StartTime更准确反映调度顺序

3.5 Pod资源请求计算工具函数

CPU

Memory

EphemeralStorage

其他标量资源

GetNonzeroRequestForResource(resource, requests)

resource类型?

requests中存在CPU?

返回 DefaultMilliCPURequest=100 (0.1核)

返回 requests.Cpu().MilliValue()

requests中存在Memory?

返回 DefaultMemoryRequest=200MB

返回 requests.Memory().Value()

LocalStorageCapacityIsolation启?

返回 0

requests中存在?

返回 0

返回 quantity.Value()

IsScalarResourceName?

requests中存在?

返回 0

返回 quantity.Value()

返回 0

GetNonzeroRequests的关键设计决策:

  1. 未设置≠零:CPU和Memory未显式设置时使用默认值,而非0

    • 这防止零请求Pod全部被调度到最小负载节点
    • 也防止普通Pod误以为零请求Pod不消耗资源
  2. 显式设置零:如果用户显式设置CPU=0或Memory=0,则使用0

    • found变量区分了"未设置"和"设为0"
    • if _, found := (*requests)[v1.ResourceCPU]; !found——只覆盖未设置的情况
  3. 默认值来源:100m CPU和200MB Memory来自集群addon Pod的典型资源请求

    • 虽然值"pretty arbitrary"(引用源码注释),但实践中证明有效
  4. EphemeralStorage特殊处理:受FeatureGate控制

    • LocalStorageCapacityIsolation禁用时,Pod请求0磁盘
    • 启用时,未设置返回0(无默认值)

3.6 配置Scheme注册机制

// scheme/scheme.go
var (
    Scheme  = runtime.NewScheme()
    Codecs  = serializer.NewCodecFactory(Scheme, serializer.EnableStrict)
)

func init() { AddToScheme(Scheme) }

func AddToScheme(scheme *runtime.Scheme) {
    utilruntime.Must(kubeschedulerconfig.AddToScheme(scheme))      // 内部版本
    utilruntime.Must(kubeschedulerconfigv1.AddToScheme(scheme))    // v1版本
    utilruntime.Must(kubeschedulerconfigv1beta1.AddToScheme(scheme)) // v1beta1版本
    utilruntime.Must(scheme.SetVersionPriority(kubeschedulerconfigv1beta1.SchemeGroupVersion))
    // ↑ v1beta1为优先版本(解码时优先使用)
}

Scheme注册顺序的重要性:

  1. 内部版本先注册,建立GVK→GoType的映射
  2. v1和v1beta1注册,建立版本化GVK→GoType映射+转换函数
  3. SetVersionPriority设置v1beta1为优先版本
  4. serializer.EnableStrict启用严格模式,未知字段报错而非静默忽略

内部版本的已知类型注册:

func addKnownTypes(scheme *runtime.Scheme) error {
    scheme.AddKnownTypes(SchemeGroupVersion,
        &KubeSchedulerConfiguration{},
        &Policy{},
        &DefaultPreemptionArgs{},
        &InterPodAffinityArgs{},
        &NodeLabelArgs{},
        &NodeResourcesFitArgs{},
        &PodTopologySpreadArgs{},
        &RequestedToCapacityRatioArgs{},
        &ServiceAffinityArgs{},
        &VolumeBindingArgs{},
        &NodeResourcesLeastAllocatedArgs{},
        &NodeResourcesMostAllocatedArgs{},
        &NodeAffinityArgs{},
    )
    // Policy也注册到无Group的内部版本(历史兼容)
    scheme.AddKnownTypes(schema.GroupVersion{Group: "", Version: runtime.APIVersionInternal}, &Policy{})
    return nil
}

四、Mermaid架构图

4.1 KubeSchedulerConfiguration类图

profiles

plugins

pluginConfig

11个扩展点

enabled/disabled

extenders

KubeSchedulerConfiguration

+TypeMeta

+int32 Parallelism

+SchedulerAlgorithmSource AlgorithmSource

+LeaderElectionConfiguration LeaderElection

+ClientConnectionConfiguration ClientConnection

+string HealthzBindAddress

+string MetricsBindAddress

+DebuggingConfiguration Debugging

+int32 PercentageOfNodesToScore

+int64 PodInitialBackoffSeconds

+int64 PodMaxBackoffSeconds

+[]KubeSchedulerProfile Profiles

+[]Extender Extenders

KubeSchedulerProfile

+string SchedulerName

+Plugins Plugins

+[]PluginConfig PluginConfig

Plugins

+PluginSet QueueSort

+PluginSet PreFilter

+PluginSet Filter

+PluginSet PostFilter

+PluginSet PreScore

+PluginSet Score

+PluginSet Reserve

+PluginSet Permit

+PluginSet PreBind

+PluginSet Bind

+PluginSet PostBind

+Append(src)

+Apply(customPlugins)

PluginSet

+[]Plugin Enabled

+[]Plugin Disabled

Plugin

+string Name

+int32 Weight

PluginConfig

+string Name

+runtime.Object Args

Extender

+string URLPrefix

+string FilterVerb

+string PreemptVerb

+string PrioritizeVerb

+int64 Weight

+string BindVerb

+bool EnableHTTPS

+ExtenderTLSConfig TLSConfig

+Duration HTTPTimeout

+bool NodeCacheCapable

+[]ExtenderManagedResource ManagedResources

+bool Ignorable

4.2 配置版本转换流程图

验证层

内部版本

转换层

v1beta1处理

解码层

用户配置

GVK注册?

out-of-tree

通过

失败

YAML/JSON配置文件

Scheme.Decode()

v1beta1.KubeSchedulerConfiguration

SetDefaults_*函数

DecodeNestedObjects()

EncodeNestedObjects()

convertToInternalPluginConfigArgs()

AlgorithmSource.Provider=DefaultProvider

config.KubeSchedulerConfiguration

ValidateKubeSchedulerConfiguration()

Validate*Args()

运行时使用

拒绝启动

4.3 Plugins配置架构图

最终生效插件

Plugins.Apply合并

默认插件集

QueueSort: PrioritySort

PreFilter: NodePorts, NodeResourcesFit, PodTopologySpread, InterPodAffinity, VolumeBinding, NodeAffinity...

Filter: NodeUnschedulable, NodeName, NodePorts, NodeAffinity, TaintToleration...

PostFilter: DefaultPreemption

PreScore: InterPodAffinity, PodTopologySpread

Score: NodeResourcesFit, NodeAffinity, PodTopologySpread, InterPodAffinity, ImageLocality...

Reserve: VolumeBinding

Permit: -

PreBind: VolumeBinding

Bind: DefaultBinder

PostBind: -

mergePluginSets(default, custom)

QueueSort: PrioritySort + custom.Enabled - custom.Disabled

Filter: 默认Filter插件 + custom.Enabled - custom.Disabled

Score: 默认Score插件(加权) + custom.Enabled(加权) - custom.Disabled

Bind: DefaultBinder + custom.Enabled - custom.Disabled

4.4 配置验证流程图

ValidateKubeSchedulerConfiguration(cc)

ValidateClientConnectionConfiguration

ValidateLeaderElectionConfiguration

Parallelism > 0?

len(Profiles) > 0?

validateKubeSchedulerProfile

SchedulerName非空?

SchedulerName唯一?

validateCommonQueueSort

所有Profile的QueueSort PluginSet相等?

IsValidSocketAddr(HealthzBindAddress)

IsValidSocketAddr(MetricsBindAddress)

PercentageOfNodesToScore ∈ [0,100]?

PodInitialBackoffSeconds > 0?

PodMaxBackoffSeconds ≥ PodInitialBackoffSeconds?

validateExtenders

PrioritizeVerb → Weight > 0?

BindVerb → binders ≤ 1?

ManagedResources名称合法+不重复?

全通过?

配置合法

返回field.ErrorList

4.5 DefaultBinder Bind流程图

API Server内部

DefaultBinder.Bind(ctx, state, pod, nodeName)

klog.V(3) 日志

构造v1.Binding对象

ObjectMeta: Namespace, Name, UID

Target: Kind=Node, Name=nodeName

handle.ClientSet().CoreV1().Pods(ns).Bind(ctx, binding, CreateOptions{})

API调用成功?

return nil (绑定成功)

return framework.AsStatus(err)

准入控制器校验

写入etcd: pod.spec.nodeName = nodeName

4.6 QueueSort排序算法流程图

是 高优先级优先

否 p1 < p2

是 同优先级

是 先入队优先 FIFO

PrioritySort.Less(pInfo1, pInfo2)

p1 = PodPriority(pInfo1.Pod)

p2 = PodPriority(pInfo2.Pod)

p1 > p2?

return true

p1 == p2?

return false

pInfo1.Timestamp.Before(pInfo2.Timestamp)?

return true

return false

4.7 Pod优先级排序决策树

Pod-A: 100 > Pod-B: 50

Pod-A: 50 < Pod-B: 100

Pod-A: 100 == Pod-B: 100

Pod-A: 10:00 < Pod-B: 10:05

Pod-A: 10:05 > Pod-B: 10:00

PodPriority函数

PodPriority(pod)

pod.Spec.Priority != nil?

return *pod.Spec.Priority

return 0

两个Pod进入比较

优先级对比

Pod-A胜出
高优先级先调度

Pod-B胜出

时间戳对比

Pod-A胜出
同优先级FIFO

Pod-B胜出

4.8 资源请求计算流程图

否 未设置

否 未设置

注意

显式设置CPU=0 → 使用0

显式设置Memory=0 → 使用0

未设置 → 使用默认值

区分 '未设置' vs '设为0'

GetNonzeroRequests(requests)

GetNonzeroRequestForResource(CPU, requests)

GetNonzeroRequestForResource(Memory, requests)

CPU在requests中?

返回100 (0.1核)

返回requests.Cpu().MilliValue()

Memory在requests中?

返回200MB

返回requests.Memory().Value()

4.9 Metrics指标体系图

辅助

GaugeVec

HistogramVec

CounterVec

Histogram

scheduling_algorithm_duration_seconds
桶: 1ms~32s

preemption_victims
桶: 5,10,15,...,50 (线性)

pod_scheduling_attempts
桶: 1,2,4,8,16 (2^n, 5桶)

Counter

preemption_attempts_total

schedule_attempts_total
标签: result, profile

queue_incoming_pods_total
标签: queue, event

e2e_scheduling_duration_seconds
桶: 1ms~32s (2^n, 15桶)
标签: result, profile

pod_scheduling_duration_seconds
桶: 10ms~88m (2^n, 20桶)
标签: attempts

framework_extension_point_duration_seconds
桶: 0.1ms~200ms (2^n, 12桶)
标签: extension_point, status, profile

plugin_execution_duration_seconds
桶: 0.01ms~22ms (1.5^n, 20桶)
标签: plugin, extension_point, status

permit_wait_duration_seconds
桶: 1ms~32s
标签: result

pending_pods
标签: queue(active/backoff/unschedulable)

scheduler_goroutines
标签: work

scheduler_cache_size
标签: type(nodes/pods/assumed_pods)

PendingPodsRecorder
Inc/Dec/Clear

PodScheduled/Unschedulable/ScheduleError
→ observeScheduleAttemptAndLatency

4.10 配置加载→Framework实例化全链路图

kube-scheduler启动

--config指定配置文件路径

读取YAML文件

scheme.Codecs.UniversalDeserializer().Decode()

识别GVK: kubescheduler.config.k8s.io/v1beta1

反序列化为v1beta1.KubeSchedulerConfiguration

SetDefaults_KubeSchedulerConfiguration

Parallelism=16

Profiles=[{SchedulerName=default-scheduler}]

HealthzBindAddress=0.0.0.0:10251

MetricsBindAddress=0.0.0.0:10251

ClientConnection.QPS=50, Burst=100

LeaderElection.ResourceLock=leases

PodInitialBackoffSeconds=1

PodMaxBackoffSeconds=10

DecodeNestedObjects

PluginConfig.Args: RawExtension → runtime.Object

转换到内部版本 config.KubeSchedulerConfiguration

convertToInternalPluginConfigArgs

scheme.Default(args)

scheme.ConvertToVersion(args, internal)

ValidateKubeSchedulerConfiguration

Validate*Args for each PluginConfig

验证通过?

拒绝启动

遍历Profiles

为每个Profile创建Framework

registry.NewInTreeRegistry()

遍历Plugins的11个扩展点

mergePluginSets(default, custom)

实例化每个插件: Plugin.New(pluginConfig.Args, handle)

DefaultBinder.New → &DefaultBinder{handle}

PrioritySort.New → &PrioritySort{}

其他插件.New → ...

FrameworkImpl就绪

Scheduler.Run()

SchedulingQueue使用PrioritySort.Less排序

调度循环使用DefaultBinder.Bind绑定


五、关键代码逐行解析

5.1 mergePluginSets逐行解析

func mergePluginSets(defaultPluginSet, customPluginSet PluginSet) PluginSet {
    // 第1步:构建禁用插件名集合
    disabledPlugins := sets.NewString()
    for _, disabledPlugin := range customPluginSet.Disabled {
        disabledPlugins.Insert(disabledPlugin.Name)  // "*"也会被插入
    }

    var enabledPlugins []Plugin
    
    // 第2步:如果不是"禁用所有"模式,保留未被禁用的默认插件
    if !disabledPlugins.Has("*") {
        for _, defaultEnabledPlugin := range defaultPluginSet.Enabled {
            if disabledPlugins.Has(defaultEnabledPlugin.Name) {
                continue  // 该默认插件被显式禁用,跳过
            }
            enabledPlugins = append(enabledPlugins, defaultEnabledPlugin)  // 保留
        }
    }
    // 如果是"*"模式,enabledPlugins为空,所有默认插件被清除

    // 第3步:追加自定义启用的插件
    enabledPlugins = append(enabledPlugins, customPluginSet.Enabled...)
    // 注意:自定义插件在默认插件之后
    // 这意味着默认插件先执行,自定义插件后执行
    // 对于Bind扩展点:短路语义,默认插件(DefaultBinder)成功则跳过自定义
    // 对于Score扩展点:所有插件都执行,权重独立计算

    return PluginSet{Enabled: enabledPlugins}
}

5.2 DecodeNestedObjects逐行解析

func (c *KubeSchedulerConfiguration) DecodeNestedObjects(d runtime.Decoder) error {
    for i := range c.Profiles {
        prof := &c.Profiles[i]
        for j := range prof.PluginConfig {
            err := prof.PluginConfig[j].decodeNestedObjects(d)
            if err != nil {
                // 精确的错误路径:.profiles[0].pluginConfig[2]
                return fmt.Errorf("decoding .profiles[%d].pluginConfig[%d]: %w", i, j, err)
            }
        }
    }
    return nil
}

func (c *PluginConfig) decodeNestedObjects(d runtime.Decoder) error {
    // 1. 根据插件名推导GVK
    //    例如: Name="DefaultPreemption" → GVK=kubescheduler.config.k8s.io/v1beta1, Kind=DefaultPreemptionArgs
    gvk := SchemeGroupVersion.WithKind(c.Name + "Args")
    
    // 2. 干跑检测——传入nil字节,仅检查GVK是否在Scheme中注册
    //    如果返回NotRegisteredError,说明是外部插件,优雅跳过
    if _, _, err := d.Decode(nil, &gvk, nil); runtime.IsNotRegisteredError(err) {
        return nil  // 外部插件不报错,保持RawExtension原样
    }
    
    // 3. 正式解码——从c.Args.Raw字节反序列化为Go对象
    obj, parsedGvk, err := d.Decode(c.Args.Raw, &gvk, nil)
    if err != nil {
        return fmt.Errorf("decoding args for plugin %s: %w", c.Name, err)
    }
    
    // 4. GVK一致性校验——防止解码出的类型与期望不符
    //    例如:期望DefaultPreemptionArgs但实际解码出InterPodAffinityArgs
    if parsedGvk.GroupKind() != gvk.GroupKind() {
        return fmt.Errorf("args for plugin %s were not of type %s, got %s",
            c.Name, gvk.GroupKind(), parsedGvk.GroupKind())
    }
    
    // 5. 将解码后的对象挂到Args.Object上
    c.Args.Object = obj
    return nil
}

5.3 validateCommonQueueSort逐行解析

func validateCommonQueueSort(path *field.Path, profiles []config.KubeSchedulerProfile) field.ErrorList {
    allErrs := field.ErrorList{}
    
    // 取第一个Profile的QueueSort作为基准(canon)
    var canon config.PluginSet
    if profiles[0].Plugins != nil {
        canon = profiles[0].Plugins.QueueSort
    }
    
    // 遍历其余Profile,与基准比较
    for i := 1; i < len(profiles); i++ {
        var curr config.PluginSet
        if profiles[i].Plugins != nil {
            curr = profiles[i].Plugins.QueueSort
        }
        // 使用go-cmp深度比较——确保插件名、权重、顺序完全一致
        if !cmp.Equal(canon, curr) {
            allErrs = append(allErrs, field.Invalid(
                path.Index(i).Child("plugins", "queueSort"), curr,
                "has to match for all profiles"))
        }
    }
    
    // TODO(#88093): 还应验证QueueSort插件的PluginConfig在所有Profile中一致
    return allErrs
}

为什么QueueSort必须全局一致?

  • 调度队列(SchedulingQueue)是全局共享的,所有Profile的Pod进入同一个队列
  • 队列使用堆排序,堆的Less函数只能有一个
  • 如果不同Profile使用不同的QueueSort,堆排序的偏序关系会不一致

5.4 SetDefaults_KubeSchedulerConfiguration逐行解析

func SetDefaults_KubeSchedulerConfiguration(obj *v1beta1.KubeSchedulerConfiguration) {
    // 并行度默认16
    if obj.Parallelism == nil {
        obj.Parallelism = pointer.Int32Ptr(16)
    }

    // 无Profile时创建默认Profile
    if len(obj.Profiles) == 0 {
        obj.Profiles = append(obj.Profiles, v1beta1.KubeSchedulerProfile{})
    }
    // 单Profile且未设名称时使用"default-scheduler"
    if len(obj.Profiles) == 1 && obj.Profiles[0].SchedulerName == nil {
        obj.Profiles[0].SchedulerName = pointer.StringPtr(v1.DefaultSchedulerName)
    }

    // HealthzBindAddress三段式默认逻辑
    defaultBindAddress := net.JoinHostPort("0.0.0.0", strconv.Itoa(config.DefaultInsecureSchedulerPort))
    if obj.HealthzBindAddress == nil {
        obj.HealthzBindAddress = &defaultBindAddress  // 情况1: 未设置,全默认
    } else {
        if host, port, err := net.SplitHostPort(*obj.HealthzBindAddress); err == nil {
            if len(host) == 0 { host = "0.0.0.0" }
            hostPort := net.JoinHostPort(host, port)
            obj.HealthzBindAddress = &hostPort  // 情况2: 有端口,补全host
        } else {
            if host := net.ParseIP(*obj.HealthzBindAddress); host != nil {
                hostPort := net.JoinHostPort(*obj.HealthzBindAddress,
                    strconv.Itoa(config.DefaultInsecureSchedulerPort))
                obj.HealthzBindAddress = &hostPort  // 情况3: 纯IP,补全端口
            } else {
                obj.HealthzBindAddress = &defaultBindAddress  // 情况4: 无效值,全默认
            }
        }
    }
    // MetricsBindAddress同逻辑...

    // 领导选举配置
    if len(obj.LeaderElection.ResourceLock) == 0 {
        obj.LeaderElection.ResourceLock = "leases"  // 1.20+迁移到Lease
    }
    if len(obj.LeaderElection.ResourceNamespace) == 0 {
        obj.LeaderElection.ResourceNamespace = "kube-system"
    }
    if len(obj.LeaderElection.ResourceName) == 0 {
        obj.LeaderElection.ResourceName = "kube-scheduler"
    }

    // 客户端连接配置
    if len(obj.ClientConnection.ContentType) == 0 {
        obj.ClientConnection.ContentType = "application/vnd.kubernetes.protobuf"
    }
    if obj.ClientConnection.QPS == 0.0 {
        obj.ClientConnection.QPS = 50.0  // Scheduler专属QPS,高于通用默认5
    }
    if obj.ClientConnection.Burst == 0 {
        obj.ClientConnection.Burst = 100  // Scheduler专属Burst,高于通用默认10
    }

    // 退避时间
    if obj.PodInitialBackoffSeconds == nil { val := int64(1); obj.PodInitialBackoffSeconds = &val }
    if obj.PodMaxBackoffSeconds == nil { val := int64(10); obj.PodMaxBackoffSeconds = &val }

    // 调试配置
    if obj.EnableProfiling == nil { enableProfiling := true; obj.EnableProfiling = &enableProfiling }
    if *obj.EnableProfiling && obj.EnableContentionProfiling == nil {
        enableContentionProfiling := true; obj.EnableContentionProfiling = &enableContentionProfiling
    }
}

5.5 ProfileMetrics辅助函数逐行解析

var (
    scheduledResult     = "scheduled"
    unschedulableResult = "unschedulable"
    errorResult         = "error"
)

func PodScheduled(profile string, duration float64) {
    observeScheduleAttemptAndLatency(scheduledResult, profile, duration)
}

func PodUnschedulable(profile string, duration float64) {
    observeScheduleAttemptAndLatency(unschedulableResult, profile, duration)
}

func PodScheduleError(profile string, duration float64) {
    observeScheduleAttemptAndLatency(errorResult, profile, duration)
}

func observeScheduleAttemptAndLatency(result, profile string, duration float64) {
    // 双重记录:
    // 1. e2e_scheduling_duration_seconds 直方图——记录端到端延迟分布
    e2eSchedulingLatency.WithLabelValues(result, profile).Observe(duration)
    // 2. schedule_attempts_total 计数器——记录调度尝试次数
    scheduleAttempts.WithLabelValues(result, profile).Inc()
}

三个结果标签的含义:

  • scheduled:Pod成功调度并绑定到节点
  • unschedulable:Pod无法调度(Filter阶段全部节点不满足或Score阶段无合适节点)
  • error:调度过程中发生内部错误(如API Server通信失败)

六、Metrics指标体系深度分析

6.1 指标分类与用途

指标名 类型 用途 关键标签
schedule_attempts_total CounterVec 调度尝试总次数 result, profile
e2e_scheduling_duration_seconds HistogramVec 端到端调度延迟 result, profile
scheduling_algorithm_duration_seconds Histogram 调度算法延迟 -
preemption_victims Histogram 抢占受害者数量 -
preemption_attempts_total Counter 抢占尝试总次数 -
pending_pods GaugeVec 待调度Pod数量 queue
scheduler_goroutines GaugeVec 调度器goroutine数 work
pod_scheduling_duration_seconds HistogramVec Pod调度总耗时(含多次尝试) attempts
pod_scheduling_attempts Histogram Pod成功调度所需尝试次数 -
framework_extension_point_duration_seconds HistogramVec 扩展点执行延迟 extension_point, status, profile
plugin_execution_duration_seconds HistogramVec 单插件执行延迟 plugin, extension_point, status
queue_incoming_pods_total CounterVec 入队Pod数 queue, event
permit_wait_duration_seconds HistogramVec Permit等待时长 result
scheduler_cache_size GaugeVec 缓存大小 type

6.2 桶分布设计哲学

e2e_scheduling_duration_secondsExponentialBuckets(0.001, 2, 15)

  • 范围:1ms → 32s
  • 指数分布,关注毫秒级延迟
  • 用途:监控端到端调度性能

plugin_execution_duration_secondsExponentialBuckets(0.00001, 1.5, 20)

  • 范围:0.01ms → ~22ms
  • 更细粒度的1.5倍因子,因为插件延迟非常敏感
  • 用途:定位慢插件

preemption_victimsLinearBuckets(5, 5, 10)

  • 范围:5 → 50+
  • 线性分布,因为受害者数量通常较小
  • 用途:监控抢占规模

6.3 PendingPodsRecorder

type PendingPodsRecorder struct {
    recorder metrics.GaugeMetric
}

func NewActivePodsRecorder() *PendingPodsRecorder {
    return &PendingPodsRecorder{recorder: ActivePods()}
}
func NewUnschedulablePodsRecorder() *PendingPodsRecorder {
    return &PendingPodsRecorder{recorder: UnschedulablePods()}
}
func NewBackoffPodsRecorder() *PendingPodsRecorder {
    return &PendingPodsRecorder{recorder: BackoffPods()}
}

func (r *PendingPodsRecorder) Inc()  { r.recorder.Inc() }
func (r *PendingPodsRecorder) Dec()  { r.recorder.Dec() }
func (r *PendingPodsRecorder) Clear() { r.recorder.Set(float64(0)) }

设计意图:

  • 封装底层GaugeMetric,提供统一接口
  • Inc/Dec原子操作,确保并发安全
  • Clear用于队列清空时重置
  • 三个工厂方法分别对应三种队列(activeQ, backoffQ, unschedulableQ)

6.4 Pod资源指标收集器(resources/resources.go)

var podResourceDesc = resourceMetricsDescriptors{
    requests: resourceLifecycleDescriptors{
        total: metrics.NewDesc("kube_pod_resource_request",
            "Resources requested by workloads...",
            []string{"namespace", "pod", "node", "scheduler", "priority", "resource", "unit"},
            ...),
    },
    limits: resourceLifecycleDescriptors{
        total: metrics.NewDesc("kube_pod_resource_limit",
            "Resources limit for workloads...",
            []string{"namespace", "pod", "node", "scheduler", "priority", "resource", "unit"},
            ...),
    },
}

指标标签含义:

  • namespace/pod/node/scheduler/priority:Pod的元信息
  • resource:资源名(cpu, memory, ephemeral-storage, 扩展资源等)
  • unit:单位(cores, bytes, integer)

单位映射逻辑:

  • CPU → “cores”
  • Memory/Storage/EphemeralStorage/HugePages → “bytes”
  • AttachableVolume → “integer”
  • 其他 → 无单位(不记录)

终态Pod过滤:

func podRequestsAndLimitsByLifecycle(pod, reuseReqs, reuseLimits) (reqs, limits, terminal) {
    switch {
    case len(pod.Spec.NodeName) == 0:
        // 未调度的Pod不可能是终态
    case pod.Status.Phase == v1.PodSucceeded, pod.Status.Phase == v1.PodFailed:
        terminal = true  // 终态Pod排除在资源计算之外
    }
    ...
}

七、Extender API类型体系

7.1 Extender通信协议

Extender Scheduler Extender Scheduler Filter阶段 Prioritize阶段 Bind阶段 Preempt阶段 POST /urlPrefix/filterVerb ExtenderArgs{Pod, Nodes/NodeNames} ExtenderFilterResult{Nodes/NodeNames, FailedNodes, Error} POST /urlPrefix/prioritizeVerb ExtenderArgs{Pod, Nodes/NodeNames} HostPriorityList[{Host, Score}] POST /urlPrefix/bindVerb ExtenderBindingArgs{PodName, PodNamespace, PodUID, Node} ExtenderBindingResult{Error} POST /urlPrefix/preemptVerb ExtenderPreemptionArgs{Pod, NodeNameToVictims} ExtenderPreemptionResult{NodeNameToMetaVictims}

7.2 关键类型

ExtenderArgs——Filter/Prioritize请求:

type ExtenderArgs struct {
    Pod       *v1.Pod       // 待调度Pod
    Nodes     *v1.NodeList  // 候选节点列表(NodeCacheCapable=false时)
    NodeNames *[]string     // 候选节点名列表(NodeCacheCapable=true时)
}

ExtenderFilterResult——Filter响应:

type ExtenderFilterResult struct {
    Nodes                     *v1.NodeList  // 过滤后的节点
    NodeNames                 *[]string     // 过滤后的节点名
    FailedNodes               FailedNodesMap // 失败节点+原因
    FailedAndUnresolvableNodes FailedNodesMap // 不可解决的失败节点(抢占也无用)
    Error                     string         // 错误信息
}

HostPriority——Prioritize响应元素:

type HostPriority struct {
    Host  string  // 节点名
    Score int64   // 分数(范围0-10)
}

八、设计模式与最佳实践总结

8.1 设计模式

  1. Scheme模式:统一的类型注册/编解码/版本转换体系,借鉴了Kubernetes API Machinery的成熟模式
  2. 插件模式:Plugins+PluginConfig的声明式插件系统,支持运行时组合和替换
  3. 默认值函数模式SetDefaults_*函数集中管理默认值,与验证逻辑分离
  4. RawExtension嵌套编解码:PluginConfig.Args使用RawExtension承载任意结构,延迟解码
  5. Profile模式:多Profile支持多租户调度,每个Profile独立配置但共享队列和扩展器
  6. MetricRecorder封装:PendingPodsRecorder封装GaugeMetric,提供类型安全的Inc/Dec/Clear接口

8.2 关键数值常量

常量 含义
DefaultInsecureSchedulerPort 10251 旧版不安全端口
DefaultKubeSchedulerPort 10259 新版安全端口
DefaultMilliCPURequest 100 默认CPU请求(0.1核)
DefaultMemoryRequest 200MB 默认Memory请求
MaxCustomPriorityScore 10 自定义优先级函数最大分数
MaxTotalScore math.MaxInt64 总分上限
MaxWeight MaxTotalScore/10 最大权重
MinExtenderPriority 0 Extender最低优先级
MaxExtenderPriority 10 Extender最高优先级
DefaultBindTimeoutSeconds 600 VolumeBinding超时

8.3 注意事项与陷阱

  1. QueueSort一致性:所有Profile必须使用相同的QueueSort插件和配置,否则验证失败
  2. Bind短路语义:多个Bind插件按顺序调用,第一个成功即短路
  3. Score权重累积:Score插件权重可叠加,需注意总分校准
  4. PluginConfig Args类型推导:依赖插件名+"Args"的命名约定,外部插件不受影响
  5. 显式零vs未设置:v1beta1使用指针类型区分,内部版本使用值类型(由默认值函数处理)
  6. Extender Bind唯一性:最多1个Extender实现Bind,否则验证失败
  7. YAML配置的JSON转换:PluginConfig Args在编码时可能经历YAML→JSON转换
  8. Pod默认资源请求:未设置CPU/Memory的Pod使用非零默认值,影响调度决策
  9. Terminal Pod排除:Succeeded/Failed的Pod不参与资源指标统计
  10. Leader Election锁迁移:1.17→EndpointsLease,1.20→Lease,确保平滑过渡

九、配置版本演进路线

v1 (Policy)                    v1beta1 (ComponentConfig)           v1beta2 (未来)
┌──────────────────┐      ┌──────────────────────────┐      ┌────────────────────┐
│ Predicates +     │      │ Profiles + Plugins       │      │ Profiles + Plugins │
│ Priorities       │ ───→ │ PluginConfig             │ ───→ │ (移除AlgorithmSrc) │
│ HardPodAffinity  │      │ DecodeNestedObjects      │      │ 强类型安全         │
│ AlwaysCheckAll   │      │ SetDefaults_*            │      │ 更严格的验证       │
│ Extenders        │      │ Extenders(共享)          │      │                    │
└──────────────────┘      └──────────────────────────┘      └────────────────────┘
  已废弃                      当前推荐版本                      规划中

关键演进方向:

  • 从命令式(Predicates+Priorities)→声明式(Plugins+PluginConfig)
  • 从单调度器→多Profile调度器
  • 从硬编码算法源→可插拔框架
  • 从松散验证→严格模式(EnableStrict)
  • 从值类型→指针类型→强类型Schema

本文档基于Kubernetes源码(pkg/scheduler/apis/config, staging/src/k8s.io/kube-scheduler/config, framework/plugins/defaultbinder, framework/plugins/queuesort, util, metrics)进行超深度逐行分析,覆盖API类型体系、配置系统、版本转换、验证逻辑、DefaultBinder、PrioritySort、工具函数库和Metrics指标体系的完整技术栈。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐