Lesson 14 of 32 · Level 2 — Operator
Scheduling in depth
Control where pods land: node labels and affinity, taints and tolerations, spreading across zones, and priority with preemption. Plus how to read a Pending pod's events.
How the scheduler decides
For every pod without a node, the scheduler runs two phases:
- Filter: remove nodes that can't run the pod: not enough CPU or memory for its requests, a taint it doesn't tolerate, a nodeSelector or required affinity that doesn't match, a port conflict, a volume in another zone…
- Score: rank the nodes that are left, using spreading, preferred affinity and resource balance. The highest score wins, and the pod is bound to that node.
If the filter phase leaves zero nodes, the pod stays Pending and an event explains why.
Picture seating guests at a wedding. First you cross out tables that can't work: full tables, the kids' table for adults, the family table for strangers (filter). Then, from the tables left, you pick the best: near friends, not all cousins at one table (score). A taint is a "Reserved" sign on a table. Only guests with a matching invitation (toleration) can sit there.
Steering pods towards nodes
nodeSelector: the simple, hard rule.
spec:
nodeSelector:
disktype: ssd
Node affinity: more expressive, with required (hard) and preferred (soft) rules.
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/arch
operator: In
values: ["amd64"]
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 50
preference:
matchExpressions:
- key: disktype
operator: In
values: ["ssd"]
"IgnoredDuringExecution" means that if a node's labels change later, running pods are not moved.
Keeping pods away from nodes: taints and tolerations
A taint on a node repels pods. A toleration on a pod lets it ignore that taint.
$ kubectl taint node w1 dedicated=gpu:NoSchedule
node/w1 tainted
spec:
tolerations:
- key: dedicated
operator: Equal
value: gpu
effect: NoSchedule
| Effect | Meaning |
|---|---|
NoSchedule |
New pods without the toleration won't be scheduled here |
PreferNoSchedule |
Try to avoid, but allowed if needed |
NoExecute |
Also evicts running pods that don't tolerate it |
Your control-plane nodes already carry node-role.kubernetes.io/control-plane:NoSchedule, which is why normal pods never land there.
A toleration is permission, not a destination
Tolerating the GPU taint lets a pod use GPU nodes, but doesn't send it there. For truly dedicated nodes, combine a taint (keep others out) with node affinity (send these pods in).
Pods and other pods: affinity and anti-affinity
Keep replicas of the same app on different nodes, so one node failure doesn't take them all:
spec:
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
topologyKey: kubernetes.io/hostname
labelSelector:
matchLabels:
app: web
topologyKey defines what counts as "the same place": a hostname, a zone, a rack label. Pod affinity (the opposite) co-locates pods, such as a cache next to its app.
Spreading evenly: topology spread constraints
The modern, more precise way to spread replicas:
spec:
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: ScheduleAnyway # or DoNotSchedule for a hard rule
labelSelector:
matchLabels:
app: web
maxSkew: 1 means the busiest zone may have at most one more matching pod than the emptiest.
Priority and preemption
When the cluster is full, what matters most? A PriorityClass answers that:
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: business-critical
value: 100000
globalDefault: false
description: "Customer-facing services"
Pods with priorityClassName: business-critical are scheduled first, and if they can't fit, the scheduler may preempt (evict) lower-priority pods. Kubernetes' own critical add-ons use the built-in system-cluster-critical and system-node-critical classes.
Reading a Pending pod
$ kubectl describe pod web-7c9d8b6f5-x2kqz
...
Events:
Type Reason Message
---- ------ -------
Warning FailedScheduling 0/3 nodes are available: 1 node(s) had untolerated taint
{node-role.kubernetes.io/control-plane: }, 2 Insufficient memory.
Read it as a list: every node was filtered out, and each clause explains why. Here the control-plane node is tainted, and both workers lack the requested memory. The fixes are smaller requests, more nodes, or evicting something less important. Not removing the control-plane taint.
Try it: see each rule in action (kind lab)
- Label your workers as two zones:
kubectl label node lab-worker topology.kubernetes.io/zone=zone-aandkubectl label node lab-worker2 topology.kubernetes.io/zone=zone-b - Deploy
webwith 4 replicas and the topology spread constraint above. Checkkubectl get pods -o wide: 2 per zone. - Taint
lab-worker2withdedicated=gpu:NoExecute. Watch its pods get evicted and rescheduled ontolab-worker. - Remove the taint, then give a pod
nodeSelector: disktype: ssd. Read its FailedScheduling event, then label a node to fix it.
Going deeper: scheduling at scale
- Requests drive scheduling, not usage. A node at 20% real CPU can still be "full" if requests are over-generous. Right-sizing requests (lesson 23) is the biggest scheduling win.
- Prefer topology spread over pod anti-affinity for large Deployments. Required anti-affinity can make big rollouts unschedulable.
- Default cluster-wide spreading can be configured in the scheduler profile. Multiple scheduler profiles, or entirely custom schedulers, exist for special workloads (batch, GPU).
- Preemption respects PodDisruptionBudgets only on a best-effort basis. Keep priorities few and meaningful (for example critical / standard / batch), or everything becomes "critical".
Recap
- Scheduling = filter (can it run here?) then score (where is best?).
- nodeSelector / node affinity attract pods to nodes; taints / tolerations let nodes repel pods; use both for dedicated nodes.
- Anti-affinity and topology spread protect you from single-node and single-zone failures.
- PriorityClasses decide who wins when capacity runs out.
Pending→ read the FailedScheduling event; it lists every reason.
This site is a public version of my personal engineering knowledge hub. It intentionally excludes confidential company information and internal operational details.