Chapter 40 of 47 · Scheduling and scaling

Scheduling: where Pods run

When you create a Pod, nobody tells it which machine to go to. The scheduler picks a node for it, a bit like a hotel receptionist picking a room. Most of the time its choice is fine. This chapter shows how it decides, and the knobs you can turn when you need a say: "only on SSD nodes", "never two copies on the same machine", "keep normal apps off the GPU nodes".

How the scheduler decides

The scheduler is a control-plane program (chapter 44). It watches for Pods that have no node yet, and for each one it does two rounds:

Filter, then scoreFour nodes enter the filter step. Two are removed because they do not fit. The remaining two get a score, and the node with the highest score is chosen. All nodes node-1 node-2 gpu-node control-plane 1 · Filter"Can it go here at all?"enough CPU/memory? labels?taints tolerated? 2 · Score"Which is best?"node-1: 62node-2: 87 node-2 ★
Filter throws out nodes that cannot work. Score ranks the rest. Highest score wins.

Try it: where can this Pod go?

Four nodes, each with labels and maybe a taint. Add rules to the Pod and watch nodes drop out:

Everything below is just a different way of feeding the filter or the score step.

nodeSelector: the simple filter

Nodes carry labels, just like Pods. Some come for free (kubernetes.io/hostname, topology.kubernetes.io/zone, kubernetes.io/arch); you can add your own:

$ kubectl label node node-1 disktype=ssd
$ kubectl get nodes -L disktype,topology.kubernetes.io/zone
NAME       STATUS   DISKTYPE   ZONE
node-1     Ready    ssd        zone-a
node-2     Ready    hdd        zone-b

Then ask for it in the Pod spec. Only nodes with all these labels pass:

spec:
  nodeSelector:
    disktype: ssd
  containers:
    - name: query
      image: micro-query:1.0

Simple, and enough for most cases. Its limit: it can only say "must equal". No "or", no "prefer".

Node affinity: must vs prefer

Node affinity is nodeSelector with more words. Its two flavours have long but honest names:

affinity:
  nodeAffinity:
    requiredDuringSchedulingIgnoredDuringExecution:
      nodeSelectorTerms:
        - matchExpressions:
            - key: topology.kubernetes.io/zone
              operator: In
              values: [zone-a, zone-b]       # either zone is fine
            - key: kubernetes.io/arch
              operator: NotIn
              values: [arm64]

A hard rule, part of the filter. No matching node → Pending. Operators like In, NotIn, Exists, Gt give you "or" and "not", which nodeSelector cannot.

"IgnoredDuringExecution" means: the rule is only checked when the Pod is placed. If someone later removes the label from the node, the running Pod stays where it is.

Pod affinity and anti-affinity

Sometimes the question is not "which node?" but "next to whom?". These rules look at the other Pods already on a node.

Anti-affinity: keep apart

Three posts replicas on one node means one node failure takes all three down. Ask for "never two posts Pods on the same node":

affinity:
  podAntiAffinity:
    requiredDuringSchedulingIgnoredDuringExecution:
      - labelSelector:
          matchLabels: { app: posts }
        topologyKey: kubernetes.io/hostname

Affinity: keep together

query talks to a Redis cache all the time. Prefer running on the same node as a cache Pod, to save network hops:

affinity:
  podAffinity:
    preferredDuringSchedulingIgnoredDuringExecution:
      - weight: 50
        podAffinityTerm:
          labelSelector:
            matchLabels: { app: redis }
          topologyKey: kubernetes.io/hostname

topologyKey decides what "the same place" means: kubernetes.io/hostname = same node, topology.kubernetes.io/zone = same zone (data centre). Same rule, different scale.

A required anti-affinity with 5 replicas on 3 nodes leaves 2 Pods Pending forever. For "spread them out", the preferred form or topology spread (next) is usually wiser.

Topology spread: share them out fairly

Anti-affinity only knows "together" or "apart". Topology spread constraints say "keep the numbers balanced": for example 6 comments Pods across 3 zones → 2, 2, 2.

