46

Pod Priority and Preemption

CKA prep • PriorityClass, preemption flow, scheduler behavior, globalDefault, preemptionPolicy

Key terms

TermMeaning
PriorityClassA cluster object mapping a name to an integer priority value
priorityThe integer used to rank pending pods and decide eviction
PreemptionEvicting lower-priority pods to make room for a higher-priority one
globalDefaultThe PriorityClass applied to pods that name none
preemptionPolicyPreemptLowerPriority (default) or Never
PendingPod state while the scheduler cannot place it
VictimA lower-priority pod chosen for eviction during preemption

Problem & solution

When a cluster fills up, which pod wins the last free CPU/memory — your Prometheus monitoring stack or a nightly batch job? Without priority, scheduling is essentially first-come. Pod priority ranks pending pods, and preemption lets a high-priority pod (here, Prometheus) evict lower-priority ones when there is otherwise no room.

Solution: Define PriorityClasses, stamp pods with a priorityClassName, and let the scheduler order the pending queue by priority and preempt lower-priority victims when a high-priority pod cannot otherwise be placed.

The analogy

When the last berth at a full port is needed by a VIP cargo ship, the harbor master ranks the waiting ships and bumps a lower-priority vessel off the berth so the VIP can dock, the displaced ship must wait or move on. Lower-ranked cargo never displaces equal or higher-ranked cargo. Kubernetes scheduling mirrors this: a PriorityClass ranks pods, the scheduler orders the pending queue by priority, and when a high-priority pod (Prometheus) cannot fit on a full node it preempts, evicting the lowest-priority victim to make room.

Graph legend — each Kubernetes node maps a port concept to the real Prometheus preemption case:

Graph nodeMaps toWhat it does
Pod prometheusPod with priorityClassName: high-priorityThe critical monitoring pod that must be scheduled
kube-scheduler ordering by prioritykube-schedulerSorts the pending queue by .spec.priority
full nodea Node with no free CPU/memoryThe contended resource Prometheus needs
preemption evicts a batch victimscheduler preemption stepRemoves the lowest-priority batch pod to make room

Where priority fits in scheduling

The scheduler does not place pods at random — it runs each pending pod through a short pipeline (queue, filter, score, pick), and priority is what decides a pod's place in line. The diagram below shows where priority and preemption (evicting lower-ranked pods to make room) slot into that cycle: a high-priority pod like Prometheus jumps to the front of the queue and, if no node fits, triggers the preemption step.

Graph legend — each node is a stage of the scheduling cycle for the Prometheus pod:

Graph nodeMaps toWhat it does
pending queue ordered by priorityscheduler's active queueDequeues high-priority pods (Prometheus) before low ones
filter nodes, fit?scheduler Filter phaseDrops nodes that cannot satisfy requests
score nodesscheduler Score phaseRanks the surviving feasible nodes
pick bestscheduler Bind decisionSelects the top-scoring node
preemption, evict lower-priority victimsscheduler PostFilterFrees room by evicting lower-priority pods when none fit

Define PriorityClasses

A PriorityClass is cluster-scoped. Higher value = higher priority. Values above one billion are reserved for system-critical classes (system-cluster-critical, system-node-critical).

apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: high-priority
value: 1000000
globalDefault: false
description: "Critical platform workloads (Prometheus, Alertmanager)"
---
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: low-priority
value: 1000
globalDefault: false
preemptionPolicy: Never        # this pod waits, never evicts others
description: "Best-effort batch jobs"
kubectl get priorityclass                # built-ins + yours
kubectl describe priorityclass high-priority

Assign priority to a pod

Reference the class by name; the admission controller stamps the numeric priority onto the pod spec. Here a real Prometheus server claims high priority.

apiVersion: v1
kind: Pod
metadata:
  name: prometheus
  labels: { app: prometheus }
spec:
  priorityClassName: high-priority
  containers:
    - name: prometheus
      image: prom/prometheus:v2.54.0
      args: ["--config.file=/etc/prometheus/prometheus.yml"]
      ports:
        - { containerPort: 9090, name: web }
      resources:
        requests:
          cpu: "1"
          memory: 1Gi
kubectl get pod prometheus -o jsonpath='{.spec.priority}{"\n"}'   # 1000000

globalDefault and preemptionPolicy

Two fields decide how a class behaves when pods omit a priority and whether it may evict others. Here is what each one does:

   globalDefault: true   -> pods WITHOUT a priorityClassName get this value
                            (only ONE class should set this; default otherwise = 0)
   preemptionPolicy:
     PreemptLowerPriority (default) -> may evict lower-priority pods to fit
     Never                          -> scheduled by priority order, but NEVER evicts;
                                       useful for high-priority but non-urgent jobs

How preemption chooses victims

When the Prometheus pod is Pending and no node fits, the scheduler looks for a node where evicting one or more lower-priority pods would let it fit, picking the set with the least disruption. Victims get a graceful termination.

Graph legend — each node is a real element of the victim-selection step:

Graph nodeMaps toWhat it does
node-1 fulla saturated NodeHolds low/mid-priority pods using all CPU
incoming prometheusthe high-priority Pending podTriggers preemption because nothing fits
scheduler evicts fewest, lowest-priority victimspreemption logicSelects minimal lowest-priority set to evict
node-1 afterthe node post-preemptionNow runs the higher-priority pods including Prometheus
kubectl get events --field-selector reason=Preempted
kubectl describe pod prometheus | grep -i preempt

End-to-end: a high-priority pod preempts to schedule

This diagram traces the full life of one high-priority pod, from the moment you create it to the moment it lands on a node. The key branch is what happens when no node has room: depending on the pod's preemptionPolicy (whether it is allowed to evict others), it either waits or forces lower-priority pods off a node to take their place.

