4.2 Advanced Scheduling: Affinity, Topology Spread, Priority & Disruption Budgets
Key Takeaways
- Pod affinity attracts Pods to nodes already running matching Pods, while pod anti-affinity repels them — both are evaluated against a topologyKey such as kubernetes.io/hostname or topology.kubernetes.io/zone.
- topologySpreadConstraints distribute replicas evenly across failure domains using maxSkew, and are the modern replacement for hand-written anti-affinity rules.
- A PriorityClass assigns a numeric priority to Pods; when the cluster is full, the scheduler may preempt lower-priority Pods to make room for a higher-priority pending Pod.
- A PodDisruptionBudget limits how many replicas may be voluntarily disrupted at once, protecting availability during node drains and cluster upgrades.
- PodDisruptionBudgets constrain only voluntary disruptions such as kubectl drain — they cannot prevent involuntary disruptions like hardware failure or an OOM kill.
4.2 Advanced Scheduling: Affinity, Topology Spread, Priority & Disruption Budgets
Quick Answer:
nodeSelectorand node affinity decide which node a Pod can land on. The controls in this section decide how Pods relate to each other: pod affinity/anti-affinity co-locate or separate workloads relative to atopologyKey; topology spread constraints enforce an even distribution across zones or nodes usingmaxSkew; PriorityClasses let an urgent Pod preempt a less important one when capacity runs out; and PodDisruptionBudgets cap how many replicas may be taken down voluntarily at once.
Section 4.1 covered placement against node properties. Real availability engineering needs placement against other Pods — three replicas that all land on one node are not highly available, no matter what the replica count says.
1. Pod Affinity and Anti-Affinity
Where node affinity matches node labels, pod affinity matches the labels of Pods already running, evaluated within a topology domain.
podAffinity— schedule this Pod near Pods matching a selector. Used for latency-sensitive pairs, for example a web tier co-located with its cache in the same zone.podAntiAffinity— schedule this Pod away from Pods matching a selector. Used to spread replicas so a single node or zone failure cannot take out the whole service.
topologyKey Is the Whole Idea
The topologyKey names the node label that defines the boundary of "near" and "away":
topologyKey | Domain it defines | Typical use |
|---|---|---|
kubernetes.io/hostname | A single node | Never put two replicas on one machine |
topology.kubernetes.io/zone | An availability zone | Survive an AZ outage |
topology.kubernetes.io/region | A cloud region | Multi-region distribution |
spec:
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchLabels:
app: payment-api
topologyKey: kubernetes.io/hostname
This says: do not place this Pod on a node that already runs a Pod labelled app: payment-api. With required…, a 4-replica Deployment on a 3-node cluster will leave one Pod Pending forever — the rule is hard. Switching to preferredDuringSchedulingIgnoredDuringExecution with a weight makes it a soft preference that the scheduler honours when it can.
Cost warning: pod affinity rules are evaluated against every candidate node for every pending Pod, which is computationally expensive. On large clusters, prefer topology spread constraints.
2. Topology Spread Constraints
topologySpreadConstraints express the common intent — spread these replicas evenly — declaratively, and far more cheaply than anti-affinity.
spec:
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app: payment-api
| Field | Meaning |
|---|---|
maxSkew | The maximum permitted difference between the most-populated and least-populated domain. maxSkew: 1 across three zones means counts like 2/2/1 are fine but 3/1/1 is not. |
topologyKey | The node label that defines a domain (zone, hostname, rack). |
whenUnsatisfiable | DoNotSchedule (hard — Pod stays Pending) or ScheduleAnyway (soft — scheduler prefers a balanced placement but will not block). |
labelSelector | Which existing Pods are counted when measuring skew. |
Anti-affinity says "not here"; spread constraints say "keep the distribution flat". For a 9-replica service across 3 zones, spread constraints give you 3/3/3, which hostname anti-affinity alone cannot express.
3. Priority and Preemption
A PriorityClass is a cluster-scoped object mapping a name to an integer. Higher numbers mean higher priority.
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: mission-critical
value: 1000000
globalDefault: false
preemptionPolicy: PreemptLowerPriority
description: "Payment and auth services only."
A Pod references it with spec.priorityClassName: mission-critical. Priority does two things:
- Scheduling order — pending Pods are dequeued highest-priority first.
- Preemption — if a high-priority Pod cannot be scheduled anywhere, the scheduler looks for a node where evicting one or more lower-priority Pods would make room, and evicts them. Preempted Pods are terminated gracefully and go back to the scheduling queue.
Setting preemptionPolicy: Never gives a Pod scheduling precedence in the queue without letting it evict anything — the right setting for batch work that should be prioritised but must never disturb serving traffic.
Kubernetes ships two built-in classes, system-cluster-critical and system-node-critical, used by control plane and node add-ons so that CNI and DNS Pods are never evicted to make room for application workloads.
4. PodDisruptionBudgets
Kubernetes distinguishes two kinds of disruption, and only one of them can be budgeted:
| Kind | Examples | Can a PDB stop it? |
|---|---|---|
| Voluntary | kubectl drain, node upgrade, cluster autoscaler scale-down, preemption, deliberate Pod deletion by a controller | Yes |
| Involuntary | Hardware failure, kernel panic, node running out of memory, someone unplugging a cable | No |
A PodDisruptionBudget declares the availability floor the cluster must respect while performing voluntary disruptions:
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: payment-api-pdb
spec:
minAvailable: 2 # or: maxUnavailable: 1
selector:
matchLabels:
app: payment-api
With minAvailable: 2 and three replicas, kubectl drain on the node holding one replica succeeds; a drain that would take the count to 1 blocks until a replacement Pod is Ready elsewhere. This is precisely what turns a node-by-node cluster upgrade from an outage into a non-event.
Exam trap: a PDB set to
minAvailable: 3on a Deployment withreplicas: 3makes every node drain block forever — there is no slack. Budgets need headroom.
5. Choosing the Right Control
| Goal | Mechanism |
|---|---|
| Run only on GPU nodes | nodeSelector or node affinity |
| Keep application Pods off control plane nodes | Taints on the node, tolerations on the Pod |
| Never put two replicas on one node | podAntiAffinity with topologyKey: kubernetes.io/hostname |
| Keep replicas balanced across three zones | topologySpreadConstraints with maxSkew: 1 |
| Guarantee the payment service gets capacity first | PriorityClass with a high value |
| Keep two replicas alive throughout an upgrade | PodDisruptionBudget with minAvailable: 2 |
| Co-locate a cache with its consumer | podAffinity with a zone topologyKey |
A three-replica Deployment uses podAntiAffinity with requiredDuringSchedulingIgnoredDuringExecution and topologyKey: kubernetes.io/hostname. What happens if the cluster has only two schedulable nodes?
A PodDisruptionBudget specifies minAvailable: 2 for a service with three replicas. Which situation does it actually protect against?
What does maxSkew: 1 mean in a topologySpreadConstraint using topology.kubernetes.io/zone?