Overview

The Kubernetes scheduler is one of the most critical control plane components—it decides which node each Pod runs on. Scheduling quality directly impacts cluster resource utilization, application performance, and reliability. Understanding how the scheduler works is fundamental to effective K8s production operations.

This article systematically covers the scheduling flow, filter-and-score mechanism, affinity, taints and tolerations, priority and preemption, and custom schedulers.

Based on Kubernetes v1.30. Reference: Kubernetes Scheduler Documentation

Scheduling Flow

Overall Flow

Pod created → API Server → etcd → Scheduler Watch → Scheduling decision → Bind to node → kubelet creates container

The scheduler’s core work has two phases:

1. Filter: Exclude nodes that don't meet conditions → Candidate node set
2. Score: Score candidate nodes → Select highest-scoring node

Detailed Scheduling Flow

                    ┌──────────────┐
                    │  Pod enqueued │
                    └──────┬───────┘
                    ┌──────────────┐
                    │ Scheduling    │
                    │ cycle starts  │
                    └──────┬───────┘
               ┌────────────────────────┐
               │  Extension: PreFilter   │  ← Pre-filter (check if Pod is schedulable)
               └────────────┬────────────┘
               ┌────────────────────────┐
               │  Extension: Filter       │  ← Filter out infeasible nodes
               │  (exclude infeasible)    │
               └────────────┬────────────┘
               ┌────────────────────────┐
               │  Extension: PostFilter  │  ← No nodes after filter? (trigger preemption)
               └────────────┬────────────┘
               ┌────────────────────────┐
               │  Extension: Score        │  ← Score candidate nodes
               │  (select best node)     │
               └────────────┬────────────┘
               ┌────────────────────────┐
               │  Extension: Reserve      │  ← Reserve resources
               └────────────┬────────────┘
               ┌────────────────────────┐
               │  Extension: Permit       │  ← Allow/delay/reject
               └────────────┬────────────┘
               ┌────────────────────────┐
               │  Extension: PreBind      │  ← Pre-bind processing
               └────────────┬────────────┘
               ┌────────────────────────┐
               │  Extension: Bind         │  ← Bind to node
               └────────────┬────────────┘
               ┌────────────────────────┐
               │  Extension: PostBind     │  ← Post-bind processing
               └────────────────────────┘

Scheduling Queues

The scheduler maintains three internal queues:

QueueDescriptionPriority
activeQueuePods awaiting scheduling, sorted by priorityHigher priority scheduled first
backoffQueuePods that failed scheduling, awaiting retryExponential backoff
unschedulableQueueUnschedulable Pods, waiting for condition changesChecked periodically
Pod enters → activeQueue → Scheduling succeeds → Bind
                ↓ Scheduling fails
           backoffQueue → Wait backoff time → activeQueue (retry)
                ↓ Multiple failures
           unschedulableQueue → When conditions change → activeQueue

Filtering and Scoring

Filter Phase

The filter phase excludes nodes that don’t meet Pod requirements, involving multiple plugins:

Filter PluginFunction
PodFitsResourcesWhether node resources satisfy Pod’s requests
PodFitsHostPortsWhether node has free hostPort
NodeSelectorWhether node matches nodeSelector
NodeAffinityWhether node matches node affinity
TaintTolerationWhether Pod tolerates node’s taints
VolumeBindingWhether node can bind Pod’s requested PV
VolumeZoneWhether PV topology constraints match
PodTopologySpreadWhether topology spread constraints are met
NodeUnschedulableWhether node is cordoned
NodeMemoryPressureWhether node has memory pressure
NodeDiskPressureWhether node has disk pressure
NodePIDPressureWhether node has PID pressure
NetworkFilterWhether network is available

Score Phase

The score phase scores each candidate node (0-100); highest score wins:

Score PluginFunctionWeight
NodeResourcesFitResource utilization scoringHigh
InterPodAffinityPod affinity scoringMedium
NodeAffinityNode affinity preference scoringMedium
PodTopologySpreadTopology spread scoringMedium
NodeUnschedulableUnschedulable penalty-
ImageLocalityNode already has imageLow
TaintTolerationTaint toleration preference scoringLow

Resource Scoring Strategies

NodeResourcesFit supports three scoring strategies:

# Specify via scheduler configuration
apiVersion: kubescheduler.config.k8s.io/v1
kind: KubeSchedulerConfiguration
profiles:
- schedulerName: default-scheduler
  pluginConfig:
  - name: NodeResourcesFit
    args:
      scoringStrategy:
        type: LeastAllocated    # Scoring strategy
        resources:
        - name: cpu
          weight: 1
        - name: memory
          weight: 1
