news 2026/7/13 11:50:01

Kubernetes 调度器深度解析:从 Filter 到 Score 的调度决策全过程

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Kubernetes 调度器深度解析:从 Filter 到 Score 的调度决策全过程

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[Sort<br/>队列排序] --> PF[PreFilter<br/>预处理/前置过滤] PF --> F[Filter<br/>过滤不可用节点] F --> PostF[PostFilter<br/>无可用节点时的补救] PostF --> PreS[PreScore<br/>评分前预处理] PreS --> Sc[Score<br/>对剩余节点打分] Sc --> NormS[NormalizeScore<br/>分数归一化] NormS --> Res[Reserve<br/>预留资源] Res --> Perm[Permit<br/>许可/等待/拒绝] end subgraph BindingCycle["绑定周期 (Binding Cycle)"] Perm --> PreB[PreBind<br/>绑定前准备] PreB --> Bind[Bind<br/>绑定 Pod 到节点] Bind --> PostB[PostBind<br/>绑定后通知] end PostB --> Done[调度完成] Res -.->|"调度失败<br/>(Reserve 后出错)"| UR[Unreserve<br/>回滚资源预留] style SchedulingCycle fill:#e1f5fe style BindingCycle fill:#fff3e0 style UR fill:#ffcdd2

每个扩展点的职责:

扩展点阶段核心职责典型插件
Sort队列排序决定 Pod 的出队顺序PrioritySort
PreFilter前置过滤预处理 Pod 信息,计算调度需要的状态NodeResourcesFit
Filter节点过滤排除不满足条件的节点NodeUnschedulable, NodePorts, VolumeRestrictions
PostFilter后置过滤当无节点通过 Filter 时的补救(抢占)DefaultPreemption
PreScore评分前处理为 Score 阶段准备共享数据InterPodAffinity
Score节点评分对每个候选节点打分NodeResourcesBalancedAllocation, ImageLocality
NormalizeScore分数归一化将各插件分数统一到 [0, 100](框架内置)
Reserve资源预留原子性地预留节点资源VolumeBinding
Permit许可控制允许/拒绝/等待(如 Gang Scheduling)(自定义场景)
PreBind绑定前绑定前最后的准备工作VolumeBinding
Bind绑定将 Pod 绑定到 NodeDefaultBinder
PostBind绑定后绑定后的通知/清理(事件通知)

二、Filter 阶段:多级过滤漏斗

Filter 阶段是一个漏斗模型——候选节点集从全部节点开始,逐级过滤缩小范围。默认调度器启用了 13 个 Filter 插件,每个插件检查一个维度的条件:

flowchart LR All[全部节点<br/>5000 Nodes] --> F1["NodeUnschedulable<br/>排除不可调度节点"] F1 -->|"4800 left"| F2["NodePorts<br/>检查端口冲突"] F2 -->|"4780 left"| F3["NodeResourcesFit<br/>CPU/内存是否满足"] F3 -->|"2300 left"| F4["NodeAffinity<br/>节点亲和性匹配"] F4 -->|"1200 left"| F5["TaintToleration<br/>污点容忍检查"] F5 -->|"800 left"| F6["VolumeLimits<br/>卷数量限制"] 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)实现独立的调度器,与默认调度器共存。适合实验性或定制化需求。

多调度器共存的典型场景:

# 场景:默认调度器处理普通 Pod,GPU 调度器处理 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-elect=true - --leader-elect-resource-name=gpu-scheduler - --port=10260 # 与默认调度器 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

调度延迟优化策略

  1. percentageOfNodesToScore:大集群(> 1000 节点)建议设为 5-10%,牺牲调度最优性换取延迟
  2. Pod Topology Spread:对 Filter 阶段影响较大,复杂约束会增加计算时间
  3. 禁用不必要的插件:每个插件都会增加 Filter/Score 各阶段的耗时。审查所有启用的插件,关闭不需要的
  4. NodeCache 刷新频率:默认 100ms(nodeCacheRefreshInterval),对于动态变化的 GPU 指标,可在节点端做聚合上报来减少调度器的刷新压力

五、总结