Graph legend — each node is a real step from pod creation to scheduling:

Graph nodeMaps toWhat it does
Create pod prometheuspriorityClassName: high-prioritySubmits the critical monitoring pod
Admission stamps .spec.priorityPriority admission controllerResolves the class name to the integer 1000000
Scheduler orders pending queuekube-schedulerPlaces Prometheus ahead of lower-priority pods
preemptionPolicy?.spec.preemptionPolicyDecides whether the pod may evict others
Set victims deletionTimestamppreemption evictionGracefully terminates the chosen low-priority pods
Nominate node for prometheus.status.nominatedNodeNameReserves the freed node for the next scheduling cycle

End-to-end example: fill a node, then preempt with Prometheus

A complete walkthrough: define two PriorityClasses, saturate a node with low-priority batch pods, then schedule the high-priority Prometheus server that cannot fit and watch the scheduler evict a low-priority victim to make room.

  1. Create the two PriorityClasses:
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: low-priority
value: 1000
globalDefault: false
description: "Best-effort batch jobs"
---
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: high-priority
value: 1000000
globalDefault: false
description: "Critical platform workloads (Prometheus)"
kubectl get priorityclass low-priority high-priority
# expected: both listed with values 1000 and 1000000
  1. Pick one node and size requests so three low-priority batch pods nearly fill its allocatable CPU:
kubectl get nodes
NODE=$(kubectl get nodes -o jsonpath='{.items[0].metadata.name}')
kubectl describe node "$NODE" | grep -A4 Allocatable   # note cpu, e.g. 4
# low-fill.yaml  -> 3 replicas, each requesting ~1 CPU, pinned to the chosen node
apiVersion: apps/v1
kind: Deployment
metadata:
  name: batch-filler
spec:
  replicas: 3
  selector:
    matchLabels: { app: batch-filler }
  template:
    metadata:
      labels: { app: batch-filler }
    spec:
      priorityClassName: low-priority
      nodeName: NODE_PLACEHOLDER
      containers:
        - name: pause
          image: registry.k8s.io/pause:3.9
          resources:
            requests:
              cpu: "1"
  1. Substitute the node name and apply; confirm the node is now nearly full:
sed "s/NODE_PLACEHOLDER/$NODE/" low-fill.yaml | kubectl apply -f -
kubectl get pods -l app=batch-filler -o wide
# expected: 3 batch-filler pods Running on $NODE

kubectl describe node "$NODE" | grep -A3 "Allocated resources"
# expected: cpu requests near 100% of allocatable
  1. Schedule the Prometheus pod with high priority and a request that cannot fit without eviction:
apiVersion: v1
kind: Pod
metadata:
  name: prometheus
  labels: { app: prometheus }
spec:
  priorityClassName: high-priority
  nodeName: NODE_PLACEHOLDER
  containers:
    - name: prometheus
      image: prom/prometheus:v2.54.0
      args: ["--config.file=/etc/prometheus/prometheus.yml"]
      resources:
        requests:
          cpu: "1"
sed "s/NODE_PLACEHOLDER/$NODE/" prometheus.yaml | kubectl apply -f -
  1. Watch the scheduler preempt a low-priority victim, then bind Prometheus:
kubectl get events --field-selector reason=Preempted
# expected: pod/batch-filler-xxxx Preempted by default/prometheus on node $NODE

kubectl get pods -l app=batch-filler
# expected: one batch-filler pod gone (or Pending, recreated by the Deployment)

kubectl get pod prometheus -o wide
# expected: prometheus Running on $NODE
  1. Inspect the priority that was stamped and confirm equal/higher pods are never victims:
kubectl get pod prometheus -o jsonpath='{.spec.priority}{"\n"}'   # 1000000
kubectl describe pod prometheus | grep -i preempt
# expected: nominated node + preemption note for the victim

Graph legend — each node is a real step in the fill-then-preempt walkthrough:

Graph nodeMaps toWhat it does
3 low-priority batch-filler pods fill node CPUthe batch-filler DeploymentSaturates the node with low-priority pods
Create prometheushigh-priority PodThe critical pod that cannot fit
Admission stamps .spec.priorityPriority admission controllerResolves the class to 1000000
Scan nodes for lower-priority victimsscheduler PostFilterLooks for evictable lower-priority pods
Pick fewest/lowest-priority batch podsvictim selectionMinimizes disruption while freeing room
Evict victim: set deletionTimestampgraceful evictionTerminates a batch-filler pod
Nominate node for prometheus.status.nominatedNodeNameReserves the node for Prometheus on the next cycle

Common pitfalls

These are the priority and preemption mistakes that bite most often:

   - priorityClassName typo        -> pod rejected; the class must already exist
   - two globalDefault classes     -> only one may set globalDefault: true
   - surprise evictions            -> a high-priority pod silently preempted batch jobs
   - editing priority on a pod     -> immutable after creation; recreate the pod
   - PDB ignored under pressure    -> PDBs are best-effort during preemption
   - values >1e9                   -> reserved for system-critical classes

Key takeaways

  • A PriorityClass maps a name to an integer; higher = scheduled first.
  • Pods reference it via priorityClassName; the value is immutable.
  • globalDefault sets the priority for pods that name no class (else 0).
  • Preemption evicts the fewest, lowest-priority victims to fit a higher-priority pod.
  • preemptionPolicy: Never lets a pod jump the queue but never evict others.
  • Values above one billion are reserved for system-critical workloads.

Checklist

  • [ ] Created high- and low-priority PriorityClasses
  • [ ] Assigned priorityClassName to the Prometheus pod and read .spec.priority
  • [ ] Filled a node and watched Prometheus preempt a batch victim
  • [ ] Set preemptionPolicy: Never and confirmed it never evicts
  • [ ] Explained globalDefault and the system-critical reserved range