StrategyScoring LogicEffect
LeastAllocatedPrefer nodes with least resource allocationEven distribution
MostAllocatedPrefer nodes with most resource allocationCompact distribution
RequestedToCapacityRatioScore by resource ratioCustom weights
# LeastAllocated (default): Pods spread across different nodes
# Suitable: General scenarios, even load

# MostAllocated: Pods concentrate on same node
# Suitable: Saving nodes (scale-down scenarios)

Node Affinity

Node Selector vs Node Affinity

FeaturenodeSelectornodeAffinity
MatchingExact matchMultiple operators
Hard constraintYesrequired
Soft constraintNopreferred
RecommendedNot recommendedRecommended

required (Hard Constraint)

spec:
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
        - matchExpressions:
          - key: kubernetes.io/arch
            operator: In
            values:
            - amd64
          - key: node.kubernetes.io/instance-type
            operator: In
            values:
            - c5.2xlarge
            - c5.4xlarge
          - key: topology.kubernetes.io/zone
            operator: NotIn
            values:
            - us-east-1a    # Exclude this AZ

preferred (Soft Constraint)

spec:
  affinity:
    nodeAffinity:
      preferredDuringSchedulingIgnoredDuringExecution:
      - weight: 100            # Weight 1-100
        preference:
          matchExpressions:
          - key: ssd
            operator: In
            values:
            - "true"
      - weight: 50
        preference:
          matchExpressions:
          - key: topology.kubernetes.io/zone
            operator: In
            values:
            - us-east-1b

Operators

OperatorDescriptionExample
InValue in listkey In [a, b]
NotInValue not in listkey NotIn [a, b]
ExistsKey existskey Exists
DoesNotExistKey doesn’t existkey DoesNotExist
GtGreater than (numeric)key Gt 10
LtLess than (numeric)key Lt 10

Node Label Management

# Add labels
kubectl label nodes node-1 disktype=ssd
kubectl label nodes node-1 zone=east
kubectl label nodes node-1 gpu=true

# View labels
kubectl get nodes --show-labels
kubectl get nodes -l disktype=ssd

# Delete label
kubectl label nodes node-1 disktype-

Pod Affinity and Anti-Affinity

Pod Affinity

Pod affinity makes Pods prefer scheduling to nodes that already have certain Pods:

spec:
  affinity:
    podAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
      - labelSelector:
          matchExpressions:
          - key: app
            operator: In
            values:
            - cache
        topologyKey: kubernetes.io/hostname
        # Pod should be on same node as app=cache Pods

Pod Anti-Affinity

Pod anti-affinity makes Pods prefer scheduling to nodes that don’t have certain Pods:

spec:
  affinity:
    podAntiAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
      - labelSelector:
          matchExpressions:
          - key: app
            operator: In
            values:
            - web
        topologyKey: kubernetes.io/hostname
        # web Pods can't be on same node (high availability)
      preferredDuringSchedulingIgnoredDuringExecution:
      - weight: 100
        podAffinityTerm:
          labelSelector:
            matchExpressions:
            - key: app
              operator: In
              values:
              - web
          topologyKey: topology.kubernetes.io/zone
          # Try to spread web Pods across different AZs

Common Usage Patterns

ScenarioConfigurationDescription
Co-locatepodAffinity + topologyKey: hostnameFrontend and cache on same node
Separate nodespodAntiAffinity + topologyKey: hostnameSame service Pods spread out
Different AZspodAntiAffinity + topologyKey: zoneCross-AZ high availability
Rack awarenesspodAntiAffinity + topologyKey: rackCross-rack distribution

Performance Issues with Anti-Affinity

Note: requiredDuringSchedulingIgnoredDuringExecution Pod anti-affinity has performance issues in large clusters. The scheduler must iterate all Pods on all nodes to check label matches—when node count exceeds 1000, scheduling latency increases noticeably.

The alternative is podTopologySpread, which is designed with performance in mind:

spec:
  topologySpreadConstraints:
  - maxSkew: 1                           # Max skew between topology domains
    topologyKey: kubernetes.io/hostname   # Topology domain: node
    whenUnsatisfiable: DoNotSchedule       # Reject if not satisfiable
    labelSelector:
      matchLabels:
        app: web
  - maxSkew: 1
    topologyKey: topology.kubernetes.io/zone  # Topology domain: AZ
    whenUnsatisfiable: ScheduleAnyway         # Best effort
    labelSelector:
      matchLabels:
        app: web

Taints and Tolerations

Taints

Taints mark nodes to prevent Pods that don’t tolerate the taint from being scheduled:

