


















深入 kube-scheduler 源码,按调度流水线实际触发顺序,逐一拆解 NodeResourcesFit、NodeAffinity、TaintToleration、InterPodAffinity、PodTopologySpread、VolumeBinding 六大核心插件的职责边界、源码结构与生产踩坑点。所有源码行号基于 k8s v1.36.1 tag。
阅读收益:理解调度器两阶段决策模型 / 掌握六大插件核心逻辑 / 规避生产高频踩坑
● 核心前置清单(必掌握)
pkg/scheduler/framework/plugins/names/names.go L19-43 行NewInTreeRegistry() 将插件名映射到工厂函数(registry.go L50-79)UnschedulableAndUnresolvable 立即中断,不调用后续插件● 理解层级(了解即可)
EventsToRegister 决定哪些事件触发 Pending Pod 重新入队Unschedulable(资源不足,等扩容)vs UnschedulableAndUnresolvable(任何节点都不行)文章导航
早期 k8s 调度器(v1.14 及之前)是一个 monolithic 大函数,内置几十个 predicate 和 priority 函数,每加一个策略都要改主循环代码。这带来三个根本痛点:
从 v1.15 起,调度器重构成 Scheduler Framework:将"过滤 / 打分 / 绑定"切成固定扩展点,每个扩展点可挂载多个插件。内置插件是出厂预装,第三方可以 OutOfTree 方式注册自己的插件。
| 维度 | 传统 monolithic(v1.14 及之前) | Scheduler Framework + 内置插件(v1.15+) |
|---|---|---|
| 扩展性 | 改源码 + 重新编译 | OutOfTree 注册自定义插件 |
| 代码组织 | 所有 predicate/priority 堆在主文件 | 每个插件独占一个目录 |
| 替换单个策略 | 无法实现 | SchedulerConfiguration 中 disablePlugin 禁用 |
| 短路能力 | 所有 predicate 必须跑完 | UnschedulableAndUnresolvable 立刻中断 |
| 调试可见性 | 日志看不到哪个 predicate 失败 | 每个插件返回 Status 带 reason,describe pod 可看 |
| 扩展点数量 | 硬编码:预选 -> 优选 -> 绑定 | 14 个扩展点(PreEnqueue ~ Bind) |
● 生产禁忌
不要直接修改 kube-scheduler 源码来定制内置插件的行为。正确做法是在 SchedulerConfiguration 中 disablePlugin: - NodeResourcesFit 后,实现自己的同名插件 OutOfTree 注册。直接改源码会让集群脱离上游,无法升级。
调度器对每个 Pod 的决策分为两个阶段:
┌──────────────────────────────────────────────────────────────┐ │ Filter(硬门槛,行/不行) │ │ │ │ NodeResourcesFit ──► NodeAffinity ──► TaintToleration │ │ │ │ │ │ │ 资源够吗? 节点 label 匹配? 容忍污点吗? │ │ │ │ │ │ │ ▼ ▼ ▼ │ │ 任一插件返回 UnschedulableAndUnresolvable ──► 短路跳过 │ └──────────────────────────────────────────────────────────────┘ ┌──────────────────────────────────────────────────────────────┐ │ Score(软打分,好/更好) │ │ │ │ NodeResourcesFit ──► NodeAffinity ──► TaintToleration │ │ InterPodAffinity ──► PodTopologySpread ──► VolumeBinding │ │ │ │ │ │ │ 资源越闲越分高 亲和匹配越少越分高 卷容量越足越分高 │ │ │ │ ▼ NormalizeScore(归一化到 0-MaxNodeScore) │ │ 所有分数加权求和 ──► 选出分数最高节点 ──► Bind │ └──────────────────────────────────────────────────────────────┘
所有内置插件的名字都定义在同一 const 块中,NewInTreeRegistry() 通过这个名字映射到对应的工厂函数:
pkg/scheduler/framework/plugins/names/names.go (L19-43, k8s v1.36.1)
const (
NodeResourcesFit = "NodeResourcesFit"
NodeAffinity = "NodeAffinity"
TaintToleration = "TaintToleration"
InterPodAffinity = "InterPodAffinity"
PodTopologySpread = "PodTopologySpread"
VolumeBinding = "VolumeBinding"
NodeResourcesBalancedAllocation = "NodeResourcesBalancedAllocation"
NodeUnschedulable = "NodeUnschedulable"
NodeVolumeLimits = "NodeVolumeLimits"
ImageLocality = "ImageLocality"
// ... 共 23 个内置插件,完整定义见 names.go L19-43
)
pkg/scheduler/framework/plugins/registry.go (L50-79, k8s v1.36.1)
func NewInTreeRegistry() runtime.Registry {
fts := plfeature.NewSchedulerFeaturesFromGates(feature.DefaultFeatureGate)
registry := runtime.Registry{
nodeaffinity.Name: runtime.FactoryAdapter(fts, nodeaffinity.New),
noderesources.Name: runtime.FactoryAdapter(fts, noderesources.NewFit),
tainttoleration.Name: runtime.FactoryAdapter(fts, tainttoleration.New),
podtopologyspread.Name: runtime.FactoryAdapter(fts, podtopologyspread.New),
volumebinding.Name: runtime.FactoryAdapter(fts, volumebinding.New),
interpodaffinity.Name: runtime.FactoryAdapter(fts, interpodaffinity.New),
// ... 共 25 个插件,完整注册见 registry.go L50-79
}
return registry
}
● FactoryAdapter 的作用
每个插件工厂函数被 runtime.FactoryAdapter 包裹,以支持 feature gate 动态控制插件启用状态。feature gate 关闭时,对应插件工厂返回 nil,插件不被实例化。
下面按调度流水线上的实际触发顺序,逐一拆解六个核心插件。每个插件统一六段式:核心职责 -> 设计目的 -> 源码精读 -> YAML 示例 -> 生产踩坑 -> FAQ。
核心职责:判断节点 CPU / 内存 / GPU / 临时存储是否满足 Pod 的 resource.requests。资源不够时 Filter 直接返回 Unschedulable。Score 阶段对节点打分,越闲的节点分越高(默认 LeastAllocated 策略)。
pkg/scheduler/framework/plugins/noderesources/fit.go (k8s v1.36.1)
// Fit struct 定义(L92-105)
type Fit struct {
ignoredResources sets.Set[string]
ignoredResourceGroups sets.Set[string]
enableInPlacePodVerticalScaling bool
enableSidecarContainers bool
enableSchedulingQueueHint bool
enablePodLevelResources bool
enableDRAExtendedResource bool
handle fwk.Handle
*resourceAllocationScorer // 复用打分计算器
placementScorer *resourceAllocationScorer
}
// Filter 方法(L612-648)
// 核心逻辑:累加 pod 所有容器的 requests,对比 nodeInfo.Allocatable
// 任意资源不足 → 返回 Unschedulable 或 UnschedulableAndUnresolvable
func (f *Fit) Filter(ctx context.Context, cycleState fwk.CycleState,
pod *v1.Pod, nodeInfo fwk.NodeInfo) *fwk.Status {
s, err := getPreFilterState(cycleState)
insufficientResources := fitsRequest(s, nodeInfo, ...)
if len(insufficientResources) != 0 {
statusCode := fwk.Unschedulable
for _, ir := range insufficientResources {
if ir.Unresolvable {
statusCode = fwk.UnschedulableAndUnresolvable
}
}
return fwk.NewStatus(statusCode, failureReasons...)
}
return nil
}
// Score 方法(L767-786)- LeastAllocated 默认策略
// 公式: score = (capacity - requested) * MaxNodeScore / capacity
// 越闲 → 分子越大 → 分越高
func (f *Fit) Score(ctx context.Context, state fwk.CycleState,
pod *v1.Pod, nodeInfo fwk.NodeInfo) (int64, *fwk.Status) {
s, err := getPreScoreState(state)
return f.score(ctx, pod, nodeInfo, s.podRequests, s.draPreScoreState)
}
// EventsToRegister(L368-398)
// 关键事件:Pod 删除/缩容、节点 Add/UpdateNodeAllocatable/UpdateNodeTaint/UpdateNodeLabel
// 配合 enableSchedulingQueueHint 时,只注册 Add | UpdateNodeAllocatable 减少无效重试
● Score 四种策略
LeastAllocated(默认):最闲优先,适合通用计算。MostAllocated:最忙优先,适合批量回收。BalancedAllocation:CPU/内存均衡。RequestedToCapacityRatio:自定义资源权重,适合 GPU/FPGA 异构集群。
# 触发 NodeResourcesFit 的 Pod(k8s v1.36.1)
apiVersion: v1
kind: Pod
metadata:
name: resource-demo
spec:
containers:
- name: nginx
image: nginx:1.25
resources:
requests:
cpu: "2000m"
memory: "512Mi"
limits:
cpu: "4000m"
memory: "1Gi"
核心职责:解析 pod.spec.affinity.nodeAffinity 和 pod.spec.nodeSelector,根据节点 label 判断是否匹配。requiredDuringSchedulingIgnoredDuringExecution 是硬条件(Filter),preferredDuringSchedulingIgnoredDuringExecution 是软打分(Score)。
pkg/scheduler/framework/plugins/nodeaffinity/node_affinity.go (k8s v1.36.1)
// NodeAffinity struct(L38-44)
type NodeAffinity struct {
handle fwk.Handle
addedNodeSelector *nodeaffinity.NodeSelector // 配置级追加的选择器
addedPrefSchedTerms *nodeaffinity.PreferredSchedulingTerms
enableSchedulingQueueHint bool
}
// Filter(L216-239)- 硬条件匹配
func (pl *NodeAffinity) Filter(ctx context.Context, state fwk.CycleState,
pod *v1.Pod, nodeInfo fwk.NodeInfo) *fwk.Status {
node := nodeInfo.Node()
// 配置级追加的选择器优先判断
if pl.addedNodeSelector != nil && !pl.addedNodeSelector.Match(node) {
return fwk.NewStatus(fwk.UnschedulableAndUnresolvable, errReasonEnforced)
}
// requiredDuringSchedulingIgnoredDuringExecution 硬匹配
match, _ := s.requiredNodeSelectorAndAffinity.Match(node)
if !match {
return fwk.NewStatus(fwk.UnschedulableAndUnresolvable, ErrReasonPod)
}
return nil
}
// Score(L269-297)- 软偏好打分
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
// 累加匹配的 preferred terms 权重
if pl.addedPrefSchedTerms != nil {
count += pl.addedPrefSchedTerms.Score(node)
}
if s.preferredNodeAffinity != nil {
count += s.preferredNodeAffinity.Score(node)
}
return count, nil
}
# 触发 NodeAffinity 的 Pod(k8s v1.36.1)
# 必须调度到 node-type=general 或 compute 节点
apiVersion: v1
kind: Pod
metadata:
name: affinity-demo
spec:
nodeSelector:
az: cn-east-1
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: node-type
operator: In
values: [general, compute]
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
preference:
matchExpressions:
- key: az
operator: In
values: [cn-east-1]
核心职责:判断 Pod 的 tolerations 是否覆盖节点的污点。NoSchedule / NoExecute 污点无匹配时 Filter 返回 UnschedulableAndUnresolvable。PreferNoSchedule 污点无匹配时 Score 扣分。NoExecute 的驱逐由 kubelet 处理,不参与调度决策。
pkg/scheduler/framework/plugins/tainttoleration/taint_toleration.go (k8s v1.36.1)
// TaintToleration struct(L34-39)
type TaintToleration struct {
handle fwk.Handle
enableSchedulingQueueHint bool
enableTaintTolerationComparisonOperators bool // 支持 Exists 操作符
}
// ErrReasonNotMatch(L47-54)
const (
Name = names.TaintToleration
ErrReasonNotMatch = "node(s) had taints that the pod didn't tolerate"
)
// Filter(L118-132)
// 核心:调用 FindMatchingUntoleratedTaint 找无法容忍的污点
// NoSchedule/NoExecute 污点无匹配 → UnschedulableAndUnresolvable
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
}
return fwk.NewStatus(fwk.UnschedulableAndUnresolvable,
"node(s) had untolerated taint(s)")
}
// Score(L194-207)
// 无法容忍的 PreferNoSchedule 污点越多 → 分越低(越不想去)
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)
score := int64(pl.countIntolerableTaintsPreferNoSchedule(
logger, node.Spec.Taints, s.tolerationsPreferNoSchedule))
return score, nil
}
# 触发 TaintToleration 的 Pod(k8s v1.36.1)
apiVersion: v1
kind: Pod
metadata:
name: taint-demo
spec:
tolerations:
- key: "dedicated"
operator: "Equal"
value: "true"
effect: "NoSchedule"
# 关键业务:对 not-ready/unreachable 延迟 5 分钟驱逐
- key: "node.kubernetes.io/not-ready"
operator: "Exists"
effect: "NoExecute"
tolerationSeconds: 300
- key: "node.kubernetes.io/unreachable"
operator: "Exists"
effect: "NoExecute"
tolerationSeconds: 300
核心职责:根据 pod.spec.affinity.podAffinity(亲和)和 podAntiAffinity(反亲和)判断 Pod 之间的关系。硬亲和/反亲和在 Filter 阶段强制约束,软打分在 Score 阶段做偏好评估。
pkg/scheduler/framework/plugins/interpodaffinity/ (k8s v1.36.1)
// InterPodAffinity struct(plugin.go L46-53)
type InterPodAffinity struct {
parallelizer fwk.Parallelizer
args config.InterPodAffinityArgs
sharedLister fwk.SharedLister // 访问所有 Pod / Node
nsLister listersv1.NamespaceLister
enableSchedulingQueueHint bool
}
// Filter(filtering.go L410-432)
// 三层判断:Pod 亲和 -> Pod 反亲和 -> 已有 Pod 反亲和
// 任一层不满足 → 返回 Unschedulable 或 UnschedulableAndUnresolvable
func (pl *InterPodAffinity) Filter(ctx context.Context, cycleState fwk.CycleState,
pod *v1.Pod, nodeInfo fwk.NodeInfo) *fwk.Status {
state, err := getPreFilterState(cycleState)
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
}
// Score(scoring.go L236-255)
// 统计节点拓扑域内匹配的 Pod 数,越少分越高(反亲和)或越高(亲和)
func (pl *InterPodAffinity) Score(ctx context.Context, cycleState fwk.CycleState,
pod *v1.Pod, nodeInfo fwk.NodeInfo) (int64, *fwk.Status) {
node := nodeInfo.Node()
s, err := getPreScoreState(cycleState)
var score int64
for tpKey, tpValues := range s.topologyScore {
if v, exist := node.Labels[tpKey]; exist {
score += tpValues[v]
}
}
return score, nil
}
# 触发 InterPodAffinity 的 Pod(k8s v1.36.1)
# web 和 redis 必须分布在不同 zone(反亲和)
apiVersion: v1
kind: Pod
metadata:
name: web-pod
spec:
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchLabels:
app: redis-cache
topologyKey: topology.kubernetes.io/zone
podAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchLabels:
app: web-frontend
topologyKey: topology.kubernetes.io/zone
核心职责:解析 pod.spec.topologySpreadConstraints,让同一组 Pod 按 maxSkew 均匀分布在 zone / node 等拓扑域里。v1.18 引入,替代手动写反亲和规则。对没有声明约束的 Pod,会自动注入 system-defaulted 约束(v1.27+)。
pkg/scheduler/framework/plugins/podtopologyspread/plugin.go (k8s v1.36.1)
// PodTopologySpread struct(L59-73)
type PodTopologySpread struct {
systemDefaulted bool // 是否为 system 自动注入的约束
defaultConstraints []v1.TopologySpreadConstraint
sharedLister fwk.SharedLister
services corelisters.ServiceLister
replicaSets appslisters.ReplicaSetLister
statefulSets appslisters.StatefulSetLister
enableMatchLabelKeysInPodTopologySpread bool
enableSchedulingQueueHint bool
enableTaintTolerationComparisonOperators bool
}
// Filter:skew = count(匹配Pod) - min(所有拓扑域匹配数)
// 超过 maxSkew → UnschedulableAndUnresolvable
// Score:
// scoreForCount(cnt, maxSkew, tpWeight) = cnt * tpWeight + (maxSkew - 1)
// 匹配 Pod 越少 → skew 越小 → 分越高
# PodTopologySpread skew 计算示例 假设 3 个 zone,各有 Pod 数: zone-a: 5 个 Pod ← skew = 5 - 2 = 3 ← 超过 maxSkew zone-b: 2 个 Pod ← skew = 2 - 2 = 0 ← OK zone-c: 2 个 Pod ← skew = 2 - 2 = 0 ← OK 若 maxSkew = 2: → zone-a 超过 maxSkew,新 Pod 无法调度到 zone-a → 只能调度到 zone-b 或 zone-c 若 whenUnsatisfiable = ScheduleAnyway: → 允许调度到 zone-a,但记录拓扑违规
# 触发 PodTopologySpread 的 Pod(k8s v1.36.1)
# 让 demo-app 副本均匀分布在 zone 级别,maxSkew=1
apiVersion: v1
kind: Pod
metadata:
name: topology-spread-demo
spec:
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule # 硬约束
labelSelector:
matchLabels:
app: demo-app
- maxSkew: 2
topologyKey: topology.kubernetes.io/hostname
whenUnsatisfiable: ScheduleAnyway # 软约束
labelSelector:
matchLabels:
app: demo-app
核心职责:唯一横跨四个调度阶段的内置插件。解决"并发调度时 PV 被两个 Pod 同时抢占"的并发安全问题。核心机制是 AssumeCache:PreFilter 阶段"先假设能绑",Reserve 占位,PreBind 提交真实绑定,冲突时 RevertAssumedPodVolumes 回退。
pkg/scheduler/framework/plugins/volumebinding/ (k8s v1.36.1)
// VolumeBinding struct(volume_binding.go L70-79)
type VolumeBinding struct {
Binder SchedulerVolumeBinder // 核心绑定器接口
PVCLister corelisters.PersistentVolumeClaimLister
classLister storagelisters.StorageClassLister
scorer volumeCapacityScorer // 容量打分器(alpha)
fts feature.Features
}
// SchedulerVolumeBinder 接口(binder.go L153-196)
type SchedulerVolumeBinder interface {
// 获取 Pod 所有 PVC(已绑定 / 延迟绑定 / 立即绑定)
GetPodVolumeClaims(pod *v1.Pod) (*PodVolumeClaims, error)
// 查找匹配的 PV,检查节点亲和/容量/拓扑
FindPodVolumes(pod, node) (*PodVolumes, ConflictReasons, error)
// PreFilter 阶段:假设能绑,提前写入 AssumeCache
AssumePodVolumes(assumedPod, nodeName, volumes) (allFullyBound bool, err error)
// 绑定失败时回退
RevertAssumedPodVolumes(volumes)
// PreBind 阶段:提交真实绑定到 etcd
BindPodVolumes(ctx, assumedPod, volumes) error
}
// Filter(volume_binding.go L409-453)
// 调用 FindPodVolumes 检查 PV 节点亲和/容量/拓扑匹配
// 匹配失败 → UnschedulableAndUnresolvable(无任何节点可满足)
func (pl *VolumeBinding) Filter(ctx context.Context, cs fwk.CycleState,
pod *v1.Pod, nodeInfo fwk.NodeInfo) *fwk.Status {
logger := klog.FromContext(ctx)
node := nodeInfo.Node()
podVolumes, reasons, err := pl.Binder.FindPodVolumes(
logger, pod, state.podVolumeClaims, node)
if len(reasons) > 0 {
status := fwk.NewStatus(fwk.UnschedulableAndUnresolvable)
for _, reason := range reasons {
status.AppendReason(string(reason))
}
return status
}
state.Lock()
state.podVolumesByNode[node.Name] = podVolumes
state.Unlock()
return nil
}
// Score(volume_binding.go L470-523)
// 节点指定 StorageClass 剩余容量越多 → 分越高
// 需要 --feature-gates=StorageCapacityScoring=true(alpha)
func (pl *VolumeBinding) Score(...) (int64, *fwk.Status) {
if pl.scorer == nil { return 0, nil }
// 按 StorageClass 分组,计算剩余容量
// broken linear function:剩余越多分越高
return pl.scorer(classResources), nil
}
● AssumeCache 并发安全机制
PreFilter 阶段调用 AssumePodVolumes 把 PVC 信息临时写入 AssumeCache。Reserve 阶段占住节点,PreBind 阶段提交真实绑定。如果另一个 Pod 在此期间已经真正绑定了这个 PV,PreBind 阶段会发现冲突并调用 RevertAssumedPodVolumes 回退。这就是 VolumeBinding 要跨越四个阶段的原因——解决"并发调度时 PV 抢用"的 race condition。
# 触发 VolumeBinding 的 Pod(k8s v1.36.1)
apiVersion: v1
kind: Pod
metadata:
name: volume-demo
spec:
containers:
- name: app
image: nginx
volumeMounts:
- name: data
mountPath: /usr/share/nginx/html
volumes:
- name: data
persistentVolumeClaim:
claimName: my-pvc
---
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: fast-ssd
provisioner: pd.csi.storage.gke.io
parameters:
type: pd-ssd
volumeBindingMode: WaitForFirstConsumer # 关键:延迟绑定
allowVolumeExpansion: true
| 插件 | Filter 职责 | Score 职责 | 跨越阶段 | 核心数据结构 |
|---|---|---|---|---|
| NodeResourcesFit | CPU/内存/GPU/临时存储是否足够 | LeastAllocated 最闲优先(默认) | Filter / Score | resourceAllocationScorer |
| NodeAffinity | nodeSelector + nodeAffinity required 硬匹配 | preferred 软偏好打分 | Filter / Score | nodeaffinity.NodeSelector |
| TaintToleration | NoSchedule/NoExecute 污点容忍判断 | PreferNoSchedule 污点越多分越低 | Filter / Score | Toleration[] vs NodeSpec.Taints[] |
| InterPodAffinity | 硬亲和/反亲和拓扑域匹配 | 软亲和/反亲和加权打分 | Filter / Score | WeightedPodAffinityTerm[] |
| PodTopologySpread | 拓扑域 skew 是否超过 maxSkew | 匹配 Pod 数越少分越高 | Filter / Score | TopologySpreadConstraint[] |
| VolumeBinding | PV 节点亲和/容量/拓扑匹配 | StorageClass 剩余容量打分(alpha) | PreFilter / Filter / Reserve / PreBind | SchedulerVolumeBinder |
● 问题现象
Pod 申请了 GPU 资源(nvidia.com/gpu: 1),节点确认有 GPU,但调度反复报 Insufficient nvidia.com/gpu,Pod 始终 Pending。
根因:v1.26 之前 NodeResourcesFit 读取的是 container[].resources.limits 计算资源;v1.26 起改为读取 container[].resources.requests。如果 Pod 只写了 limits 没写 requests,v1.26+ 的行为与之前不一致。
排查命令:
# 查看节点真实 Allocatable
kubectl describe node <node-name> | grep -A5 "Allocated resources"
# 查看 Pod 实际 requests/limits
kubectl get pod <pod-name> -o jsonpath='{.spec.containers[*].resources}'
# 开启调度器详细日志
kubectl get pod <pod-name> -n kube-system -o jsonpath='{.spec.nodeName}' 2>/dev/null || echo "未调度"
修复:
containers:
- name: gpu-worker
resources:
requests:
nvidia.com/gpu: "1" # v1.26+ 必须显式声明
limits:
nvidia.com/gpu: "1"
● 问题现象
Pod 的 nodeSelector 写了 node-type: general,节点确认打上了 label,但调度一直失败。
根因:调度器通过 node informer 同步节点 label。如果 Pod 提交后、调度前 label 刚更新但 informer 还未同步完成,调度器看到的是旧数据。Filter 阶段匹配失败。
修复:使用 preferredDuringSchedulingIgnoredDuringExecution 替代硬匹配,或确保 Pod 提交前 label 已存在。
● 问题现象
两个 Deployment 互为 required 硬亲和,结果双方各有部分 Pod 调度成功,但始终有部分 Pod 一直 Pending。
根因:requiredDuringSchedulingIgnoredDuringExecution 的硬亲和在 Filter 阶段强制要求拓扑域内已有匹配 Pod。初始部署时双方 Pod 互相依赖,出现"鸡生蛋蛋生鸡"的死锁。
修复:将 required 改为 preferred,降低初始部署阶段的耦合度。
● 问题现象
Pod 没有写任何 topologySpreadConstraints,但调度器仍然给它应用了 zone 打散约束,导致 Pod 被调度到不符合业务预期的 zone。
根因:v1.27+ 中,当 Pod 不声明约束且 SchedulerConfiguration 的 defaultingType = SystemDefaulting 时,PodTopologySpread 自动注入 hostname skew=3 和 zone skew=5 两个 system-defaulted 约束。
修复:显式声明自己的约束覆盖默认行为,或在 SchedulerConfiguration 中把 defaultingType 设为 CustomDefaulting 并清空 defaultConstraints。
● 问题现象
使用 WaitForFirstConsumer PVC 的 Pod 反复调度、始终不绑定,卡在 Pending。
根因:VolumeBinding 在 PreFilter 阶段假设 PVC 能绑定。如果 StorageClass 是 WaitForFirstConsumer,真正绑定要等 Pod 调度到节点后才触发。假设期间节点信息变化(污点/label 更新),AssumeCache 里的假设失效,导致调度循环。
修复:确保 Pod 的 nodeSelector 或亲和性先选定候选节点,再让 PVC 在该节点附近绑定。
Q1: Filter 和 Score 的本质区别是什么?
Answer:Filter 是"行 / 不行"的二元判断,返回 Unschedulable 或 nil。任何 Filter 返回 UnschedulableAndUnresolvable 时调度立即短路,不再调用后续 Filter 和所有 Score。Score 是"好 / 更好"的打分,返回 0-MaxNodeScore 的整数值,所有 Score 分数加权求和后决定节点最终排名。Filter 是硬约束,Score 是软偏好。
Q2: Unschedulable 和 UnschedulableAndUnresolvable 的差别?
Answer:Unschedulable 表示当前节点不行,但其他节点可能行(如资源不够,扩容节点就能解决),调度器会等事件触发重试。UnschedulableAndUnresolvable 表示不仅当前节点不行,任何节点都不行(如 PVC 不存在),调度器直接让 Pod 进入 Unschedulable 队列,不做无意义的重试。
Q3: EventsToRegister 和调度队列重试是什么关系?
Answer:每个插件在 EventsToRegister 中声明自己关心哪些 Kubernetes 事件。当这些事件发生时(如节点 label 更新、Pod 删除、PV 绑定完成),调度器的 SchedulingQueue 把等待中的 Pod 重新入队触发重试。如果没有注册对应事件,即使资源释放了 Pod 也不会被重新调度。
Q4: 为什么 VolumeBinding 是唯一跨越四个阶段的内置插件?
Answer:因为它需要解决并发调度时的 PV 抢用问题。PreFilter 假设能绑(AssumePodVolumes),Reserve 占住节点,PreBind 提交真实绑定。如果另一个 Pod 已经绑定了这个 PV,PreBind 阶段会发现冲突并 RevertAssumedPodVolumes 回退。这个两阶段设计(假设 + 确认)是并发安全的核心。
Q5: NodeResourcesFit 的四个 Score 策略分别适合什么场景?
Answer:LeastAllocated(默认):最闲优先,适合通用计算,让资源均匀分布。MostAllocated:最忙优先,适合批量回收,把碎片 Pod 往已满节点集中。BalancedAllocation:CPU/内存均衡,适合计算密集型。RequestedToCapacityRatio:自定义每种资源权重,适合 GPU/FPGA 异构集群。
Q6: PodTopologySpread 的 skew 公式是什么?
Answer:skew = count(当前拓扑域匹配 Pod) - min(所有拓扑域匹配 Pod)。例如 zone-a 有 5 个 Pod,zone-b 有 2 个,zone-c 有 2 个,则 zone-a 的 skew=5-2=3。如果 maxSkew=2,zone-a 超过约束,新 Pod 无法调度到 zone-a。
Q7: TaintToleration 的 tolerationSeconds 只对哪种 effect 有效?
Answer:只对 NoExecute effect 有效。如果节点被打上 NoExecute 污点,有 tolerationSeconds: 300 的 Pod 会等 300 秒后再驱逐(给节点恢复或 Pod 迁移留出缓冲)。NoSchedule 和 PreferNoSchedule 不支持此字段。
Q8: NodeAffinity 的 addedNodeSelector 和 addedPrefSchedTerms 是什么?
Answer:这两个字段用于 SchedulerConfiguration 级别的 NodeAffinity 扩展。可以在配置中为所有 Pod 统一追加节点选择器和优先调度条件,让所有 Pod 自动继承额外的亲和性规则,而不需要每个 Pod 单独配置。
Q9: InterPodAffinity 的 Filter 阶段具体计算什么?
Answer:对硬亲和(requiredDuringSchedulingIgnoredDuringExecution)的每个 topologyTerm,统计当前节点所在拓扑域内已运行 Pod 的加权匹配数。Filter 阶段分三层判断:satisfyPodAffinity、satisfyPodAntiAffinity、satisfyExistingPodsAntiAffinity,任一层不满足就过滤。软打分(preferred)不参与 Filter。
Q10: VolumeBinding 的 SchedulerVolumeBinder 接口有哪些核心方法?
Answer:五个核心方法:GetPodVolumeClaims 获取 Pod 所需 PVC;FindPodVolumes 查找匹配的 PV 并返回冲突原因;AssumePodVolumes 预占 PV(AssumeCache);RevertAssumedPodVolumes 绑定失败时回退;BindPodVolumes 在 PreBind 阶段提交真实绑定到 etcd。
Q11: enableSchedulingQueueHint(调度队列提示)功能是什么?
Answer:v1.28+ beta 特性。SchedulingQueueHint 利用插件注册的 QueueingHintFn 智能判断"某个事件发生后,Pod 是否真的需要重新调度"。避免每次节点 label 变化都盲目重试所有 Pending Pod。这能显著减少无效调度循环,降低调度延迟和 API server 压力。
Q12: PodTopologySpread 的 systemDefaulted 字段何时为 true?
Answer:当 Pod 没有声明任何 topologySpreadConstraints 且 SchedulerConfiguration 的 defaultingType = SystemDefaulting(v1.27+ 默认)时,PodTopologySpread 自动注入 hostname skew=3 和 zone skew=5 两个 system-defaulted 约束。不想自动注入可设为 CustomDefaulting 并清空 defaultConstraints。
Q13: VolumeBinding 的 Score 策略 volumeCapacityScoring 是什么?
Answer:VolumeBinding 的 Score 目标不是节点资源,而是指定 StorageClass 的剩余卷容量。采用 broken linear function 公式,节点剩余容量越多得分越高。需要 --feature-gates=StorageCapacityScoring=true 开启(v1.36.1 中为 alpha)。对多 StorageClass 集群很有用。
Q14: 如何确认某个 Pod 最终由哪个插件决定了调度失败?
Answer:kubectl describe pod <pod-name> 的 Events 会按时间顺序列出调度失败原因(如 0/3 nodes are available: 1 node(s) had taints..., 2 node(s) resource fit failed)。开启调度器详细日志 --v=5,每个 Filter 插件的通过/失败都有记录。
Q15: 如何禁用某个内置插件而不影响其他插件?
Answer:在 SchedulerConfiguration 的 profiles[].plugins.multiPoint.disabled 中禁用指定插件名:disabledPlugin: - TaintToleration。禁用后该插件在所有 Profile 所有扩展点都不会被调用。如果只想禁用 Score 而保留 Filter,需要在 Score 扩展点单独配置。
Q16: InterPodAffinity 和 PodTopologySpread 都能做 Pod 打散,区别是什么?
Answer:InterPodAffinity 通过 pod.spec.affinity.podAntiAffinity 做两两 Pod 之间的精确控制,适合"Pod A 必须和 Pod B 在一起/分开"的场景。PodTopologySpread 是 v1.18 引入的声明式拓扑打散,通过 topologySpreadConstraints 自动计算全局最优分布,适合"让 N 个副本均匀散开"的场景。两者可以叠加使用。
Q17: 如何解决 InterPodAffinity 硬亲和导致的新建 Deployment 死锁?
Answer:将 requiredDuringSchedulingIgnoredDuringExecution 改为 preferredDuringSchedulingIgnoredDuringExecution。初始部署阶段使用软约束降低耦合度,Pod 调度成功后自然形成拓扑分布。如果必须用硬亲和,先单独部署一方到目标节点,再部署依赖方。
Q18: VolumeBinding 的 WaitForFirstConsumer 和 Immediate 模式分别适合什么场景?
Answer:Immediate(默认):PVC 创建时立即绑定 PV,适合已知 Pod 会调度到哪个节点的场景。WaitForFirstConsumer:等第一个 Pod 调度到节点后才绑定 PV,让 PV 优先选择 Pod 所在节点或拓扑域,适合云盘等有区域限制的存储类型,可减少跨区流量。
Q19: 六个插件的执行顺序是怎么确定的?
Answer:Filter 阶段按 SchedulerConfiguration/profiles[].plugins.filter.enabled 数组顺序执行(默认按注册顺序)。Score 阶段先并行执行所有 Score 插件,再用 NormalizeScore 归一化到 [0, MaxNodeScore] 范围,最后加权求和。插件执行顺序不影响最终结果,只影响中间短路时机。
Q20: NodeResourcesFit 中 enablePodLevelResources 和 enableInPlacePodVerticalScaling 的区别?
Answer:enablePodLevelResources(v1.27+)允许在 Pod 级而非容器级声明资源请求,多容器 Pod 的所有容器 requests 会被累加计算。enableInPlacePodVerticalScaling(v1.27+)支持 Pod 在运行中动态调整容器 resource.requests 而不需要重建容器,结合 InPlacePodVerticalScaling feature gate 使用。
Q21: TaintToleration 的 enableTaintTolerationComparisonOperators 参数有什么用?
Answer:默认情况下 toleration 的 key 和 taint 的 key 必须完全匹配。启用此参数后支持 Exists 操作符:operator: Exists 且不指定 value 表示"容忍所有带此 key 的污点"。例如 tolerations: - operator: Exists 可以容忍所有 NoExecute 污点。
Q22: Pod 调度失败时,SchedulingQueue 的 Unschedulable 队列和 BackoffQ 队列有什么区别?
Answer:Unschedulable 队列存放调度失败的 Pod,等待触发事件重新入队(如节点 label 变化、Pod 删除)。BackoffQ 是退避队列,对反复调度失败的 Pod 做指数退避等待,减少无效重试对 API server 的压力。当有 BackoffPods 或 UnschedulablePods 时,可用 kubectl drain 或调整 Pod 优先级让它优先重试。
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。