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:
| Queue | Description | Priority |
|---|---|---|
activeQueue | Pods awaiting scheduling, sorted by priority | Higher priority scheduled first |
backoffQueue | Pods that failed scheduling, awaiting retry | Exponential backoff |
unschedulableQueue | Unschedulable Pods, waiting for condition changes | Checked 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 Plugin | Function |
|---|---|
PodFitsResources | Whether node resources satisfy Pod’s requests |
PodFitsHostPorts | Whether node has free hostPort |
NodeSelector | Whether node matches nodeSelector |
NodeAffinity | Whether node matches node affinity |
TaintToleration | Whether Pod tolerates node’s taints |
VolumeBinding | Whether node can bind Pod’s requested PV |
VolumeZone | Whether PV topology constraints match |
PodTopologySpread | Whether topology spread constraints are met |
NodeUnschedulable | Whether node is cordoned |
NodeMemoryPressure | Whether node has memory pressure |
NodeDiskPressure | Whether node has disk pressure |
NodePIDPressure | Whether node has PID pressure |
NetworkFilter | Whether network is available |
Score Phase
The score phase scores each candidate node (0-100); highest score wins:
| Score Plugin | Function | Weight |
|---|---|---|
NodeResourcesFit | Resource utilization scoring | High |
InterPodAffinity | Pod affinity scoring | Medium |
NodeAffinity | Node affinity preference scoring | Medium |
PodTopologySpread | Topology spread scoring | Medium |
NodeUnschedulable | Unschedulable penalty | - |
ImageLocality | Node already has image | Low |
TaintToleration | Taint toleration preference scoring | Low |
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
| Strategy | Scoring Logic | Effect |
|---|---|---|
LeastAllocated | Prefer nodes with least resource allocation | Even distribution |
MostAllocated | Prefer nodes with most resource allocation | Compact distribution |
RequestedToCapacityRatio | Score by resource ratio | Custom 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
| Feature | nodeSelector | nodeAffinity |
|---|---|---|
| Matching | Exact match | Multiple operators |
| Hard constraint | Yes | required |
| Soft constraint | No | preferred |
| Recommended | Not recommended | Recommended |
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
| Operator | Description | Example |
|---|---|---|
In | Value in list | key In [a, b] |
NotIn | Value not in list | key NotIn [a, b] |
Exists | Key exists | key Exists |
DoesNotExist | Key doesn’t exist | key DoesNotExist |
Gt | Greater than (numeric) | key Gt 10 |
Lt | Less 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
| Scenario | Configuration | Description |
|---|---|---|
| Co-locate | podAffinity + topologyKey: hostname | Frontend and cache on same node |
| Separate nodes | podAntiAffinity + topologyKey: hostname | Same service Pods spread out |
| Different AZs | podAntiAffinity + topologyKey: zone | Cross-AZ high availability |
| Rack awareness | podAntiAffinity + topologyKey: rack | Cross-rack distribution |
Performance Issues with Anti-Affinity
Note:
requiredDuringSchedulingIgnoredDuringExecutionPod 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
| Effect | Description | Use Case |
|---|---|---|
NoSchedule | Don’t schedule new Pods, existing Pods unaffected | Dedicated nodes |
PreferNoSchedule | Try not to schedule, non-binding | Soft isolation |
NoExecute | Don’t schedule new Pods, evict existing Pods that don’t tolerate | Maintenance 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
| Taint | Description | Default Toleration |
|---|---|---|
node.kubernetes.io/not-ready | Node NotReady | Core components tolerate 300s |
node.kubernetes.io/unreachable | Node unreachable | Core components tolerate 300s |
node.kubernetes.io/memory-pressure | Memory pressure | Don’t schedule new Pods |
node.kubernetes.io/disk-pressure | Disk pressure | Don’t schedule new Pods |
node.kubernetes.io/pid-pressure | PID pressure | Don’t schedule new Pods |
node.kubernetes.io/network-unavailable | Network unavailable | Core components tolerate |
node.kubernetes.io/unschedulable | Unschedulable (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
| PriorityClass | Value | Description |
|---|---|---|
system-node-critical | 2000001000 | Node-critical components |
system-cluster-critical | 2000000000 | Cluster-critical components |
user-created | 0-1000000000 | User-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
minAvailableconstraint 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
| Value | Behavior | Use Case |
|---|---|---|
DoNotSchedule | Reject if constraint not met | Hard constraint (HA required) |
ScheduleAnyway | Try to satisfy, schedule anyway if not | Soft 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
| Method | Complexity | Flexibility | Use Case |
|---|---|---|---|
| Scheduler config | Low | Medium | Adjust plugin weights and strategies |
| Scheduling Framework | Medium | High | Custom plugin logic |
| Multiple schedulers | High | Very high | Completely different scheduling strategies |
| Scheduler Extender | Medium | Medium | HTTP 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
| Cause | Solution |
|---|---|
Insufficient cpu/memory | Add nodes or reduce requests |
had untolerated taint | Add toleration |
didn't match node affinity | Check nodeSelector/affinity |
node(s) had volume node affinity conflict | PV topology constraint conflict |
Insufficient nvidia.com/gpu | Insufficient 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:
- Two-phase scheduling: Understand the filter and score phases—filter excludes infeasible nodes, score selects the best node.
- Choose the right affinity type: Node affinity controls which nodes Pods schedule to; Pod affinity controls relationships between Pods.
- Use taints for isolation: Taints + tolerations implement node dedication, more flexible than nodeSelector.
- Topology spread for HA:
topologySpreadConstraintsis a more efficient HA solution than Pod anti-affinity. - Priority for preemption: Use high priority for core workloads, low priority for non-core—automatically ensures core workloads during resource pressure.
- Configuration over customization: Most scheduling needs can be met through scheduler configuration files without writing custom plugins.
- 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:
- Kubernetes Scheduler Documentation — Kubernetes Official, referenced for Kubernetes Scheduler Documentation