# Add taint
kubectl taint nodes node-1 dedicated=gpu:NoSchedule

# View taints
kubectl describe node node-1 | grep Taint

# Remove taint
kubectl taint nodes node-1 dedicated=gpu:NoSchedule-

Three Taint Effects

EffectDescriptionUse Case
NoScheduleDon’t schedule new Pods, existing Pods unaffectedDedicated nodes
PreferNoScheduleTry not to schedule, non-bindingSoft isolation
NoExecuteDon’t schedule new Pods, evict existing Pods that don’t tolerateMaintenance mode

Toleration

spec:
  tolerations:
  - key: "dedicated"
    operator: "Equal"
    value: "gpu"
    effect: "NoSchedule"

  # Shorthand: only match key and effect
  - key: "dedicated"
    operator: "Exists"
    effect: "NoSchedule"

  # Tolerate all taints (use with caution)
  - operator: "Exists"

  # Tolerate NoExecute with eviction delay
  - key: "node.kubernetes.io/not-ready"
    operator: "Exists"
    effect: "NoExecute"
    tolerationSeconds: 300    # Evict 300s after node becomes NotReady

System-Automated Taints

TaintDescriptionDefault Toleration
node.kubernetes.io/not-readyNode NotReadyCore components tolerate 300s
node.kubernetes.io/unreachableNode unreachableCore components tolerate 300s
node.kubernetes.io/memory-pressureMemory pressureDon’t schedule new Pods
node.kubernetes.io/disk-pressureDisk pressureDon’t schedule new Pods
node.kubernetes.io/pid-pressurePID pressureDon’t schedule new Pods
node.kubernetes.io/network-unavailableNetwork unavailableCore components tolerate
node.kubernetes.io/unschedulableUnschedulable (cordon)Don’t schedule new Pods

Common Use Cases

# 1. GPU dedicated nodes
kubectl taint nodes gpu-node-1 nvidia.com/gpu=:NoSchedule
# Only Pods tolerating this taint (GPU tasks) will be scheduled

# 2. Maintenance mode
kubectl taint nodes node-1 maintenance=true:NoExecute
# All Pods evicted (except those tolerating), for node maintenance

# 3. Special nodes
kubectl taint nodes special-node-1 dedicated=special-team:NoSchedule
kubectl label nodes special-node-1 team=special-team

Priority and Preemption

PriorityClass

apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: high-priority
value: 1000000
globalDefault: false
description: "High priority workloads"
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: low-priority
value: 100
globalDefault: false
description: "Low priority workloads"

System PriorityClasses

PriorityClassValueDescription
system-node-critical2000001000Node-critical components
system-cluster-critical2000000000Cluster-critical components
user-created0-1000000000User-defined

Using Priority in Pods

apiVersion: apps/v1
kind: Deployment
metadata:
  name: critical-app
spec:
  template:
    spec:
      priorityClassName: high-priority
      containers:
      - name: app
        image: myapp:v1

Preemption Mechanism

When a high-priority Pod can’t be scheduled, the scheduler tries to evict lower-priority Pods to make room:

1. High-priority Pod can't be scheduled (insufficient resources)
2. Scheduler looks for lower-priority Pods to evict
3. Evicts lower-priority Pods, releases resources
4. High-priority Pod scheduled successfully
5. Lower-priority Pods enter Pending state

Preemption Protection

# Configure PDB for low-priority Pods to prevent eviction
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: low-priority-pdb
spec:
  minAvailable: 1                    # Keep at least 1 available
  selector:
    matchLabels:
      app: low-priority-app

Note: PDB only limits Voluntary Disruptions, and preemption is a voluntary disruption. But PDB’s minAvailable constraint is still respected—if eviction would drop below minAvailable, preemption won’t happen.

Topology Spread Constraints

Basic Usage

spec:
  topologySpreadConstraints:
  - maxSkew: 1
    topologyKey: topology.kubernetes.io/zone
    whenUnsatisfiable: DoNotSchedule
    labelSelector:
      matchLabels:
        app: web
    minDomains: 2                   # Requires at least 2 topology domains
    matchLabelKeys:
    - pod-template-hash             # For rolling updates

Distribution Example

3 AZs, 6 web Pods, maxSkew=1

zone-a: [web-0, web-1]
zone-b: [web-2, web-3]
zone-c: [web-4, web-5]
Max skew = 0 ≤ 1 ✓
If zone-c is unavailable:
zone-a: [web-0, web-1, web-2]
zone-b: [web-3, web-4, web-5]
Max skew = 0 ≤ 1 ✓

whenUnsatisfiable Options

