



























k8s v1.36.1 Scheduler Framework 内置插件 调度源码 Filter / Score / PreFilter
上一篇我们把 kube-scheduler 的"骨架"拆完了:从 Scheduler 对象构建、Profile 注册、调度队列,到 SchedulingCycle 流水线,每个扩展点(PreEnqueue / PreFilter / Filter / PostFilter / Reserve / Permit / PreBind / Bind 等)出现的时机和职责都讲清楚了。但骨架归骨架,真正决定一个 Pod "能不能跑、跑得好不好"的,是挂在这些扩展点上的一颗颗"齿轮" —— 也就是我们今天要聊的内置 InTreePlugin。
本文我们会一头扎进 kube-scheduler 的源码深处,按"调度流程实际触发顺序"挑选六个最核心的内置插件:NodeResourcesFit → NodeAffinity → TaintToleration → InterPodAffinity → PodTopologySpread → VolumeBinding。每个插件都按相同的六段式拆给你看:它是什么、为什么要有、怎么用、源码长什么样、生产踩过什么坑、QA 大放送。
读完之后,你会对调度器"为什么把 Pod 放到 A 节点而不是 B 节点"有完整的可追溯链路。本文所有源码引用、API 字段、配置示例,均以 k8s v1.36.1 的 tag 为准。
🔓 学习重点提示
★ 【必须】必须掌握
• 扩展点签名:Filter(ctx, state, pod, nodeInfo) *Status 和 Score(ctx, state, pod, nodeInfo) (int64, *Status) 的精确写法
• Plugin Name 常量:所有插件名字都集中在 pkg/scheduler/framework/plugins/names/names.go
• Plugin 注册表:pkg/scheduler/framework/plugins/registry.go 的 NewInTreeRegistry() 函数
• Filter 与 Score 的本质区别:Filter 是"行 / 不行",Score 是"好 / 更好",前者短路、后者打分
• CycleState 复用:PreFilter 算的 pod 资源请求会被 Filter / Reserve / PreBind 跨阶段复用
☆ 次重点(了解即可)
• 了解每个插件的 EventsToRegister 决定了哪些 informer 事件能让 unschedulable 的 Pod 被重新入队
• 了解 Unschedulable 与 UnschedulableAndUnresolvable 的差别
📋 文章目录
"内置插件"是 kube-scheduler 在编译期就内置在二进制里、不需要外挂的调度策略实现。它们各自只负责一件事,但组合起来就能完成"挑选最佳节点"这个核心任务。下面我们按"调度流水线里的实际触发顺序"逐个亮相。
NodeResourcesFit:最经典的 Fit 插件。它只看一件事 —— 节点上的 CPU / 内存 / GPU / 临时存储等资源是否够 Pod 申请。如果不够,这节点直接出局。它本身还有四个打分策略:LeastAllocated(最闲优先,默认)、MostAllocated(最忙优先,回收场景)、BalancedAllocation(CPU/内存均衡)、RequestedToCapacityRatio(按容量比定制)。
NodeAffinity:节点亲和性。看 pod.spec.affinity.nodeAffinity 里写的节点选择器(required / preferred),跟节点的 label 配对,能不匹配。required 是硬条件,preferred 是软打分。它同时还负责解析 pod.spec.nodeSelector 这个老字段。
TaintToleration:污点容忍。节点可以被"打污点"(kubectl taint nodes xxx key=value:NoSchedule),普通 Pod 默认不能跑在被打了 NoSchedule 污点的节点上;除非 Pod 在 spec.tolerations 里声明"我能忍",否则就被过滤掉。它和 NodeAffinity 是 Node 上"准入"环节的两道闸。
InterPodAffinity:Pod 间亲和 / 反亲和。看 pod.spec.affinity.podAffinity 和 pod.spec.antiAffinity,判断"这个 Pod 想跟某些 Pod 待在一起 / 想离某些 Pod 远一点"。这是一个典型的"群居型"插件,对 Web + Redis、Deployment 多副本打散等场景至关重要。
PodTopologySpread:拓扑打散。看 pod.spec.topologySpreadConstraints,让同一组 Pod(按 labelSelector 圈定)尽量均匀分布在 zone / node / 其他拓扑域里。它是 v1.18 引入的、设计目的就是替代"之前需要手动算反亲和"那段反人类操作。
VolumeBinding:卷绑定。看 Pod 的 PVC 是不是已经绑到 PV 上、绑定的 PV 跟节点拓扑匹不匹配、节点容量是否充足。它是唯一横跨 PreFilter / Filter / Reserve / PreBind 四个阶段的内置插件,因为它要在 PreFilter 阶段做"先假设能绑"(AssumeCache),避免并发调度时一个 PV 被两个 Pod 同时拿走。
💡 小贴士 — 一句话总结:六个插件里,NodeResourcesFit / NodeAffinity / TaintToleration / InterPodAffinity / PodTopologySpread 在 Filter 阶段"判行不行",NodeAffinity / InterPodAffinity / PodTopologySpread / VolumeBinding / NodeResourcesFit 在 Score 阶段"判好不好"。Filter 阶段是"硬过滤",Score 阶段是"软打分" —— 这就是调度器的两阶段决策模型。
早期的 k8s 调度器是"一个大函数":内置几十个 predicate 和 priority 函数,每加一个策略就要改主循环。这带来了三个痛点:1)改一个策略要重新编译整个调度器;2)用户的特殊需求没法动态扩展;3)每个用户场景都要写一份 if-else,代码越来越乱。
从 v1.15 起,调度器重构成了"Scheduler Framework":把"过滤 / 打分 / 绑定"切成固定阶段(我们上一篇讲过的扩展点),每个阶段插拔式挂载插件。内置插件是"出厂预装",第三方可以 OutOfTree 注册。这种设计的最大好处是:六个内置插件互相独立、互不感知、可以单独替换。
传统方案 vs 框架方案:下面这张表把"上一代 monolithic 调度器"和"现在的 Scheduler Framework + 内置插件"做个对比,让你看到为什么 k8s 要这么拆。
| 维度 | 传统 monolithic 调度器(v1.14 及之前) | Scheduler Framework + 内置插件(v1.15+) |
|---|---|---|
| 扩展性 | 改源码 + 重新编译 | 注册自定义插件即可 |
| 代码组织 | 所有 predicate / priority 函数堆在 algorithmprovider / scheduler.go | 每个插件一个目录(pkg/scheduler/framework/plugins/<name>/*.go) |
| 替换单个策略 | 没办法 | 在 SchedulerConfiguration 的 plugins 段禁掉、装自己的 |
| 扩展点(Stage) | 硬编码:预选 → 优选 → 绑定 | 14 个扩展点(PreEnqueue / PreFilter / Filter / PostFilter / ...) |
| 调试难度 | 日志里看不到哪个 predicate 失败 | 每个插件返回的 Status 都带 reason,describe Pod 能看到 |
| 性能 | 所有 predicate 必须跑完才进优选 | 任意插件返回 UnschedulableAndUnresolvable 立刻短路 |
⚠️ 反例提示:不要试图去定制"内置插件的行为"(比如改 NodeResourcesFit 的打分逻辑),正确做法是 disablePlugin: - NodeResourcesFit 后,自己实现一个同名插件 OutOfTree 注册。生产环境禁止直接修改 kube-scheduler 源码,因为这会让集群脱离上游,无法升级。
💡 小贴士
适合用内置插件的场景:1)标准的资源 / 节点 / 污点 / 反亲和需求;2)不想引入第三方依赖;3)希望跟随上游升级。
适合 OutOfTree 的场景:1)有定制打分公式(如自家 GPU 利用率算法);2)需要访问外部业务系统判断;3)需要把外部 HTTP 调度器作为兜底。
这一节我们用一个完整可运行的 Yaml 合集展示六个插件的"最小可触发形态"。你可以把它复制到任意一个测试集群(至少 3 个节点、不同 label / taint),然后用 kubectl apply -f . 逐个验证。
下面的命令基于 k8s v1.36.1,先把节点打好标记和污点,后续 6 个 yaml 会用到这些前置条件。
// 准备命令(k8s v1.36.1)
# 假设有三个节点:node-a(az=cn-east-1,gpu=true),
# node-b(az=cn-east-2,无 gpu),node-c(az=cn-east-3,打了专用污点)
kubectl label nodes node-a az=cn-east-1 node-type=general
kubectl label nodes node-b az=cn-east-2 node-type=general
kubectl label nodes node-c az=cn-east-3 node-type=dedicated
kubectl label nodes node-a nvidia.com/gpu=true
# 给 node-c 打污点,模拟"专用节点"
kubectl taint nodes node-c dedicated=ai:NoSchedule
# 看看节点的 taint / label 是否生效
kubectl describe node node-c | grep -E 'Taints|Labels'
// demo-node-resources-fit.yaml (k8s v1.36.1)
apiVersion: v1
kind: Pod
metadata:
name: big-memory-pod
spec:
containers:
- name: app
image: nginx:1.27
resources:
requests:
memory: "100Gi" # 100Gi 内存,几乎肯定会被 Filter 拒绝
cpu: "2"
执行 kubectl apply -f demo-node-resources-fit.yaml 后,kubectl describe pod big-memory-pod 会看到 FailedScheduling 事件,reason 形如 Insufficient memory,就是 NodeResourcesFit.Filter 抛回来的。
// demo-node-affinity.yaml (k8s v1.36.1)
apiVersion: v1
kind: Pod
metadata:
name: pin-to-east-1
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: az
operator: In
values: ["cn-east-1"]
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 10
preference:
matchExpressions:
- key: nvidia.com/gpu
operator: Exists
containers:
- name: app
image: nginx:1.27
resources:
requests:
cpu: "100m"
memory: "64Mi"
如果你把 required 的 values 改成不存在的 ["xx"],Pod 会 Pending,reason = node(s) didn't match Pod's node affinity/selector。
// demo-taint-toleration.yaml (k8s v1.36.1)
apiVersion: v1
kind: Pod
metadata:
name: dedicated-pod
spec:
tolerations:
- key: dedicated
operator: Equal
value: ai
effect: NoSchedule
containers:
- name: app
image: nginx:1.27
resources:
requests:
cpu: "100m"
memory: "64Mi"
去掉 tolerations 段,Pod 永远跑不到 node-c 上,因为 TaintToleration.Filter 会拒绝它。
// demo-inter-pod-affinity.yaml (k8s v1.36.1)
apiVersion: v1
kind: Pod
metadata:
name: redis-follower
spec:
affinity:
podAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values: ["web"]
topologyKey: kubernetes.io/hostname
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app
operator: In
values: ["redis"]
topologyKey: kubernetes.io/hostname
containers:
- name: redis
image: redis:7.2
先把 web Pod 部署起来后,再 apply 这个 redis,redis 会跟 web 跑在同一节点,并且最好别跟其他 redis 同节点。
// demo-pod-topology-spread.yaml (k8s v1.36.1)
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-spread
spec:
replicas: 3
selector:
matchLabels:
app: web-spread
template:
metadata:
labels:
app: web-spread
spec:
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app: web-spread
containers:
- name: web
image: nginx:1.27
三个副本会尽量被推到三个不同的 AZ 上;如果 whenUnsatisfiable 设为 ScheduleAnyway,则会尽力打散但不强制。
// demo-volume-binding.yaml (k8s v1.36.1)
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: data-pvc
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 10Gi
---
apiVersion: v1
kind: Pod
metadata:
name: stateful-app
spec:
volumes:
- name: data
persistentVolumeClaim:
claimName: data-pvc
containers:
- name: app
image: nginx:1.27
volumeMounts:
- name: data
mountPath: /data
PVC 还没 Bound 时,Pod 会一直 Pending,reason = pod has unbound immediate PersistentVolumeClaims。这就是 VolumeBinding.PreFilter 抛的。
kubectl get pods -o wide 看 Pod 实际被调度到哪个节点kubectl describe pod <name> 在 Events 里找 FailedScheduling 的 reasonkubectl logs -n kube-system kube-scheduler-<node> --tail=200 看调度器端的 reason--v=5 后能看到每个插件的 Filter 返回值✅ 实用技巧:想看 kube-scheduler 调度一个 Pod 时每个插件的具体行为,只需在 SchedulerConfiguration 里加 --v=5 并查看日志。日志会按 "pluginName -> Filter/Score -> reason" 的格式输出,对照源码一行行读非常方便。
这一节是本文的硬核部分。我们顺着"调度流水线触发顺序",把六个内置插件的 Filter / Score / PreFilter / Reserve / PreBind 入口源码全部拆给你看。所有行号都基于 k8s v1.36.1 tag。
📢 阅读源码前的提醒:所有插件的"插件名字"都集中在一个常量文件里:pkg/scheduler/framework/plugins/names/names.go。这是 SchedulerConfiguration 里 disablePlugin 字段的标准字符串来源。先看一眼这个文件能省下大量翻代码的时间。
// pkg/scheduler/framework/plugins/names/names.go (行 19-43, k8s v1.36.1)
const (
PrioritySort = "PrioritySort"
DefaultBinder = "DefaultBinder"
DefaultPreemption = "DefaultPreemption"
DynamicResources = "DynamicResources"
GangScheduling = "GangScheduling"
ImageLocality = "ImageLocality"
InterPodAffinity = "InterPodAffinity"
NodeAffinity = "NodeAffinity"
NodeDeclaredFeatures = "NodeDeclaredFeatures"
NodeName = "NodeName"
NodePorts = "NodePorts"
NodeResourcesBalancedAllocation = "NodeResourcesBalancedAllocation"
NodeResourcesFit = "NodeResourcesFit" // <-- 我们的主角之一
NodeUnschedulable = "NodeUnschedulable"
NodeVolumeLimits = "NodeVolumeLimits"
PodTopologySpread = "PodTopologySpread"
SchedulingGates = "SchedulingGates"
TaintToleration = "TaintToleration"
VolumeBinding = "VolumeBinding"
VolumeRestrictions = "VolumeRestrictions"
VolumeZone = "VolumeZone"
)
这些常量是怎么被引用的?答案是 pkg/scheduler/framework/plugins/registry.go 的 NewInTreeRegistry(),所有内置插件的工厂函数在这里登记,scheduler 启动时按名字找。
// pkg/scheduler/framework/plugins/registry.go (行 50-79, k8s v1.36.1)
func NewInTreeRegistry() runtime.Registry {
fts := plfeature.NewSchedulerFeaturesFromGates(feature.DefaultFeatureGate)
registry := runtime.Registry{
dynamicresources.Name: runtime.FactoryAdapter(fts, dynamicresources.New),
imagelocality.Name: imagelocality.New,
tainttoleration.Name: runtime.FactoryAdapter(fts, tainttoleration.New),
nodename.Name: runtime.FactoryAdapter(fts, nodename.New),
nodeports.Name: runtime.FactoryAdapter(fts, nodeports.New),
nodeaffinity.Name: runtime.FactoryAdapter(fts, nodeaffinity.New),
nodedeclaredfeatures.Name: runtime.FactoryAdapter(fts, nodedeclaredfeatures.New),
podtopologyspread.Name: runtime.FactoryAdapter(fts, podtopologyspread.New),
nodeunschedulable.Name: runtime.FactoryAdapter(fts, nodeunschedulable.New),
noderesources.Name: runtime.FactoryAdapter(fts, noderesources.NewFit),
noderesources.BalancedAllocationName: runtime.FactoryAdapter(fts, noderesources.NewBalancedAllocation),
volumebinding.Name: runtime.FactoryAdapter(fts, volumebinding.New),
volumerestrictions.Name: runtime.FactoryAdapter(fts, volumerestrictions.New),
volumezone.Name: runtime.FactoryAdapter(fts, volumezone.New),
nodevolumelimits.CSIName: runtime.FactoryAdapter(fts, nodevolumelimits.NewCSI),
interpodaffinity.Name: runtime.FactoryAdapter(fts, interpodaffinity.New),
queuesort.Name: queuesort.New,
defaultbinder.Name: defaultbinder.New,
defaultpreemption.Name: runtime.FactoryAdapter(fts, defaultpreemption.New),
schedulinggates.Name: runtime.FactoryAdapter(fts, schedulinggates.New),
// ... 还有 gangscheduling / topologyaware 等
}
return registry
}
📅 版本更新:从 v1.15 起,scheduler 改为 Framework + 内置插件设计;到 v1.36.1,注册表已扩展到 22 个内置插件(不含我们今天没讲的 nodelabel、noderesources.MostAllocated 等)。所有插件都通过 runtime.FactoryAdapter 适配,以拿到 feature gate。
NodeResourcesFit 的源码在 pkg/scheduler/framework/plugins/noderesources/ 下,对应 Plugin 结构体叫 Fit。它实现了 fwk.PreFilterPlugin + FilterPlugin + ScorePlugin 三个扩展点。
// pkg/scheduler/framework/plugins/noderesources/fit.go (行 92-105, k8s v1.36.1)
// Fit is a plugin that checks if a node has sufficient resources.
type Fit struct {
ignoredResources sets.Set[string]
ignoredResourceGroups sets.Set[string]
enableInPlacePodVerticalScaling bool
enableSidecarContainers bool
enableSchedulingQueueHint bool
enablePodLevelResources bool
enableDRAExtendedResource bool
enableInPlacePodLevelResourcesVerticalScaling bool
handle fwk.Handle
*resourceAllocationScorer // <-- 嵌入打分器
placementScorer *resourceAllocationScorer
}
字段精讲:ignoredResources 是用户配置的"不算资源的资源名"(比如 kube.example.com/foo);enableSidecarContainers 决定能不能容忍 restartable init container(v1.28+ 默认开,v1.36.1 已 GA)。
// pkg/scheduler/framework/plugins/noderesources/fit.go (行 612-648, k8s v1.36.1)
// Filter invoked at the filter extension point.
// Checks if a node has sufficient resources, such as cpu, memory, gpu, opaque int resources etc to run a pod.
func (f *Fit) Filter(ctx context.Context, cycleState fwk.CycleState, pod *v1.Pod, nodeInfo fwk.NodeInfo) *fwk.Status {
s, err := getPreFilterState(cycleState)
if err != nil {
return fwk.AsStatus(err)
}
// ... 略:draManager / opts 装配
insufficientResources := fitsRequest(s, nodeInfo, f.ignoredResources, f.ignoredResourceGroups, draManager, opts)
if len(insufficientResources) != 0 {
// 收集所有失败原因,区分 Unschedulable / UnschedulableAndUnresolvable
failureReasons := make([]string, 0, len(insufficientResources))
statusCode := fwk.Unschedulable
for i := range insufficientResources {
failureReasons = append(failureReasons, insufficientResources[i].Reason)
if insufficientResources[i].Unresolvable {
statusCode = fwk.UnschedulableAndUnresolvable
}
}
return fwk.NewStatus(statusCode, failureReasons...)
}
return nil
}
关键点:s 是 PreFilter 阶段写进 CycleState 的 preFilterState,里面装着 Pod 的总资源请求。这是 Filter 阶段不重复计算的关键。
// pkg/scheduler/framework/plugins/noderesources/fit.go (行 321-331, k8s v1.36.1)
func computePodResourceRequest(pod *v1.Pod, opts ResourceRequestsOptions) *preFilterState {
// pod hasn't scheduled yet so we don't need to worry about InPlacePodVerticalScalingEnabled
reqs := resource.PodRequests(pod, resource.PodResourcesOptions{
SkipPodLevelResources: !opts.EnablePodLevelResources,
UseDRANodeAllocatableResourceClaimStatus: opts.EnableDRANodeAllocatableResources,
})
result := &preFilterState{}
result.SetMaxResource(reqs)
return result
}
这条注释很有意思:"pod hasn't scheduled yet so we don't need to worry about InPlacePodVerticalScalingEnabled",翻译过来就是"Pod 还没调度,所以不需要考虑就地垂直扩缩容"。这是 InitContainer 资源计算的最大值原则(取 max(request, limit) of all init containers + sum of regular containers)。
// pkg/scheduler/framework/plugins/noderesources/fit.go (行 767-786, k8s v1.36.1)
// Score invoked at the Score extension point.
func (f *Fit) Score(ctx context.Context, state fwk.CycleState, pod *v1.Pod, nodeInfo fwk.NodeInfo) (int64, *fwk.Status) {
s, err := getPreScoreState(state)
if err != nil {
s = &preScoreState{
podRequests: f.calculatePodResourceRequestList(pod, f.resources),
}
if f.enableDRAExtendedResource {
// ... DRA 场景回填
}
}
return f.score(ctx, pod, nodeInfo, s.podRequests, s.draPreScoreState)
}
Fit 的打分"招数":f.score 内部分四种策略,最常用的 LeastAllocated(默认)公式是 score = (capacity - requested) * MaxNodeScore / capacity —— 越闲的节点得分越高。
NodeAffinity 同时实现 PreFilter / Filter / PreScore / Score,是"打分 + 过滤"双修插件。核心源码:pkg/scheduler/framework/plugins/nodeaffinity/node_affinity.go。
// pkg/scheduler/framework/plugins/nodeaffinity/node_affinity.go (行 38-51, k8s v1.36.1)
// NodeAffinity is a plugin that checks if a pod node selector matches the node label.
type NodeAffinity struct {
handle fwk.Handle
addedNodeSelector *nodeaffinity.NodeSelector // <-- 集群级强制亲和
addedPrefSchedTerms *nodeaffinity.PreferredSchedulingTerms // <-- 集群级偏好
enableSchedulingQueueHint bool
}
var _ fwk.PreFilterPlugin = &NodeAffinity{}
var _ fwk.FilterPlugin = &NodeAffinity{}
var _ fwk.PreScorePlugin = &NodeAffinity{}
var _ fwk.ScorePlugin = &NodeAffinity{}
var _ fwk.EnqueueExtensions = &NodeAffinity{}
var _ fwk.SignPlugin = &NodeAffinity{}
注意 addedNodeSelector 字段 —— 它来自 SchedulerConfiguration 的 AddedAffinity,是"集群管理员强制追加"的节点选择器,不写在 Pod spec 里也能生效。比如强制让所有 Pod 都必须能跑在 SSD 节点上。
// pkg/scheduler/framework/plugins/nodeaffinity/node_affinity.go (行 269-297, k8s v1.36.1)
// Score returns the sum of the weights of the terms that match the Node.
func (pl *NodeAffinity) Score(ctx context.Context, state fwk.CycleState, pod *v1.Pod, nodeInfo fwk.NodeInfo) (int64, *fwk.Status) {
node := nodeInfo.Node()
var count int64
if pl.addedPrefSchedTerms != nil {
count += pl.addedPrefSchedTerms.Score(node)
}
s, err := getPreScoreState(state)
if err != nil {
// PreScore 没跑的兜底:现场算一遍
preferredNodeAffinity, err := getPodPreferredNodeAffinity(pod)
if err != nil {
return 0, fwk.AsStatus(err)
}
s = &preScoreState{preferredNodeAffinity: preferredNodeAffinity}
}
if s.preferredNodeAffinity != nil {
count += s.preferredNodeAffinity.Score(node)
}
return count, nil
}
打分逻辑很直白:把所有 preferred 偏好项里"命中当前节点"的权重加起来。所以 weight 越大,越倾向被选中。
TaintToleration 是 k8s 里节点"专用 / 隔离"场景的核心。源码在 pkg/scheduler/framework/plugins/tainttoleration/taint_toleration.go。
// pkg/scheduler/framework/plugins/tainttoleration/taint_toleration.go (行 34-54, k8s v1.36.1)
// TaintToleration is a plugin that checks if a pod tolerates a node's taints.
type TaintToleration struct {
handle fwk.Handle
enableSchedulingQueueHint bool
enableTaintTolerationComparisonOperators bool
}
var _ fwk.FilterPlugin = &TaintToleration{}
var _ fwk.PreScorePlugin = &TaintToleration{}
var _ fwk.ScorePlugin = &TaintToleration{}
var _ fwk.EnqueueExtensions = &TaintToleration{}
var _ fwk.SignPlugin = &TaintToleration{}
const (
Name = names.TaintToleration
preScoreStateKey = "PreScore" + Name
ErrReasonNotMatch = "node(s) had taints that the pod didn't tolerate"
)
// pkg/scheduler/framework/plugins/tainttoleration/taint_toleration.go (行 118-132, k8s v1.36.1)
// Filter invoked at the filter extension point.
func (pl *TaintToleration) Filter(ctx context.Context, state fwk.CycleState, pod *v1.Pod, nodeInfo fwk.NodeInfo) *fwk.Status {
logger := klog.FromContext(ctx)
node := nodeInfo.Node()
taint, isUntolerated := v1helper.FindMatchingUntoleratedTaint(logger, node.Spec.Taints, pod.Spec.Tolerations,
helper.DoNotScheduleTaintsFilterFunc(),
pl.enableTaintTolerationComparisonOperators)
if !isUntolerated {
return nil
}
logger.V(4).Info("node had untolerated taints", "node", klog.KObj(node), "pod", klog.KObj(pod), "untoleratedTaint", taint)
return fwk.NewStatus(fwk.UnschedulableAndUnresolvable, "node(s) had untolerated taint(s)")
}
核心调用是 v1helper.FindMatchingUntoleratedTaint,它遍历节点污点、查 Pod 容忍列表,判断是否存在"无容忍的 NoSchedule / NoExecute 污点"。如果存在,直接返回 UnschedulableAndUnresolvable —— 注意这里是"unresolvable",因为单靠这个 Pod 没法解决,必须 node 打 taint 才行。
// pkg/scheduler/framework/plugins/tainttoleration/taint_toleration.go (行 194-212, k8s v1.36.1)
// Score invoked at the Score extension point.
func (pl *TaintToleration) Score(ctx context.Context, state fwk.CycleState, pod *v1.Pod, nodeInfo fwk.NodeInfo) (int64, *fwk.Status) {
logger := klog.FromContext(ctx)
node := nodeInfo.Node()
s, err := getPreScoreState(state)
if err != nil {
return 0, fwk.AsStatus(err)
}
score := int64(pl.countIntolerableTaintsPreferNoSchedule(logger, node.Spec.Taints, s.tolerationsPreferNoSchedule))
return score, nil
}
// NormalizeScore invoked after scoring all nodes.
func (pl *TaintToleration) NormalizeScore(ctx context.Context, _ fwk.CycleState, pod *v1.Pod, scores fwk.NodeScoreList) *fwk.Status {
return helper.DefaultNormalizeScore(fwk.MaxNodeScore, true, scores)
}
打分只针对 PreferNoSchedule 效果:节点上"无法容忍的 PreferNoSchedule 污点"越多,得分越低(因为打 0 分表示最不想要),最后用 DefaultNormalizeScore(MaxNodeScore, true, scores) 把分数规范化到 [0, MaxNodeScore]。第二个参数是 reverse,因为"污点越多越不想去",所以要反过来。
InterPodAffinity 看的不只是当前 Pod,还要扫描整个集群的所有 Pod 看"邻里关系"。它在 PreFilter 阶段就一次性把所有 Pod 的亲和 / 反亲和计数算好,避免在 Filter 阶段重复扫集群。
// pkg/scheduler/framework/plugins/interpodaffinity/plugin.go (行 46-58, k8s v1.36.1)
// InterPodAffinity is a plugin that checks inter pod affinity
type InterPodAffinity struct {
parallelizer fwk.Parallelizer
args config.InterPodAffinityArgs
sharedLister fwk.SharedLister
nsLister listersv1.NamespaceLister
enableSchedulingQueueHint bool
}
var _ fwk.PreFilterPlugin = &InterPodAffinity{}
var _ fwk.FilterPlugin = &InterPodAffinity{}
var _ fwk.PreScorePlugin = &InterPodAffinity{}
var _ fwk.ScorePlugin = &InterPodAffinity{}
var _ fwk.EnqueueExtensions = &InterPodAffinity{}
var _ fwk.SignPlugin = &InterPodAffinity{}
注意 args.IgnorePreferredTermsOfExistingPods,这是一个性能开关:开启后,只考虑"带 affinity 配置"的 Pod 之间的关系,忽略"existing pods"的 preferred 反亲和。可以让 PreFilter 在大集群下提速数倍。
// pkg/scheduler/framework/plugins/interpodaffinity/filtering.go (行 411-432, k8s v1.36.1)
// Filter invoked at the filter extension point.
// It checks if a pod can be scheduled on the specified node with pod affinity/anti-affinity configuration.
func (pl *InterPodAffinity) Filter(ctx context.Context, cycleState fwk.CycleState, pod *v1.Pod, nodeInfo fwk.NodeInfo) *fwk.Status {
state, err := getPreFilterState(cycleState)
if err != nil {
return fwk.AsStatus(err)
}
if !satisfyPodAffinity(state, nodeInfo) {
return fwk.NewStatus(fwk.UnschedulableAndUnresolvable, ErrReasonAffinityRulesNotMatch)
}
if !satisfyPodAntiAffinity(state, nodeInfo) {
return fwk.NewStatus(fwk.Unschedulable, ErrReasonAntiAffinityRulesNotMatch)
}
if !satisfyExistingPodsAntiAffinity(state, nodeInfo) {
return fwk.NewStatus(fwk.Unschedulable, ErrReasonExistingAntiAffinityRulesNotMatch)
}
return nil
}
3 个判断的差别:
• satisfyPodAffinity:本 Pod 的 required 亲和是否满足 —— unresolvable(不在错误节点就不能满足,必须扩容节点)
• satisfyPodAntiAffinity:本 Pod 的 required 反亲和 —— 节点上恰好有 Pod"它不想靠近",可调度时再挪
• satisfyExistingPodsAntiAffinity:集群里已有的 Pod 对本 Pod 的反亲和 —— 同上
PodTopologySpread 的核心算法是 "MaxSkew" 概念:任意两个拓扑域之间 Pod 数量差的最大值。maxSkew 字段就是用来限制这个偏差的。
// pkg/scheduler/framework/plugins/podtopologyspread/plugin.go (行 59-83, k8s v1.36.1)
// PodTopologySpread is a plugin that ensures pod's topologySpreadConstraints is satisfied.
type PodTopologySpread struct {
systemDefaulted bool
parallelizer fwk.Parallelizer
defaultConstraints []v1.TopologySpreadConstraint
sharedLister fwk.SharedLister
services corelisters.ServiceLister
replicationCtrls corelisters.ReplicationControllerLister
replicaSets appslisters.ReplicaSetLister
statefulSets appslisters.StatefulSetLister
enableNodeInclusionPolicyInPodTopologySpread bool
enableMatchLabelKeysInPodTopologySpread bool
enableSchedulingQueueHint bool
enableTaintTolerationComparisonOperators bool
}
var _ fwk.PreFilterPlugin = &PodTopologySpread{}
var _ fwk.FilterPlugin = &PodTopologySpread{}
var _ fwk.PreScorePlugin = &PodTopologySpread{}
var _ fwk.ScorePlugin = &PodTopologySpread{}
var _ fwk.EnqueueExtensions = &PodTopologySpread{}
var _ fwk.SignPlugin = &PodTopologySpread{}
"systemDefaulted" 字段是关键:当 SchedulerConfiguration 把 DefaultingType 设为 SystemDefaulting,集群里没配 spreadConstraints 的 Service/RC/RS/StatefulSet 都会"自动被套上"两个默认约束(hostname skew=3、zone skew=5)。这就是为什么新建 Deployment 不写 spread 也能打散。
// pkg/scheduler/framework/plugins/podtopologyspread/plugin.go (行 46-57, k8s v1.36.1)
var systemDefaultConstraints = []v1.TopologySpreadConstraint{
{
TopologyKey: v1.LabelHostname, // kubernetes.io/hostname
WhenUnsatisfiable: v1.ScheduleAnyway,
MaxSkew: 3,
},
{
TopologyKey: v1.LabelTopologyZone, // topology.kubernetes.io/zone
WhenUnsatisfiable: v1.ScheduleAnyway,
MaxSkew: 5,
},
}
// pkg/scheduler/framework/plugins/podtopologyspread/scoring.go (行 196-226, k8s v1.36.1)
// Score invoked at the Score extension point.
func (pl *PodTopologySpread) Score(ctx context.Context, cycleState fwk.CycleState, pod *v1.Pod, nodeInfo fwk.NodeInfo) (int64, *fwk.Status) {
node := nodeInfo.Node()
s, err := getPreScoreState(cycleState)
if err != nil {
return 0, fwk.AsStatus(err)
}
if s.IgnoredNodes.Has(node.Name) {
return 0, nil
}
var score float64
for i, c := range s.Constraints {
if tpVal, ok := node.Labels[c.TopologyKey]; ok {
var cnt int64
if c.TopologyKey == v1.LabelHostname {
cnt = int64(countPodsMatchSelector(nodeInfo.GetPods(), c.Selector, pod.Namespace))
} else {
cnt = *s.TopologyValueToPodCounts[i][tpVal]
}
score += scoreForCount(cnt, c.MaxSkew, s.TopologyNormalizingWeight[i])
}
}
return int64(math.Round(score)), nil
}
打分公式:scoreForCount(cnt, maxSkew, tpWeight) = cnt * tpWeight + (maxSkew - 1)。cnt 是当前节点上拓扑域内匹配的 Pod 数,越少越好;加 maxSkew - 1 是为了让"小 maxSkew"的约束有权重优势。最终 Score 和 NormalizeScore 配合后,节点越空越容易被选中。
VolumeBinding 是六个内置插件里"生命周期最长"的,因为它要在 PreFilter / Filter / Reserve / PreBind 四个阶段协同工作。核心机制是 AssumeCache:在 PreFilter 阶段"先假设能绑",Filter 阶段检查 PV 节点亲和 / 容量,Reserve 阶段"占位",PreBind 阶段"提交真实绑定"。
// pkg/scheduler/framework/plugins/volumebinding/volume_binding.go (行 70-92, k8s v1.36.1)
// VolumeBinding is a plugin that binds pod volumes in scheduling.
// In the Filter phase, pod binding cache is created for the pod and used in
// Reserve and PreBind phases.
type VolumeBinding struct {
Binder SchedulerVolumeBinder
PVCLister corelisters.PersistentVolumeClaimLister
classLister storagelisters.StorageClassLister
scorer volumeCapacityScorer
fts feature.Features
}
var _ fwk.PreFilterPlugin = &VolumeBinding{}
var _ fwk.FilterPlugin = &VolumeBinding{}
var _ fwk.ReservePlugin = &VolumeBinding{}
var _ fwk.PreBindPlugin = &VolumeBinding{}
var _ fwk.PreScorePlugin = &VolumeBinding{}
var _ fwk.ScorePlugin = &VolumeBinding{}
var _ fwk.EnqueueExtensions = &VolumeBinding{}
var _ fwk.SignPlugin = &VolumeBinding{}
// pkg/scheduler/framework/plugins/volumebinding/volume_binding.go (行 357-389, k8s v1.36.1)
// PreFilter invoked at the prefilter extension point to check if pod has all
// immediate PVCs bound. If not all immediate PVCs are bound, an
// UnschedulableAndUnresolvable is returned.
func (pl *VolumeBinding) PreFilter(ctx context.Context, state fwk.CycleState, pod *v1.Pod, nodes []fwk.NodeInfo) (*fwk.PreFilterResult, *fwk.Status) {
logger := klog.FromContext(ctx)
if hasPVC, err := pl.podHasPVCs(pod); err != nil {
return nil, fwk.NewStatus(fwk.UnschedulableAndUnresolvable, err.Error())
} else if !hasPVC {
state.Write(stateKey, &stateData{})
return nil, fwk.NewStatus(fwk.Skip)
}
podVolumeClaims, err := pl.Binder.GetPodVolumeClaims(logger, pod)
if err != nil {
return nil, fwk.AsStatus(err)
}
if len(podVolumeClaims.unboundClaimsImmediate) > 0 {
status := fwk.NewStatus(fwk.UnschedulableAndUnresolvable)
status.AppendReason("pod has unbound immediate PersistentVolumeClaims")
return nil, status
}
state.Write(stateKey, &stateData{
podVolumesByNode: make(map[string]*PodVolumes),
// ... 初始化状态
})
return nil, nil
}
UnschedulableAndUnresolvable 的精妙:PreFilter 阶段如果发现 Pod 有 immediate PVC 但还没 Bound,返回的是 UnschedulableAndUnresolvable,意思是"调度器解决不了,等 PV controller 把 PVC Bound 了再试"。调度框架会把这个 Pod 放到 unschedulable queue,等到 PVC Bound 事件触发后再次入队(这是 EventsToRegister 的功劳)。
// pkg/scheduler/framework/plugins/volumebinding/volume_binding.go (行 470-523, k8s v1.36.1)
// Score invoked at the score extension point.
func (pl *VolumeBinding) Score(ctx context.Context, cs fwk.CycleState, pod *v1.Pod, nodeInfo fwk.NodeInfo) (int64, *fwk.Status) {
if pl.scorer == nil {
return 0, nil
}
state, err := getStateData(cs)
if err != nil {
return 0, fwk.AsStatus(err)
}
nodeName := nodeInfo.Node().Name
podVolumes, ok := state.podVolumesByNode[nodeName]
if !ok {
return 0, nil
}
classResources := make(classResourceMap)
if len(podVolumes.StaticBindings) != 0 || !pl.fts.EnableStorageCapacityScoring {
// 静态绑定:group static binding volumes by storage class
for _, staticBinding := range podVolumes.StaticBindings {
// ...
}
} else {
// 动态绑定:group dynamic binding volumes by storage class
for _, provision := range podVolumes.DynamicProvisions {
// ... 注意这里 Capacity 不能用 +=!
}
}
return pl.scorer(classResources), nil
}
打分公式:VolumeBinding 的打分目标是"节点上某种 StorageClass 的剩余容量越多,得分越高"。它采用 broken linear function,参数由 StorageClass.VolumeBindingMode 和 SchedulerConfiguration 共同决定。需要打开 --feature-gates=StorageCapacityScoring=true 才生效(v1.36.1 仍是 alpha)。
// pkg/scheduler/framework/plugins/volumebinding/scorer.go (行 28-54, k8s v1.36.1)
// volumeCapacityScorer calculates the score based on class storage resource information.
type volumeCapacityScorer func(classResourceMap) int64
// buildScorerFunction builds volumeCapacityScorer from the scoring function shape.
func buildScorerFunction(scoringFunctionShape helper.FunctionShape) volumeCapacityScorer {
rawScoringFunction := helper.BuildBrokenLinearFunction(scoringFunctionShape)
f := func(requested, capacity int64) int64 {
if capacity == 0 || requested > capacity {
return rawScoringFunction(maxUtilization)
}
return rawScoringFunction(requested * maxUtilization / capacity)
}
return func(classResources classResourceMap) int64 {
var nodeScore int64
weightSum := len(classResources)
if weightSum == 0 {
return 0
}
for _, resource := range classResources {
classScore := f(resource.Requested, resource.Capacity)
nodeScore += classScore
}
return int64(math.Round(float64(nodeScore) / float64(weightSum)))
}
}
注意 rawScoringFunction(requested * maxUtilization / capacity):把请求量/容量比压缩成 0-100 的整数,再丢给 broken linear function 做分段线性映射。设计目的:当容量满的时候,得分要明显衰减。
这一节汇总六个内置插件在生产环境最常见的"翻车现场"。每个坑都有具体现象、根因、排查命令和修复方案。建议至少读两遍。
⛔ 生产环境禁止操作
以下操作都属于"会让整个集群调度停摆"的级别:
• 在所有节点上 kubectl taint nodes --all key=value:NoSchedule
• 在 SchedulerConfiguration 里同时禁用 NodeResourcesFit 和 InterPodAffinity 后还启用了大量 Pod
• 删除 kube-system 命名空间下的 Pod(影响 k8s 自身组件调度)
现象:Pod 内存用了 256Mi,但 requests 写的是 512Mi,Pod 一直 Pending,describe 显示 0/N nodes are available: insufficient memory。
根因:NodeResourcesFit 在 PreFilter 阶段调用 resource.PodRequests,对 InitContainer 取 max(request, limit)。如果你没写 limit,就只算 request;如果 request 超过了节点 allocatable,就会被 Filter 拒绝。
修复:调小 request 到合理值,或者扩节点,或者使用 ResourceQuota 控制 namespace。
现象:Deployment 部署后 Pod 一直 Pending,reason 是 node(s) didn't match Pod's node affinity/selector。但用 kubectl get node --show-labels 明明能看到那个 label。
根因:YAML 里把 az 写成 AZ,label key 是大小写敏感的。
修复:用 kubectl get pod -o yaml | grep -A5 affinity 对比节点 label 精确修复。
现象:某次给节点打了 dedicated=ml:NoSchedule 污点做 AI 专用节点,但通用 Pod 全跑不上去。
根因:TaintToleration.Filter 调用 v1helper.FindMatchingUntoleratedTaint,因为 Pod spec.tolerations 为空,直接返回 UnschedulableAndUnresolvable。
修复:通用 Pod 加容忍 tolerations: [{operator: Exists}](容忍所有污点);或者用默认 PodTemplate 加在 cluster-wide PodPreset / mutating webhook。
现象:3000 节点集群,Deployment 滚动更新时 Pod 调度 P99 延迟从 50ms 飙升到 2s。
根因:PodTopologySpread 和 InterPodAffinity 都要遍历整个集群 Pod 数才能算出拓扑分布。两个一起开,PreFilter 阶段 O(N) 重复扫描。
修复:1)开 --feature-gates=SchedulerQueueingHints=true(v1.27+ 默认开);2)InterPodAffinityArgs.IgnorePreferredTermsOfExistingPods=true 关闭扫描 existing pods 的 preferred 项。
现象:快速循环创建删除 Pod 时,发现有些 PV 已经被"假设绑定"给被删除的 Pod,导致后续 Pod 卡 Pending。
根因:VolumeBinding 在 Reserve 阶段通过 AssumeCache 占位。如果 Pod 在 Permit 阶段被拒(被 kube-scheduler 决定不调度),Reserve 没被 Unreserve,AssumeCache 一直占着 PV。
修复:升级到 v1.30+;在 Reserve 阶段失败时触发 Unreserve,社区已经在 v1.29 修复了大部分 race condition。
现象:开发者写了个 OutOfTree 插件叫 NodeResourcesFit 想要替换内置,调度器启动直接 panic。
根因:注册表按名字去重,重名直接 panic。
修复:正确做法是先在 SchedulerConfiguration 的 plugins 段 disablePlugin: [{name: NodeResourcesFit}],再用 MultiPointPlugin.MyCustomFit 重新挂载。
✅ 预防建议:1)任何自定义插件都加公司前缀(如 company.com/MyFit);2)修改 SchedulerConfiguration 之前先备份 + dry-run;3)每个内置插件的 EventsToRegister 决定了 informer 压力,禁用前评估集群规模。
这一节用红问绿答的格式整理 22 个最常被问到的细节问题,分类如下:基础概念 5 题、源码原理 8 题、生产实践 9 题。
Question 1: Filter 和 Score 到底有什么区别? Answer:Filter 是"准入判决",每个插件返回一个 *Status:nil 表示通过,非 nil 表示拒绝。返回非 nil 的瞬间,调度器会把节点从候选列表里删掉,后续不会再处理这个节点。Score 是"选美比赛",所有候选节点都跑完所有 Score 插件后,把每个插件的打分按权重相加,分最高的胜出。源码里所有 Filter 都返回 *fwk.Status,所有 Score 都返回 (int64, *fwk.Status) 这样的元组。
Question 2: Unschedulable 和 UnschedulableAndUnresolvable 有什么不同? Answer:Unschedulable 表示"暂时调度不了但等集群变化后可能可以",Pod 会被丢进 unschedulable queue 等待下次重试;UnschedulableAndUnresolvable 表示"调度器自己解决不了",Pod 也进 unschedulable queue,但只有"能改变调度结果的集群事件"才会触发重试。比如 PVC 没 Bound(VolumeBinding.PreFilter 返回的)就是 Unresolvable,必须等 PV controller 把它 Bound。
Question 3: Plugin、ExtensionPoint、Stage 三个概念怎么区分? Answer:Plugin 是实现 fwk.FilterPlugin / fwk.ScorePlugin 等接口的具体结构体(比如 TaintToleration)。ExtensionPoint 是扩展点(比如 Filter 本身),是调度流水线上的"插槽"。Stage 是扩展点所属的阶段(QueueSort / Filtering / Scoring / Binding 等)。一个 Plugin 可以同时实现多个 ExtensionPoint(比如 NodeAffinity 同时是 PreFilterPlugin / FilterPlugin / PreScorePlugin / ScorePlugin)。
Question 4: 同一个扩展点上能挂多个插件吗?执行顺序是怎样的? Answer:可以。Filter 扩展点上能挂任意多个,调度器按 SchedulerConfiguration 里 Enabled 的顺序串行执行。但只要任意一个插件返回 Unschedulable,整个 Filter 阶段就短路,后续插件不再跑。Score 扩展点上的所有插件都会被调用,分数按 pluginConfig.weight 加权求和。
Question 5: 怎么自定义插件的名字才不会被 kube-scheduler 拒绝?
Answer:插件名字是大小写敏感的字符串。内置插件名字在 pkg/scheduler/framework/plugins/names/names.go 里。自定义 OutOfTree 插件不能和内置的 22 个名字冲突,建议加 company.example.com/MyPlugin 这种带域名前缀的命名。注册表 NewInTreeRegistry 里是按 string key 去重的,重名直接 panic。
Question 6: NodeResourcesFit 的资源计算是取 requests 还是 limits? Answer:取 requests。但对 InitContainer 取 max(request, limit)。这是 v1.26 之后固定的行为,目的是保证调度器算出来的资源永远不小于 kubelet 实际需要的。这是 InitContainer "最大资源原则"在调度侧的体现。
Question 7: NodeAffinity 集群级 AddedAffinity 和 Pod 内 nodeAffinity 谁优先? Answer:两者是 AND 关系。调度器会先校验集群级的 addedNodeSelector(不匹配直接出局),再校验 Pod 内 required(不匹配出局),最后把 addedPrefSchedTerms 和 Pod 内 preferred 一起加权打分。所以集群管理员可以通过 AddedAffinity 给所有 Pod 强制加约束,Pod 作者可以追加自己的偏好。
Question 8: TaintToleration 在打分阶段为什么只算 PreferNoSchedule 效果? Answer:因为 NoSchedule 已经在 Filter 阶段拒了,NoExecute 会被 eviction controller 处理。只有 PreferNoSchedule 是"能跑但最好不跑"的"软条件",所以 TaintToleration.Score 调 countIntolerableTaintsPreferNoSchedule 计算污点数。NormalizeScore 第二参数 reverse=true 表示"污点越多分越低"。
Question 9: InterPodAffinity 在 PreFilter 阶段具体算的是什么? Answer:它对所有现有 Pod 跑一遍 affinity / antiAffinity 匹配,把匹配计数存到 preFilterState 的 affinityCounts / antiAffinityCounts / existingAntiAffinityCounts 三个 map 里。map 的 key 是 topologyPair(key + value),value 是计数。Filter 阶段直接读 map 判定节点是不是满足约束,避免每次都扫全集群。这是 v1.18+ 才有的优化。
Question 10: PodTopologySpread 的 maxSkew 到底是什么意思? Answer:maxSkew 是"任意两个拓扑域之间 Pod 数量差的最大值"。比如三个 zone 各跑了 5 个 Pod,那 skew = 0;如果一个 zone 7 个、另外两个 5 个,skew = 2。maxSkew: 1 表示最多差 1。filter 阶段如果 skew > maxSkew + 当下拓扑 Pod 数,Pod 就放不上去。
Question 11: PodTopologySpread 的 defaultConstraints 何时生效? Answer:在 SchedulerConfiguration 把 DefaultingType 设为 SystemDefaulting 时生效。系统默认两个约束:hostname skew=3 + zone skew=5,只针对 Service / RC / RS / StatefulSet 创建的 Pod 生效(不带 spreadConstraints 的)。Deployment 创建的 Pod 不在 default 范围。这就是 v1.25 之后默认行为。
Question 12: VolumeBinding 的 AssumeCache 是怎么避免并发竞争的? Answer:AssumeCache 是一个 in-memory cache,模拟"如果 Pod 被调度到这个节点上,PV 会被怎么绑定"。在 PreFilter 阶段 GetPodVolumeClaims 时直接给每个节点算好"假设绑定"的结果。Filter 阶段直接读这个快照。Reserve 阶段"占位"、PreBind 阶段把快照变成真实绑定。这样多个调度 cycle 并发跑也不会让同一个 PV 被两个 Pod 同时绑定。
Question 13: VolumeBinding 的 StorageCapacityScoring 是什么?
Answer:v1.33+ 引入的 alpha 特性,开启后 VolumeBinding 会参与 Score 阶段打分,分数取决于节点剩余 StorageClass 容量。需要 --feature-gates=StorageCapacityScoring=true + StorageClass 的 VolumeBindingMode=WaitForFirstConsumer。本文对应源码在 scorer.go 的 buildScorerFunction。
Question 14: EventsToRegister 这个方法干什么用? Answer:它告诉 scheduler 的 SchedulingQueue:"如果你监听到这些 informer 事件,unschedulable queue 里的某个 Pod 现在可能可调度了,把它挪回 active queue"。比如 TaintToleration 监听 Node Add/UpdateNodeTaint,VolumeBinding 监听 StorageClass/PVC/PV 变化。没有 EventsToRegister,Pod 会等到主动重试才再次入队。
Question 15: 怎么查看当前 kube-scheduler 启用了哪些插件?
Answer:两种方法:1)kubectl describe configmap -n kube-system scheduler-config 看完整配置;2)kubectl logs -n kube-system kube-scheduler-xxx | grep -E 'enabled plugins' 看启动日志。生产环境更推荐看 configmap,因为它记录了真实生效的版本。
Question 16: 禁用某个内置插件会有什么副作用? Answer:在 SchedulerConfiguration plugins.disablePlugin 里加插件名字即可。副作用就是该插件负责的检查 / 打分不再生效:禁用 NodeResourcesFit 意味着不检查资源,Pod 可能 OOMKill;禁用 TaintToleration 意味着污点不再隔离节点;禁用 PodTopologySpread 意味着 Pod 不会被打散。每加一条都要明确知道失去了什么。
Question 17: 我能不能既用 NodeResourcesFit 又用 NodeResourcesBalancedAllocation? Answer:可以,而且通常推荐。它们两个名字不同(NodeResourcesFit / NodeResourcesBalancedAllocation),分别注册在 registry.go 第 62 和 63 行。前者负责 Filter + LeastAllocated 打分,后者负责 BalancedAllocation 打分。同时启用后,分数会累加。如果你只想要 LeastAllocated 而不要 BalancedAllocation,可以只禁用第二个。
Question 18: 自定义 OutOfTree 插件怎么打包进 kube-scheduler 二进制?
Answer:两种方式:1)独立调度器二进制:用 k8s.io/kubernetes/cmd/kube-scheduler + go build 出独立二进制,跑在集群里。这是最常见、最干净的方式。2)Pluggable scheduler:v1.18+ 支持用 --config 指定 SchedulerConfiguration,加 --out-of-tree-plugin-registrations 在加载新插件文件。注意 OutOfTree 不能改内置插件行为。
Question 19: SchedulerConfiguration 改了之后怎么应用?需要重启 kube-scheduler 吗? Answer:需要重启。kube-scheduler 启动时一次性把 SchedulerConfiguration 读到内存,运行期间不会热加载。生产环境推荐做法:先滚动更新 kube-scheduler(DaemonSet 模式),用 SLO 监控调度延迟再全量切。可以先用 staging kube-scheduler 验证配置正确性。
Question 20: 怎么定位"某个具体插件"导致的调度失败?
Answer:三步:1)kubectl describe pod <name> 看 Events 里的 FailedScheduling,reason 里会带插件名(比如 "node(s) didn't match Pod's node affinity/selector" 是 NodeAffinity,"node(s) had taints that the pod didn't tolerate" 是 TaintToleration)。2)打开 --v=5 看 kube-scheduler 日志,每行 Filter 会打印 pluginName + reason。3)用 kubectl get events --field-selector reason=FailedScheduling 拿所有失败调度记录。
Question 21: k8s v1.36.1 跟早期版本相比,调度器最重要的变化是什么? Answer:三层变化:1)DRA(Dynamic Resource Allocation):v1.30 GA,DynamicResources 插件处理 ResourceClaim;2)SchedulingQueueHints:v1.27 beta,v1.28 GA,用 QueueingHint 减少 informer 压力;3)TaintTolerationComparisonOperators:v1.27 alpha,toleration 支持 In/NotIn/Exists/DoesNotExist 操作符;4)MatchLabelKeysInPodTopologySpread:v1.27 alpha,让 topologySpreadConstraints 支持 labelKey 匹配。
Question 22: 学习源码应该先读哪个内置插件? Answer:推荐顺序:NodeName(最简单)→ NodeResourcesFit(Filter + Score 经典套路)→ NodeAffinity(涉及 PreFilter 优化)→ TaintToleration(事件驱动)→ PodTopologySpread(算法最复杂)→ InterPodAffinity(性能坑最多)→ VolumeBinding(生命周期最长)。每个插件控制在 200 行内能读完 —— 调度器源码的"亲切度"远比想象中好。
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。