K8s 调度器的 Filter → Score → Reserve 三阶段架构提供了高度可扩展的调度决策框架。三个关键实践:

  1. Filter 是漏斗,Score 是排序。Filter 的目标是快速排除不满足硬性条件的节点(GPU 数量、内存、亲和性);Score 的目标是对剩余节点做精细化排序。理解这个分工是设计自定义插件的基础——不要在 Filter 中做"打分"的工作,也不要在 Score 中做"硬性排除"的工作。

  2. 自定义插件应该关注领域特定的需求。K8s 原生调度器不理解"GPU 显存"、"NVLink 拓扑"、"推理请求队列"等 AI 推理领域的概念。通过自定义 Filter + Score 插件,可以将这些领域知识注入调度决策,避免推理 Pod 被调度到不合适的 GPU 节点。

  3. 多调度器共存是平滑迁移的最佳路径。通过schedulerName字段,AI 推理 Pod 使用 GPU 调度器,普通 Pod 继续使用默认调度器。两者独立运行、互不干扰。逐步将流量从默认调度器迁移到专用调度器,可以降低风险。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/13 11:49:49

终极指南:如何安全高效地安装Switch大气层定制固件系统

终极指南&#xff1a;如何安全高效地安装Switch大气层定制固件系统 【免费下载链接】Atmosphere-stable 大气层整合包系统稳定版 项目地址: https://gitcode.com/gh_mirrors/at/Atmosphere-stable 你是否想解锁Nintendo Switch的全部潜力&#xff1f;大气层整合包系统为…

作者头像 李华
网站建设 2026/7/13 11:49:04

Cisco Catalyst 9000 交换机输出丢弃:3步排查法与5种QoS优化策略

Cisco Catalyst 9000交换机输出丢弃故障排查与QoS优化实战指南 1. 输出丢弃现象的本质与监控方法 当网络管理员在Catalyst 9000系列交换机上看到"output drops"计数器持续增长时&#xff0c;往往意味着网络性能问题正在酝酿。输出丢弃并非总是故障信号&#xff0c;但…

作者头像 李华
网站建设 2026/7/13 11:48:17

ESP-NOW 协议深度解析:3种组网模式对比与250字节负载实战

ESP-NOW 协议深度解析&#xff1a;3种组网模式对比与250字节负载实战在物联网设备快速普及的今天&#xff0c;如何选择一种高效、低功耗的无线通信协议成为开发者面临的关键问题。ESP-NOW作为乐鑫推出的专有协议&#xff0c;凭借其无连接、低延迟的特性&#xff0c;正在智能家居…

作者头像 李华
网站建设 2026/7/13 11:48:09

C++音视频开发实战:基于FFmpeg与SDL2的采集播放系统构建

1. 项目概述&#xff1a;为什么C是音视频处理的基石如果你正在寻找一个能真正体现C威力的实战项目&#xff0c;音视频采集与播放绝对是一个绝佳的选择。这不仅仅是调用几个API那么简单&#xff0c;它涉及从硬件信号到屏幕像素、从声波到扬声器振动的完整链路&#xff0c;是理解…

作者头像 李华
网站建设 2026/7/13 11:46:50

深度学习在电力市场电价预测中的应用与TimeMixer模型解析

1. 项目概述电力市场电价预测一直是能源领域最具挑战性的任务之一。西班牙作为欧洲电力市场化改革的先行者&#xff0c;其电价波动受多种因素影响&#xff0c;包括可再生能源发电量、负荷需求、温度变化等。传统统计方法在应对这种复杂的非线性关系时往往力不从心&#xff0c;而…

作者头像 李华
网站建设 2026/7/13 11:45:52

Unity URP渲染管线CG到HLSL着色器迁移实战指南

1. 项目概述&#xff1a;为什么URP时代必须拥抱HLSL&#xff1f;如果你是从Unity内置渲染管线&#xff08;Built-in Render Pipeline&#xff09;时代一路走来的开发者&#xff0c;手里肯定攒了不少用CG语言写的Shader。这些Shader曾经是你的宝贝&#xff0c;但随着项目升级到通…

作者头像 李华