ValueBehaviorUse Case
DoNotScheduleReject if constraint not metHard constraint (HA required)
ScheduleAnywayTry to satisfy, schedule anyway if notSoft constraint (best effort)

Scheduler Configuration

Multiple Schedulers

K8s supports running multiple schedulers; Pods can specify which to use:

# Deploy custom scheduler
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-scheduler
  namespace: kube-system
spec:
  replicas: 1
  selector:
    matchLabels:
      app: my-scheduler
  template:
    metadata:
      labels:
        app: my-scheduler
    spec:
      containers:
      - name: my-scheduler
        image: registry.k8s.io/kube-scheduler:v1.30.0
        command:
        - kube-scheduler
        - --config=/etc/kubernetes/my-scheduler-config.yaml
        - --scheduler-name=my-scheduler
        volumeMounts:
        - name: config
          mountPath: /etc/kubernetes
      volumes:
      - name: config
        configMap:
          name: my-scheduler-config

---
# Pod using custom scheduler
apiVersion: v1
kind: Pod
metadata:
  name: my-pod
spec:
  schedulerName: my-scheduler    # Specify scheduler
  containers:
  - name: app
    image: myapp:v1

Scheduler Configuration File

# /etc/kubernetes/my-scheduler-config.yaml
apiVersion: kubescheduler.config.k8s.io/v1
kind: KubeSchedulerConfiguration
profiles:
- schedulerName: my-scheduler
  plugins:
    filter:
      enabled:
      - name: NodeResourcesFit
      - name: NodeAffinity
      - name: TaintToleration
      disabled:
      - name: VolumeBinding     # Disable a default plugin
    score:
      enabled:
      - name: NodeResourcesFit
        weight: 10
      - name: InterPodAffinity
        weight: 5
  pluginConfig:
  - name: NodeResourcesFit
    args:
      scoringStrategy:
        type: LeastAllocated
        resources:
        - name: cpu
          weight: 2
        - name: memory
          weight: 1

Scheduler Extensions

Scheduling Framework

K8s v1.19+ Scheduling Framework allows extending scheduling behavior without modifying scheduler source code:

Extension points:
PreFilter → Filter → PostFilter → PreScore → Score → Reserve → Permit → PreBind → Bind → PostBind

Extension Methods

MethodComplexityFlexibilityUse Case
Scheduler configLowMediumAdjust plugin weights and strategies
Scheduling FrameworkMediumHighCustom plugin logic
Multiple schedulersHighVery highCompletely different scheduling strategies
Scheduler ExtenderMediumMediumHTTP extension (legacy)

Custom Scheduling Plugin Example

// Custom Filter plugin example
package myplugin

import (
    "context"
    v1 "k8s.io/api/core/v1"
    "k8s.io/kubernetes/pkg/scheduler/framework"
)

const Name = "MyPlugin"

type MyPlugin struct {
    handle framework.Handle
}

func (p *MyPlugin) Name() string { return Name }

func (p *MyPlugin) Filter(
    ctx context.Context,
    state *framework.CycleState,
    pod *v1.Pod,
    nodeInfo *framework.NodeInfo,
) *framework.Status {
    // Custom filtering logic
    if nodeInfo.Node().Labels["custom-label"] != "allowed" {
        return framework.NewStatus(framework.Unschedulable, "node not allowed")
    }
    return framework.NewStatus(framework.Success, "")
}

func (p *MyPlugin) Score(
    ctx context.Context,
    state *framework.CycleState,
    pod *v1.Pod,
    nodeName string,
) (int64, *framework.Status) {
    // Custom scoring logic
    return 100, framework.NewStatus(framework.Success, "")
}

// New initializes the plugin
func New(
    ctx context.Context,
    configuration runtime.Object,
    f framework.Handle,
) (framework.Plugin, error) {
    return &MyPlugin{handle: f}, nil
}
# Register custom plugin
apiVersion: kubescheduler.config.k8s.io/v1
kind: KubeSchedulerConfiguration
profiles:
- schedulerName: my-scheduler
  plugins:
    filter:
      enabled:
      - name: MyPlugin
    score:
      enabled:
      - name: MyPlugin
        weight: 10

Production Practices

High Availability Deployment Scheduling

apiVersion: apps/v1
kind: Deployment
metadata:
  name: web-app
