
Kubernetes 调度器深度解析从 Filter 到 Score 的调度决策全过程Pod 为什么调度到了这个节点——当你的推理服务 Pod 被调度到一个 GPU 显存不足的节点上导致 OOM或者延迟敏感的服务和批处理任务被部署到同一个节点上相互干扰时理解调度器的完整决策流程就变得至关重要。本文将 K8s 调度器的 Filter → Score → Reserve 三阶段逐层拆解并演示自定义调度插件的开发方法。一、调度框架的扩展点体系Kubernetes 调度器kube-scheduler从 1.19 版本开始正式采用调度框架Scheduling Framework架构。它将调度过程抽象为多个扩展点Extension Points每个扩展点可以注册多个插件Plugin。flowchart TB subgraph SchedulingCycle[调度周期 (Scheduling Cycle)] S[Sortbr/队列排序] -- PF[PreFilterbr/预处理/前置过滤] PF -- F[Filterbr/过滤不可用节点] F -- PostF[PostFilterbr/无可用节点时的补救] PostF -- PreS[PreScorebr/评分前预处理] PreS -- Sc[Scorebr/对剩余节点打分] Sc -- NormS[NormalizeScorebr/分数归一化] NormS -- Res[Reservebr/预留资源] Res -- Perm[Permitbr/许可/等待/拒绝] end subgraph BindingCycle[绑定周期 (Binding Cycle)] Perm -- PreB[PreBindbr/绑定前准备] PreB -- Bind[Bindbr/绑定 Pod 到节点] Bind -- PostB[PostBindbr/绑定后通知] end PostB -- Done[调度完成] Res -.-|调度失败br/(Reserve 后出错)| UR[Unreservebr/回滚资源预留] style SchedulingCycle fill:#e1f5fe style BindingCycle fill:#fff3e0 style UR fill:#ffcdd2每个扩展点的职责扩展点阶段核心职责典型插件Sort队列排序决定 Pod 的出队顺序PrioritySortPreFilter前置过滤预处理 Pod 信息计算调度需要的状态NodeResourcesFitFilter节点过滤排除不满足条件的节点NodeUnschedulable, NodePorts, VolumeRestrictionsPostFilter后置过滤当无节点通过 Filter 时的补救抢占DefaultPreemptionPreScore评分前处理为 Score 阶段准备共享数据InterPodAffinityScore节点评分对每个候选节点打分NodeResourcesBalancedAllocation, ImageLocalityNormalizeScore分数归一化将各插件分数统一到 [0, 100]框架内置Reserve资源预留原子性地预留节点资源VolumeBindingPermit许可控制允许/拒绝/等待如 Gang Scheduling自定义场景PreBind绑定前绑定前最后的准备工作VolumeBindingBind绑定将 Pod 绑定到 NodeDefaultBinderPostBind绑定后绑定后的通知/清理事件通知二、Filter 阶段多级过滤漏斗Filter 阶段是一个漏斗模型——候选节点集从全部节点开始逐级过滤缩小范围。默认调度器启用了 13 个 Filter 插件每个插件检查一个维度的条件flowchart LR All[全部节点br/5000 Nodes] -- F1[NodeUnschedulablebr/排除不可调度节点] F1 --|4800 left| F2[NodePortsbr/检查端口冲突] F2 --|4780 left| F3[NodeResourcesFitbr/CPU/内存是否满足] F3 --|2300 left| F4[NodeAffinitybr/节点亲和性匹配] F4 --|1200 left| F5[TaintTolerationbr/污点容忍检查] F5 --|800 left| F6[VolumeLimitsbr/卷数量限制] F6 --|750 left| F7[其他 Filter 插件] F7 --|700 left| Score[进入 Score 阶段]Filter 插件的执行是并行的——每个节点可以独立判断是否满足条件。但 kube-scheduler 默认以percentageOfNodesToScore默认 0%即根据集群大小自适应来控制 Filter 阶段扫描的节点比例在 5000 节点集群中可能只扫描 5-10% 的节点。这在大集群中显著降低了调度延迟但可能导致次优的调度结果。GPU 资源的 Filter 挑战标准 Kubernetes 对 GPU 的支持通过 Device Plugin 机制实现。但在 AI 推理场景下Filter 需要更细粒度的判断——不只是是否有 GPU还包括显存、GPU 型号、拓扑亲和性等。通常需要自定义 Filter 插件package gpuscheduler import ( context fmt v1 k8s.io/api/core/v1 k8s.io/apimachinery/pkg/runtime k8s.io/kubernetes/pkg/scheduler/framework k8s.io/kubernetes/pkg/scheduler/framework/plugins/names ) // GPUFilterPlugin 自定义 Filter 插件 // 功能不仅检查 GPU 是否可用还检查显存是否满足 Pod 需求 type GPUFilterPlugin struct { handle framework.Handle } var _ framework.FilterPlugin GPUFilterPlugin{} var _ framework.PreFilterPlugin GPUFilterPlugin{} const ( PluginName GPUFilter // Pod 通过 annotation 声明 GPU 需求 GPUAnnotation gpu-scheduler/gpu-count GPUModelAnnotation gpu-scheduler/gpu-model GPUMemAnnotation gpu-scheduler/gpu-memory-gb ) // Name 返回插件名称在调度器配置中引用 func (g *GPUFilterPlugin) Name() string { return PluginName } // PreFilter 在 Filter 之前执行预处理 Pod 的 GPU 需求 // 这里将 annotation 中的 GPU 需求写入 CycleState避免后续重复解析 func (g *GPUFilterPlugin) PreFilter( ctx context.Context, state *framework.CycleState, pod *v1.Pod, ) (*framework.PreFilterResult, *framework.Status) { // 如果 Pod 没有 GPU 需求跳过所有 GPU 相关的 Filter if pod.Annotations[GPUAnnotation] { return nil, framework.NewStatus(framework.Skip, no GPU requirement) } // 将 GPU 需求写入 CycleState线程安全的临时存储 gpuRequirement : GPURequirement{ Count: parseIntOrZero(pod.Annotations[GPUAnnotation]), Model: pod.Annotations[GPUModelAnnotation], MemoryGB: parseIntOrZero(pod.Annotations[GPUMemAnnotation]), } state.Write(framework.StateKey(PluginName), gpuRequirement) return nil, framework.NewStatus(framework.Success) } // Filter 检查节点是否满足 Pod 的 GPU 需求 // 这个函数会被调度器并行调用每个候选节点一次 func (g *GPUFilterPlugin) Filter( ctx context.Context, state *framework.CycleState, pod *v1.Pod, nodeInfo *framework.NodeInfo, ) *framework.Status { // 从 CycleState 中读取之前预处理的 GPU 需求 data, err : state.Read(framework.StateKey(PluginName)) if err ! nil { // 如果没有 GPU 需求PreFilter 返回了 Skip通过 Filter return framework.NewStatus(framework.Success) } requirement : data.(*GPURequirement) node : nodeInfo.Node() // 1. 检查 GPU 数量 allocatableGPUs : node.Status.Allocatable[nvidia.com/gpu] if allocatableGPUs.Value() int64(requirement.Count) { return framework.NewStatus( framework.Unschedulable, fmt.Sprintf(节点 %s 的 GPU 不足: 需要 %d, 可用 %d, node.Name, requirement.Count, allocatableGPUs.Value()), ) } // 2. 检查 GPU 显存通过节点 label 存储由 Device Plugin 上报 gpuMemoryGB : node.Labels[GPUMemAnnotation] if parseIntOrZero(gpuMemoryGB) requirement.MemoryGB { return framework.NewStatus( framework.Unschedulable, fmt.Sprintf(节点 %s 的 GPU 显存不足: 需要 %dGB, 可用 %sGB, node.Name, requirement.MemoryGB, gpuMemoryGB), ) } // 3. 检查 GPU 型号如果指定 if requirement.Model ! { nodeGPUModel : node.Labels[GPUModelAnnotation] if nodeGPUModel ! requirement.Model { return framework.NewStatus( framework.UnschedulableAndUnresolvable, fmt.Sprintf(节点 %s 的 GPU 型号不匹配: 需要 %s, 实际 %s, node.Name, requirement.Model, nodeGPUModel), ) } } // 4. 检查 GPU 拓扑亲和性NVLink 连接 // 如果 Pod 需要多个 GPU 且要求 NVLink 互联 // 需要确认所有 GPU 在同一 NVLink 域内简化实现 if requirement.Count 1 pod.Annotations[gpu-scheduler/nvlink-required] true { if !g.checkNVLinkAffinity(node, requirement.Count) { return framework.NewStatus( framework.Unschedulable, fmt.Sprintf(节点 %s 无法提供 %d 个 NVLink 互联的 GPU, node.Name, requirement.Count), ) } } return framework.NewStatus(framework.Success) } // checkNVLinkAffinity 检查节点上是否有足够的 NVLink 互联 GPU func (g *GPUFilterPlugin) checkNVLinkAffinity( node *v1.Node, requiredCount int, ) bool { // 实际实现需要读取 NVLink 拓扑信息 // 这里简化通过节点 label 确认 NVLink 域内的 GPU 数量 maxInDomain : parseIntOrZero(node.Labels[gpu-scheduler/max-nvlink-group-size]) return maxInDomain requiredCount } type GPURequirement struct { Count int Model string MemoryGB int } func parseIntOrZero(s string) int { var result int fmt.Sscanf(s, %d, result) return result }三、Score 阶段多维度加权评分Filter 之后剩余的候选节点进入 Score 阶段。每个 Score 插件为节点打分通常 0-100然后按配置的权重加权求和。默认启用的 Score 插件及其权重Kubernetes 1.29插件权重评分逻辑NodeResourcesFit1LeastAllocated资源利用率最低得分最高或 MostAllocated资源利用率最高得分最高NodeResourcesBalancedAllocation1CPU 和内存使用比例的差异越小得分越高ImageLocality1节点上已存在 Pod 所需镜像 → 高分避免镜像拉取InterPodAffinity2满足 Pod 间亲和性/反亲和性规则NodeAffinity2满足节点亲和性偏好TaintToleration3能容忍越多污点的节点得分越高加权总分的计算公式NodeScore Σ (PluginScore_i × Weight_i)对于 AI 推理场景建议自定义 Score 插件来优化 GPU 节点的分配// GPUScorePlugin 自定义 Score 插件 // 评分策略 // 1. GPU 型号匹配度100 → 型号精确匹配 // 2. GPU 显存余量余量越多分数越高为突发请求预留 // 3. GPU 温度温度越低分数越高避免热节流 type GPUScorePlugin struct { handle framework.Handle } var _ framework.ScorePlugin GPUScorePlugin{} func (g *GPUScorePlugin) Name() string { return GPUScore } func (g *GPUScorePlugin) Score( ctx context.Context, state *framework.CycleState, pod *v1.Pod, nodeName string, ) (int64, *framework.Status) { nodeInfo, err : g.handle.SnapshotSharedLister().NodeInfos().Get(nodeName) if err ! nil { return 0, framework.NewStatus(framework.Error, err.Error()) } node : nodeInfo.Node() var totalScore int64 0 // 1. GPU 型号匹配度权重 40% requiredModel : pod.Annotations[GPUModelAnnotation] if requiredModel ! { nodeModel : node.Labels[GPUModelAnnotation] if nodeModel requiredModel { totalScore 40 } } else { totalScore 40 // 无型号要求所有节点都得满分 } // 2. GPU 显存余量评分权重 35% // 余量 (总显存 - 已用显存) / 总显存 totalMemory : parseIntOrZero(node.Labels[gpu-scheduler/total-memory-gb]) usedMemory : parseIntOrZero(node.Annotations[gpu-scheduler/used-memory-gb]) if totalMemory 0 { freeRatio : float64(totalMemory-usedMemory) / float64(totalMemory) // 余量 60% 得满分 35余量 10% 得 0 分 totalScore int64(freeRatio / 0.6 * 35) } // 3. 当前推理请求队列长度权重 25% // 队列越短分数越高实现负载均衡 queueLength : parseIntOrZero(node.Annotations[gpu-scheduler/current-queue]) // 队列长度 10 得 0 分队列 0 得满分 25 if queueLength 10 { totalScore int64((10 - queueLength) * 25 / 10) } return totalScore, framework.NewStatus(framework.Success) } // ScoreExtensions 返回 NormalizeScore 插件 func (g *GPUScorePlugin) ScoreExtensions() framework.ScoreExtensions { return g } // NormalizeScore 不做额外归一化默认已映射到 [0, 100] func (g *GPUScorePlugin) NormalizeScore( ctx context.Context, state *framework.CycleState, pod *v1.Pod, scores framework.NodeScoreList, ) *framework.Status { return nil }四、自定义调度插件的部署方式自定义插件有两种部署模式1. 内置方式In-Tree将插件编译进 kube-scheduler 二进制文件。适合组织内部的标准插件。需要维护自己的 kube-scheduler 镜像。2. 外置方式Out-of-Tree通过调度器框架Scheduler Framework实现独立的调度器与默认调度器共存。适合实验性或定制化需求。多调度器共存的典型场景# 场景默认调度器处理普通 PodGPU 调度器处理 AI 推理 Pod --- # GPU 调度器的 Deployment apiVersion: apps/v1 kind: Deployment metadata: name: gpu-scheduler namespace: kube-system spec: replicas: 2 # 高可用2 个副本使用 Leader Election selector: matchLabels: component: gpu-scheduler template: metadata: labels: component: gpu-scheduler spec: serviceAccountName: gpu-scheduler containers: - name: gpu-scheduler image: registry.example.com/gpu-scheduler:v1.2.0 command: - /usr/local/bin/kube-scheduler - --config/etc/kubernetes/gpu-scheduler-config.yaml - --leader-electtrue - --leader-elect-resource-namegpu-scheduler - --port10260 # 与默认调度器 10259 区分 volumeMounts: - name: scheduler-config mountPath: /etc/kubernetes volumes: - name: scheduler-config configMap: name: gpu-scheduler-config --- # GPU 调度器配置 apiVersion: v1 kind: ConfigMap metadata: name: gpu-scheduler-config namespace: kube-system data: gpu-scheduler-config.yaml: | apiVersion: kubescheduler.config.k8s.io/v1 kind: KubeSchedulerConfiguration leaderElection: leaderElect: true resourceName: gpu-scheduler profiles: - schedulerName: gpu-scheduler # 调度器名称 plugins: filter: enabled: - name: GPUFilter score: enabled: - name: GPUScore weight: 10 # 权重最高 - name: NodeResourcesFit weight: 1 pluginConfig: - name: GPUFilter args: minGpuMemoryGB: 8 --- # 使用 GPU 调度器的 Pod 示例 apiVersion: v1 kind: Pod metadata: name: llm-inference-pod annotations: gpu-scheduler/gpu-count: 2 gpu-scheduler/gpu-model: A100 gpu-scheduler/gpu-memory-gb: 40 gpu-scheduler/nvlink-required: true spec: schedulerName: gpu-scheduler # 指定使用 GPU 调度器 containers: - name: llm-server image: vllm/vllm-openai:latest resources: limits: nvidia.com/gpu: 2调度延迟优化策略percentageOfNodesToScore大集群 1000 节点建议设为 5-10%牺牲调度最优性换取延迟Pod Topology Spread对 Filter 阶段影响较大复杂约束会增加计算时间禁用不必要的插件每个插件都会增加 Filter/Score 各阶段的耗时。审查所有启用的插件关闭不需要的NodeCache 刷新频率默认 100msnodeCacheRefreshInterval对于动态变化的 GPU 指标可在节点端做聚合上报来减少调度器的刷新压力五、总结K8s 调度器的 Filter → Score → Reserve 三阶段架构提供了高度可扩展的调度决策框架。三个关键实践Filter 是漏斗Score 是排序。Filter 的目标是快速排除不满足硬性条件的节点GPU 数量、内存、亲和性Score 的目标是对剩余节点做精细化排序。理解这个分工是设计自定义插件的基础——不要在 Filter 中做打分的工作也不要在 Score 中做硬性排除的工作。自定义插件应该关注领域特定的需求。K8s 原生调度器不理解GPU 显存、NVLink 拓扑、推理请求队列等 AI 推理领域的概念。通过自定义 Filter Score 插件可以将这些领域知识注入调度决策避免推理 Pod 被调度到不合适的 GPU 节点。多调度器共存是平滑迁移的最佳路径。通过schedulerName字段AI 推理 Pod 使用 GPU 调度器普通 Pod 继续使用默认调度器。两者独立运行、互不干扰。逐步将流量从默认调度器迁移到专用调度器可以降低风险。