Doel
Na dit artikel weet je hoe je taints, tolerations, node affinity, pod anti-affinity en topology spread constraints inzet om precies te bepalen welke pods op welke nodes draaien in een Kubernetes-cluster.
Vereisten
- Een Kubernetes-cluster op v1.28 of nieuwer met
kubectl-toegang en rechten om nodes te tainten en Deployments aan te maken - Minimaal twee nodes in het cluster (schedulingregels hebben weinig zin met één node)
- Bekendheid met Kubernetes Services en pod-labels. Als een pod op Pending blijft staan na het toepassen van deze regels, dan beschrijft de pod Pending-gids elke schedulerfout in detail.
Taints en tolerations: pods weren van nodes
Een taint is een eigenschap op een node die pods weert, tenzij een pod de taint expliciet tolereert. Zie het als een "verboden toegang"-bord. Een toleration op een pod zegt "ik kan daar wel mee overweg."
Een taint heeft drie delen: een key, een optionele value en een effect.
Taints toevoegen en verwijderen
# Taint toevoegen aan een node
kubectl taint nodes gpu-node-1 nvidia.com/gpu=present:NoSchedule
# Taints op een node controleren
kubectl describe node gpu-node-1 | grep Taints
# Specifieke taint verwijderen (let op het min-teken)
kubectl taint nodes gpu-node-1 nvidia.com/gpu=present:NoSchedule-
De drie taint-effecten
NoSchedule is het meest gebruikt. Nieuwe pods zonder matchende toleration komen niet op de node terecht. Pods die al draaien worden niet verwijderd. Gebruik dit voor het reserveren van GPU-nodes, spot-pools of tenant-dedicated hardware.
PreferNoSchedule is de zachte variant. De scheduler probeert de node te vermijden maar plaatst pods er toch als er geen alternatief is. Bestaande pods blijven staan. Handig als je verkeer wilt wegsturen van bepaalde nodes zonder hard te blokkeren.
NoExecute is het strengst. Pods zonder matchende toleration worden direct verwijderd (evicted). Nieuwe pods zonder toleration kunnen ook niet schedulen. Dit is het effect dat de node lifecycle controller automatisch toepast als een node not-ready of unreachable wordt.
Als een node meerdere taints heeft, evalueert Kubernetes ze als gecombineerd filter: als een ongematchte taint NoSchedule heeft, wordt de pod geblokkeerd. Als alleen PreferNoSchedule-taints ongemacht zijn, vermijdt de scheduler de node maar kan er toch op plaatsen. Als een ongematchte taint NoExecute heeft, worden draaiende pods verwijderd.
Tolerations schrijven
Tolerations staan in spec.tolerations op de pod (of in de pod-template van een Deployment). Twee operators bepalen de matching:
Equal matcht een specifieke key, value en effect:
tolerations:
- key: "nvidia.com/gpu"
operator: "Equal"
value: "present"
effect: "NoSchedule"
Exists matcht elke value voor de opgegeven key:
tolerations:
- key: "nvidia.com/gpu"
operator: "Exists"
effect: "NoSchedule"
Een lege key met Exists tolereert alle taints op alle keys. DaemonSets als monitoring-agents gebruiken dit soms zodat ze op elke node draaien, ongeacht taints. Wees er zuinig mee.
tolerationSeconds bij NoExecute
Als een NoExecute-taint aan een node wordt toegevoegd (tijdens onderhoud of bij een nodecondition), kunnen pods met een matchende toleration aangeven hoe lang ze mogen blijven staan voor eviction:
tolerations:
- key: "node.kubernetes.io/not-ready"
operator: "Exists"
effect: "NoExecute"
tolerationSeconds: 120 # pod krijgt 2 minuten om netjes af te sluiten
Ingebouwde automatische taints
De node lifecycle controller voegt automatisch taints toe bij bepaalde condities. Dit zijn de meest voorkomende:
| Taint | Trigger | Effect |
|---|---|---|
node.kubernetes.io/not-ready |
Node Ready-conditie is False | NoExecute |
node.kubernetes.io/unreachable |
Node Ready-conditie is Unknown | NoExecute |
node.kubernetes.io/memory-pressure |
MemoryPressure-conditie | NoSchedule |
node.kubernetes.io/disk-pressure |
DiskPressure-conditie | NoSchedule |
node.kubernetes.io/pid-pressure |
PIDPressure-conditie | NoSchedule |
De DaemonSet-controller voegt automatisch tolerations toe voor deze ingebouwde taints, zodat systeempods (CNI-plugins, logcollectors, monitoring-agents) blijven draaien. Maar als je eigen taints aan nodes toevoegt, tolereert een DaemonSet die niet automatisch. Dan moet je de toleration expliciet aan de DaemonSet-spec toevoegen.
Node affinity: pods naar nodes trekken
Taints weren. Node affinity doet het omgekeerde: het trekt pods naar nodes met specifieke labels. Het vervangt de oudere nodeSelector met expressievere matching.
Required vs. preferred
requiredDuringSchedulingIgnoredDuringExecution is een harde regel. De pod blijft Pending als geen enkele node matcht. IgnoredDuringExecution betekent dat de pod niet verwijderd wordt als nodelabels veranderen nadat de pod is ingepland.
preferredDuringSchedulingIgnoredDuringExecution is een zachte voorkeur. De scheduler probeert een match te vinden maar plant de pod ergens anders in als dat niet lukt. Elke voorkeur heeft een weight tussen 1 en 100. Hoger gewicht betekent sterkere voorkeur.
YAML-voorbeeld
apiVersion: v1
kind: Pod
metadata:
name: gpu-training-job
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: nvidia.com/gpu.present # moet een GPU hebben
operator: In
values:
- "true"
- key: topology.kubernetes.io/zone # en in een van deze zones
operator: In
values:
- eu-west-1a
- eu-west-1b
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 80
preference:
matchExpressions:
- key: node.kubernetes.io/instance-type
operator: In
values:
- p4d.24xlarge # voorkeur voor de grotere GPU-instance
containers:
- name: training
image: ml-training:v2.3
resources:
limits:
nvidia.com/gpu: 1
Operators en termlogica
Node affinity ondersteunt zes operators: In, NotIn, Exists, DoesNotExist, Gt en Lt. De laatste twee vergelijken integer-labelwaarden.
Meerdere nodeSelectorTerms worden ge-ORd: de pod wordt ingepland als een van de terms matcht. Meerdere expressies binnen een enkele matchExpressions-lijst worden ge-ANDd: allemaal moeten matchen. Als je nodeSelector en nodeAffinity combineert, moeten beide voldoen.
Veelgebruikte nodelabels
Deze labels worden automatisch gezet op de meeste cloud-provisioned nodes en zijn veilig om te gebruiken als affinity-target:
kubernetes.io/arch: amd64 # of arm64
kubernetes.io/os: linux # of windows
node.kubernetes.io/instance-type: m5.large
topology.kubernetes.io/region: eu-west-1
topology.kubernetes.io/zone: eu-west-1a
De volledige lijst staat in de Kubernetes-referentiedocumentatie.
Waarom je zowel taints als affinity nodig hebt voor exclusieve plaatsing
Dit is het punt dat de meeste gidsen overslaan. Een toleration alleen laat een pod op een getainte node draaien, maar niets voorkomt dat diezelfde pod op een niet-getainte node terechtkomt. Node affinity alleen trekt een pod naar specifieke nodes, maar blokkeert andere pods niet van die nodes.
Voor exclusieve plaatsing (GPU-pods alleen op GPU-nodes, niets anders op GPU-nodes) heb je beide nodig:
- Taint de node zodat niet-GPU-pods er niet op kunnen landen
- Voeg een toleration toe zodat GPU-pods wel mogen
- Voeg node affinity toe zodat GPU-pods er ook daadwerkelijk naartoe gestuurd worden
Zonder alle drie komt ofwel de verkeerde pod op de node, ofwel de juiste pod op de verkeerde node.
Pod affinity en anti-affinity
Inter-pod affinity en anti-affinity sturen scheduling op basis van labels van pods die al draaien op een node, niet labels op de node zelf.
topologyKey
Het topologyKey-veld bepaalt wat "dezelfde locatie" betekent. Het is een nodelabel-key waarvan de waarde het topologiedomein definieert:
| topologyKey | Betekenis |
|---|---|
kubernetes.io/hostname |
Dezelfde node |
topology.kubernetes.io/zone |
Dezelfde beschikbaarheidszone |
topology.kubernetes.io/region |
Dezelfde regio |
Pod affinity: co-locatie voor latency
Dwing een pod af op dezelfde node als een Redis-cache waar hij van afhankelijk is:
spec:
affinity:
podAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- redis-cache
topologyKey: kubernetes.io/hostname
Pod anti-affinity: spreiden voor beschikbaarheid
Voorkom dat twee replica's van dezelfde Deployment op dezelfde node terechtkomen:
spec:
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- web-server
topologyKey: kubernetes.io/hostname
Gebruik preferredDuringSchedulingIgnoredDuringExecution in plaats van required als het aantal replica's het aantal nodes kan overschrijden. Een harde anti-affinity-regel met 5 replica's en 3 nodes laat 2 pods voor altijd op Pending staan.
Performanceopmerking
Pod affinity en anti-affinity vereisen dat de scheduler pod-labels over het hele cluster controleert bij elke schedulingcyclus. In clusters met honderden nodes of duizenden pods voegt dit aanzienlijke latency toe aan schedulingbeslissingen. Voor simpelweg spreiden zijn topology spread constraints efficienter.
Topology spread constraints
Topology spread constraints verdelen pods gelijkmatig over faaldomein (zones, nodes, regio's) zonder de per-pod label-scanning overhead van pod anti-affinity.
Kernvelden
spec:
topologySpreadConstraints:
- maxSkew: 1 # max verschil in podcount tussen domeinen
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule # of ScheduleAnyway
labelSelector:
matchLabels:
app: web
maxSkew is het maximaal toegestane verschil in podcount tussen het drukste en het leegste topologiedomein. Moet groter zijn dan 0. Een maxSkew van 1 met 3 zones en 6 replica's betekent een 2-2-2-verdeling.
whenUnsatisfiable bepaalt wat er gebeurt als de constraint niet gehaald kan worden. DoNotSchedule houdt de pod op Pending (gebruik dit voor HA-kritieke services). ScheduleAnyway plaatst de pod toch maar geeft voorkeur aan nodes die de scheefheid minimaliseren.
minDomains (GA sinds Kubernetes 1.30) stelt het minimumaantal topologiedomeinen in dat moet bestaan voordat de constraint geldt. Alleen geldig met DoNotSchedule.
Dubbele constraint: spreiden over zones en nodes
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone # strikte zonebalans
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app: web
- maxSkew: 2
topologyKey: kubernetes.io/hostname # zachte nodebalans
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels:
app: web
Dit geeft strikte zoneverdeling (geen zone heeft meer dan 1 extra pod) terwijl er wat nodeongelijkheid binnen een zone mag zijn. In de praktijk is dit het patroon dat ik het vaakst zie in productieclusters met stateless webservices.
Praktijkpatronen
GPU-node-isolatie
Dure GPU-nodes moeten alleen GPU-workloads draaien. De combinatie van taint + toleration + affinity dwingt dit af:
# GPU-nodes labelen en tainten
kubectl label nodes gpu-node-1 accelerator=nvidia-a100
kubectl taint nodes gpu-node-1 nvidia.com/gpu=present:NoSchedule
GKE voegt automatisch de taint nvidia.com/gpu=present:NoSchedule toe als je GPU-nodepools aanmaakt.
De GPU-Deployment:
apiVersion: apps/v1
kind: Deployment
metadata:
name: ml-training
spec:
replicas: 2
selector:
matchLabels:
app: ml-training
template:
metadata:
labels:
app: ml-training
spec:
tolerations:
- key: "nvidia.com/gpu"
operator: "Equal"
value: "present"
effect: "NoSchedule"
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: accelerator
operator: In
values:
- nvidia-a100
containers:
- name: training
image: ml-training:v2.3
resources:
limits:
nvidia.com/gpu: 1 # GPU-resources alleen in limits, niet in requests
GPU-resources moeten in limits staan, niet in requests. De NVIDIA-, AMD- of Intel-deviceplugin moet geinstalleerd zijn op het cluster.
Spot / preemptible node-isolatie
Fouttolerante batchworkloads draaien op goedkopere spot-instances. Kritieke workloads blijven op on-demand-nodes. Elke cloudprovider gebruikt andere taint-keys:
| Cloud | Spot-nodelabel | Automatische taint |
|---|---|---|
| AKS | kubernetes.azure.com/scalesetpriority=spot |
kubernetes.azure.com/scalesetpriority=spot:NoSchedule |
| GKE | cloud.google.com/gke-spot=true |
cloud.google.com/gke-spot=true:NoSchedule |
| EKS | eks.amazonaws.com/capacityType=SPOT |
Custom (meestal spot=true:NoSchedule) |
Een batchworker die spot-nodes tolereert en eviction netjes afhandelt:
apiVersion: apps/v1
kind: Deployment
metadata:
name: batch-worker
spec:
replicas: 8
selector:
matchLabels:
app: batch-worker
template:
metadata:
labels:
app: batch-worker
spec:
tolerations:
- key: "spot"
operator: "Equal"
value: "true"
effect: "NoSchedule"
- key: "node.kubernetes.io/not-ready"
operator: "Exists"
effect: "NoExecute"
tolerationSeconds: 120 # 2 minuten respijt als spot wordt teruggenomen
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: node.kubernetes.io/capacity-type
operator: In
values:
- spot
containers:
- name: worker
image: batch-processor:v1.8
Combineer met een PodDisruptionBudget om te beperken hoeveel pods tegelijk onbeschikbaar mogen zijn als spot-nodes worden teruggenomen:
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: batch-worker-pdb
spec:
minAvailable: 2
selector:
matchLabels:
app: batch-worker
Gebruik je Karpenter in plaats van de cluster autoscaler, definieer dan aparte NodePools voor spot en on-demand met verschillende karpenter.sh/capacity-type-requirements en bijbehorende taints.
Multi-tenant dedicated nodes
Elke tenant krijgt nodes waar alleen hun workloads op draaien:
kubectl label nodes tenant-a-node-1 tenant=team-alpha
kubectl taint nodes tenant-a-node-1 tenant=team-alpha:NoSchedule
spec:
tolerations:
- key: "tenant"
operator: "Equal"
value: "team-alpha"
effect: "NoSchedule"
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: tenant
operator: In
values:
- team-alpha
Een belangrijk voorbehoud: taints en tolerations zijn geen securitygrens. Een verkeerd geconfigureerde pod kan gewoon een matchende toleration toevoegen en de taint omzeilen. Voor echte multi-tenant-isolatie dwing je tolerationbeperkingen af met een policy-engine als Kyverno of OPA/Gatekeeper. Voor labelsecurity gebruik je het prefix node-restriction.kubernetes.io/ zodat kubelet die labels niet zelf kan aanpassen.
Resultaat verifieren
Na het toepassen van taints, tolerations en affinity-regels verifieer je of alles goed terechtgekomen is:
# Controleer alle taints op alle nodes
kubectl get nodes -o custom-columns=NAME:.metadata.name,TAINTS:.spec.taints
# Bevestig dat de pod op de verwachte node draait
kubectl get pod <pod-name> -o wide
# Als een pod op Pending blijft staan, lees de schedulerevents
kubectl describe pod <pod-name>
De Events-sectie in kubectl describe pod laat precies zien welk schedulerfilter faalde. Zoek naar berichten als 0/5 nodes are available: 3 node(s) had untolerated taint {nvidia.com/gpu: present}. De pod Pending-gids beschrijft elke schedulerfoutmelding in detail.
Veelvoorkomende problemen
| Symptoom | Waarschijnlijke oorzaak | Oplossing |
|---|---|---|
| Pod Pending met "untolerated taint" | Ontbrekende toleration in podspec | Voeg de matchende toleration toe |
| Pod Pending met "didn't match node affinity" | Node mist het vereiste label | Voeg het label toe aan de node of versoepel naar preferred |
| Pod Pending met "didn't match pod anti-affinity" | Meer replica's dan nodes | Schakel over naar preferred anti-affinity of voeg nodes toe |
| Pod komt op verkeerde node ondanks affinity | Toleration zonder node affinity | Voeg node affinity toe om de pod te sturen, niet alleen toe te staan |
| DaemonSet draait niet op getainte node | Custom taint zonder toleration in DaemonSet | Voeg de custom taint-toleration toe aan de DaemonSet-spec |