spec:
  replicas: 6
  template:
    metadata:
      labels:
        app: web
    spec:
      # Cross-node + cross-AZ distribution
      topologySpreadConstraints:
      - maxSkew: 1
        topologyKey: topology.kubernetes.io/zone
        whenUnsatisfiable: DoNotSchedule
        labelSelector:
          matchLabels:
            app: web
      - maxSkew: 2
        topologyKey: kubernetes.io/hostname
        whenUnsatisfiable: ScheduleAnyway
        labelSelector:
          matchLabels:
            app: web
      # Node affinity: select appropriate nodes
      affinity:
        nodeAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
            - matchExpressions:
              - key: node-role.kubernetes.io/worker
                operator: Exists
              - key: kubernetes.io/arch
                operator: In
                values:
                - amd64
      # Tolerate taints
      tolerations:
      - key: "node.kubernetes.io/not-ready"
        operator: "Exists"
        effect: "NoExecute"
        tolerationSeconds: 30
      - key: "node.kubernetes.io/unreachable"
        operator: "Exists"
        effect: "NoExecute"
        tolerationSeconds: 30
      containers:
      - name: web
        image: myapp:v1
        resources:
          requests:
            cpu: 500m
            memory: 512Mi
          limits:
            cpu: 1000m
            memory: 1Gi

GPU Scheduling

# Taint GPU nodes
kubectl taint nodes gpu-node nvidia.com/gpu=:NoSchedule
kubectl label nodes gpu-node accelerator=nvidia

---
# GPU Pod
apiVersion: v1
kind: Pod
metadata:
  name: gpu-task
spec:
  tolerations:
  - key: "nvidia.com/gpu"
    operator: "Exists"
    effect: "NoSchedule"
  nodeSelector:
    accelerator: nvidia
  containers:
  - name: gpu-container
    image: tensorflow/tensorflow:latest-gpu
    resources:
      limits:
        nvidia.com/gpu: 2       # Request 2 GPUs

Dedicated Nodes

# Create dedicated node pool
kubectl label nodes node-pool-a dedicated=team-a
kubectl taint nodes node-pool-a dedicated=team-a:NoSchedule
# Team A's Pods
spec:
  nodeSelector:
    dedicated: team-a
  tolerations:
  - key: "dedicated"
    operator: "Equal"
    value: "team-a"
    effect: "NoSchedule"

Scheduling Troubleshooting

Pod Stuck in Pending

# View Pod scheduling failure reason
kubectl describe pod <pod-name> -n <namespace>

# Focus on Events section:
# Events:
#   Type     Reason            Age   From               Message
#   ----     ------            ----  ----               -------
#   Warning  FailedScheduling  10s   default-scheduler  0/10 nodes are available:
#     3 Insufficient cpu, 2 Insufficient memory,
#     3 node(s) had untolerated taint,
#     2 node(s) didn't match Pod's node affinity

Common Pending Causes

CauseSolution
Insufficient cpu/memoryAdd nodes or reduce requests
had untolerated taintAdd toleration
didn't match node affinityCheck nodeSelector/affinity
node(s) had volume node affinity conflictPV topology constraint conflict
Insufficient nvidia.com/gpuInsufficient GPU resources

Scheduler Logs

# View scheduler logs
kubectl logs -n kube-system -l component=kube-scheduler --tail=100

# View scheduling decision details
# Need to enable verbose scheduler logging
kube-scheduler --v=5  # Increase log level

Simulate Scheduling

# Use kubectl debug to simulate scheduling
kubectl debug node/<node-name> -it --image=busybox

# Use scheduler simulator to test scheduling policies
# Reference: https://github.com/kubernetes-sigs/kube-scheduler-simulator

Summary

The K8s scheduler is a highly extensible component. Key takeaways:

  1. Two-phase scheduling: Understand the filter and score phases—filter excludes infeasible nodes, score selects the best node.
  2. Choose the right affinity type: Node affinity controls which nodes Pods schedule to; Pod affinity controls relationships between Pods.
  3. Use taints for isolation: Taints + tolerations implement node dedication, more flexible than nodeSelector.
  4. Topology spread for HA: topologySpreadConstraints is a more efficient HA solution than Pod anti-affinity.
  5. Priority for preemption: Use high priority for core workloads, low priority for non-core—automatically ensures core workloads during resource pressure.
  6. Configuration over customization: Most scheduling needs can be met through scheduler configuration files without writing custom plugins.
  7. Resource requests must be configured: The scheduler schedules based on requests; Pods without requests disrupt scheduling decisions.

Scheduler tuning is an ongoing process. Regularly review Pod scheduling distribution and node resource utilization, and adjust affinity rules and topology constraints based on actual conditions.

References & Acknowledgments

This article referenced the following materials during writing. We thank the original authors for their contributions:

  1. Kubernetes Scheduler Documentation — Kubernetes Official, referenced for Kubernetes Scheduler Documentation