spec:
  topologySpreadConstraints:
    - maxSkew: 1                               # zones may differ by at most 1 Pod
      topologyKey: topology.kubernetes.io/zone
      whenUnsatisfiable: ScheduleAnyway        # or DoNotSchedule (hard)
      labelSelector:
        matchLabels: { app: comments }
Six Pods spread across three zones with a skew of at most one zone-a zone-b zone-c comments comments comments comments comments comments
A whole zone can go down and you still have two thirds of your capacity.

Taints and tolerations: "keep out" signs

Everything so far is the Pod choosing nodes. A taint is the opposite: the node pushes Pods away. Only Pods with a matching toleration ("I'm OK with that") may come in. Think of a "staff only" sign: everyone stays out except people with a badge.

# the node says: keep out unless you need a GPU
$ kubectl taint nodes gpu-node gpu=true:NoSchedule

# the Pod carries the badge
spec:
  tolerations:
    - key: gpu
      operator: Equal
      value: "true"
      effect: NoSchedule
EffectMeans
NoScheduleNew Pods without the toleration are not placed here. Running ones stay.
PreferNoScheduleTry to avoid this node, but use it if there is nothing else.
NoExecuteNot placed here, and Pods already running without the toleration are evicted. Kubernetes itself uses this when a node becomes unreachable.
A toleration is permission, not an invitation. It lets a Pod onto the GPU node; it does not send it there. To make sure an ML job lands on GPU nodes and nobody else does, combine both: a taint on the node + a toleration and a nodeSelector on the Pod.

You see taints every day without noticing: control-plane nodes carry node-role.kubernetes.io/control-plane:NoSchedule, which is why your apps never land there.

$ kubectl describe node gpu-node | grep Taints
Taints:             gpu=true:NoSchedule

Priority and preemption: who goes first

When the cluster is full, some Pods matter more than others. A PriorityClass gives Pods a rank:

apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: business-critical
value: 100000              # higher = more important
description: "Core blog APIs: posts and comments"
---
spec:
  priorityClassName: business-critical
  containers:
    - name: posts
      image: micro-posts:1.0
  1. node-1: report-job (priority 0)   batch-import (priority 0)   … no room left

    Every node is full of low-priority work.

  2. posts-7d9f8-x2k   Pending   priority 100000

    A high-priority Pod arrives and fits nowhere.

  3. preempting report-job on node-1 …
    report-job   Terminating   (graceful shutdown, chapter 14)

    Preemption: the scheduler evicts lower-priority Pods to make room. They get their normal graceful shutdown.

  4. posts-7d9f8-x2k   Running   node-1
    report-job        Pending   (waits for space)

    The important Pod runs. The evicted one goes back in the queue.

Use priorities sparingly: if everything is "critical", nothing is. Kubernetes' own components use the built-in classes system-cluster-critical and system-node-critical. Priority is separate from QoS (chapter 22): priority decides who gets scheduled, QoS who gets evicted when a node runs out of memory.

Which tool when?

Placement is only decided once. As the cluster changes, Pods can end up badly spread. The optional descheduler add-on evicts such Pods now and then so the scheduler can place them again, better.

Recap

Quick check

A Pod is Pending with 0/3 nodes are available: 3 node(s) had untolerated taint. What do you do?
Show answer

Run kubectl describe node to see the taints. Either the Pod really belongs elsewhere (add nodes without that taint), or it should run there and needs a matching toleration.

You add a toleration for the GPU taint to the posts Deployment. Will posts now run on the GPU node?
Show answer

Maybe, maybe not. A toleration only allows it. The scheduler still picks by score. To force it there, also add a nodeSelector or required node affinity.

Pod scheduling: common questions

What is the difference between node affinity and taints?

Affinity is the Pod choosing nodes. A taint is the node keeping Pods away; only Pods with a matching toleration may run there.

Why is my Pod Pending with untolerated taint?

Every node carries a taint the Pod does not tolerate. Add a matching toleration if it should run there, or provide nodes without that taint.