☸️ Kubernetes Lektion 17/20 ~9 Min. Fortgeschritten

Scheduling: Affinity, Taints, Priorities

Erweiterte Pod-Platzierung.

📝 Meine Notizen

Lernziele

Nach dieser Lektion kannst du erklären, wie der kube-scheduler in zwei Phasen (Filtern, Bewerten) einen Node auswählt und mit welchen Werkzeugen du diese Entscheidung steuerst: nodeSelector und nodeAffinity ziehen Pods auf bestimmte Nodes, Taints und Tolerations halten Pods von Nodes fern, podAffinity/podAntiAffinity ordnen Pods relativ zueinander an, und PriorityClasses entscheiden per Preemption, wer bei Knappheit weichen muss. Du kannst für jedes Werkzeug ein realistisches Einsatzszenario nennen — das ist es, was Interviews abfragen.

Wie der Scheduler entscheidet — und warum das oft nicht reicht

Der kube-scheduler beobachtet Pods ohne Node-Zuweisung und wählt in zwei Phasen einen Node: Filtering wirft alle Nodes raus, auf denen der Pod nicht laufen kann (zu wenig freie CPU/Memory gemessen an den Requests, fehlende Volumes in der Zone, nicht tolerierte Taints), Scoring bewertet die verbleibenden und nimmt den besten. Das Ergebnis landet als nodeName im Pod — ab da übernimmt das kubelet. Findet das Filtering keinen Kandidaten, bleibt der Pod Pending, und kubectl describe pod listet präzise auf, warum welcher Node ausschied — die wichtigste Informationsquelle bei Scheduling-Problemen.

Per Default ist dem Scheduler egal, welcher passende Node es wird. In echten Clustern ist es das oft nicht: GPU-Workloads gehören auf die teuren GPU-Nodes (und nur die!), Datenbank-Replicas dürfen nicht gemeinsam auf einem Node sterben, ein Team-Nodepool soll exklusiv bleiben, und bei Knappheit soll der Batch-Job weichen, nicht das Checkout-Backend. Für jedes dieser Probleme gibt es ein eigenes Primitive — die Kunst ist, sie nicht zu verwechseln.

Pods anziehen: nodeSelector und nodeAffinity

nodeSelector ist die einfachste Form: nodeSelector: {disktype: ssd} im Pod-Spec, und nur Nodes mit diesem Label kommen durchs Filtering. Exakte Label-Gleichheit, hart, keine Ausdrucksstärke — für „dieser Pod braucht SSD-Nodes” völlig ausreichend.

nodeAffinity ist die ausdrucksstarke Variante mit zwei Härtegraden, deren sperrige Namen du im Interview parat haben solltest: requiredDuringSchedulingIgnoredDuringExecution ist eine harte Bedingung (ohne passenden Node bleibt der Pod Pending), preferredDuringSchedulingIgnoredDuringExecution eine gewichtete Präferenz (Scheduler versucht es, nimmt sonst einen anderen Node). Dazu Operatoren wie In, NotIn, Exists — damit gehen Dinge wie „bevorzugt Zone eu-central-1a, notfalls 1b, niemals 1c”. Das IgnoredDuringExecution im Namen ist eine ehrliche Ansage: Die Regel gilt nur beim Scheduling. Ändert sich das Node-Label später, bleibt der laufende Pod, wo er ist.

Pods fernhalten: Taints und Tolerations

Affinity beantwortet „wo will der Pod hin” — Taints drehen die Logik um: Ein Taint auf einem Node (kubectl taint nodes gpu-1 gpu=true:NoSchedule) stößt alle Pods ab, die ihn nicht ausdrücklich per Toleration tolerieren. Der Unterschied ist konzeptionell wichtig: Affinity ist eine Eigenschaft des Pods (opt-in des Workloads), Taints sind eine Eigenschaft des Nodes (Schutz des Nodes, den Pods explizit überwinden müssen). Drei Effekte: NoSchedule (hart, keine neuen Pods), PreferNoSchedule (weich), NoExecute (wirft auch laufende Pods ohne Toleration runter).

Der Klassiker, den du herleiten können musst: GPU-Nodes exklusiv halten. Der Taint allein verhindert, dass normale Workloads die teuren Nodes belegen. Aber Achtung — die Toleration allein erlaubt dem GPU-Pod nur, dort zu laufen; sie zwingt ihn nicht dorthin. Für „GPU-Pods laufen genau auf GPU-Nodes” brauchst du beides: Taint + Toleration (hält andere fern) und nodeAffinity (zieht die GPU-Pods hin). Diese Kombination ist eine der beliebtesten Interview-Fangfragen im Scheduling.

Kubernetes nutzt den Mechanismus selbst: Control-Plane-Nodes tragen standardmäßig einen NoSchedule-Taint, und bei Node-Problemen setzt der Controller automatisch NoExecute-Taints wie node.kubernetes.io/not-ready, die Pods nach einer Toleration-Frist (default 300 s) evakuieren — so funktioniert das Selbstheilen bei Node-Ausfall unter der Haube.

Pods zueinander anordnen: podAffinity und Anti-Affinity

Manchmal geht es nicht um Node-Eigenschaften, sondern um Nachbarschaft. podAffinity zieht Pods zu Nodes, auf denen bestimmte andere Pods laufen (Latenz: Cache neben App). Häufiger gebraucht wird podAntiAffinity: „Platziere meine Replicas nicht dort, wo schon eine läuft” — damit nicht alle auf demselben Node gemeinsam sterben. Bezugsrahmen ist die topologyKey: kubernetes.io/hostname verteilt über Nodes, topology.kubernetes.io/zone über Availability Zones.

Zwei ehrliche Warnungen: Harte Anti-Affinity (required...) mit drei Replicas auf einem Drei-Node-Cluster heißt, dass die vierte Replica — und jeder Ersatz während eines Node-Ausfalls — Pending bleibt; meist ist preferred die robustere Wahl. Und für das Standardproblem „gleichmäßig über Zonen verteilen” gibt es inzwischen das passendere Primitive topologySpreadConstraints, das mit maxSkew echte Gleichverteilung beschreibt statt nur „nicht zusammen”.

Wer weicht zuerst: PriorityClasses und Preemption

Wenn der Cluster voll ist, entscheidet die PriorityClass, wessen Pods warten. Jeder Pod hat eine Priorität (default 0); eine PriorityClass ist ein clusterweites Objekt mit einem Wert (z. B. 1000000 für kritische Workloads). Das Entscheidende ist die Preemption: Findet der Scheduler für einen hochprioren Pod keinen Platz, darf er niederpriore Pods verdrängen — sie werden (unter Beachtung ihres PodDisruptionBudgets und der Graceful-Termination) evicted, um Platz zu schaffen.

Das ist mächtig und gefährlich zugleich: Großzügig verteilte hohe Prioritäten verdrängen im Ernstfall Workloads, die du ebenfalls brauchst. Sinnvoll ist eine kleine, dokumentierte Stufenleiter (system-kritisch → produktkritisch → normal → batch) statt individueller Werte pro Team. Für Batch-Jobs gibt es preemptionPolicy: Never — hohe Priorität in der Warteschlange, ohne andere rauszuwerfen.

Praxis

Taints, Tolerations und Anti-Affinity in einem Multi-Node-kind-Cluster:

cat <<EOF | kind create cluster --config=-
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
  - role: control-plane
  - role: worker
  - role: worker
EOF

# Einen Worker tainten und labeln (simulierter GPU-Node)
kubectl taint nodes kind-worker gpu=true:NoSchedule
kubectl label nodes kind-worker gpu=true

# Normales Deployment: landet nur auf kind-worker2
kubectl create deployment normal --image=nginx --replicas=4
kubectl get pods -o wide

# Pod mit Toleration + nodeAffinity: landet auf dem GPU-Node
kubectl apply -f - <<EOF
apiVersion: v1
kind: Pod
metadata:
  name: gpu-job
spec:
  tolerations:
    - key: gpu
      operator: Equal
      value: "true"
      effect: NoSchedule
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
          - matchExpressions:
              - key: gpu
                operator: In
                values: ["true"]
  containers:
    - name: job
      image: busybox:1.36
      command: ["sleep", "3600"]
EOF
kubectl get pod gpu-job -o wide   # läuft auf kind-worker

# Pending provozieren und die Erklärung lesen
kubectl taint nodes kind-worker2 maintenance=true:NoSchedule
kubectl create deployment more --image=nginx --replicas=2
kubectl describe pod -l app=more | grep -A4 Events
# "... node(s) had untolerated taint ..." — die Filtering-Begründung im Klartext

Der letzte Schritt ist der wichtigste: Die Event-Zeile unter describe pod ist exakt das, was du im Incident und im Live-Troubleshooting-Interview als Erstes vorlesen wirst.

Typische Stolperfallen

Toleration mit Affinity verwechselt: Eine Toleration erlaubt, sie erzwingt nicht. GPU-Pods nur mit Toleration auszustatten heißt: Sie dürfen auch auf normale Nodes — für Exklusivität braucht es zusätzlich Affinity (und umgekehrt den Taint).

Harte Anti-Affinity blockiert Failover: required-Anti-Affinity über hostname mit Replicas == Node-Anzahl bedeutet: Beim Node-Ausfall kann die Ersatz-Replica nirgendwo hin und bleibt Pending. preferred oder topologySpreadConstraints sind meist die bessere Wahl.

Scheduling-Regeln gelten nicht rückwirkend: IgnoredDuringExecution — neue Taints mit NoSchedule und geänderte Labels lassen laufende Pods unberührt. Wer bestehende Pods umziehen will, braucht NoExecute, ein Drain oder einen Rollout.

Preemption-Überraschung: Ein neues Deployment mit hoher PriorityClass kann im vollen Cluster kommentarlos fremde Pods evicten. Wer PriorityClasses einführt, sollte zuerst die Stufenleiter definieren und PodDisruptionBudgets prüfen — sonst ist die erste Wirkung ein Incident im Nachbarteam.

Interview-Vorbereitung

Auf „Wie steuerst du, wo Pods laufen?” antwortest du als sortierter Werkzeugkasten: Scheduler-Grundlage (Filtern nach Requests/Taints/Constraints, dann Scoring) → Anziehen (nodeSelector simpel, nodeAffinity mit hart/weich) → Abstoßen (Taints am Node, Tolerations im Pod — Schutzrichtung umgekehrt zur Affinity) → Relativ zueinander (podAntiAffinity bzw. topologySpreadConstraints für HA-Verteilung) → Knappheit (PriorityClass + Preemption). Stark wirkt, wer zu jedem Werkzeug das eine Standardszenario nennt: SSD-Label, GPU-Pool, Replica-Verteilung, Batch-vs-Kritisch.

Typische Follow-ups:

  • „Wie reservierst du Nodes exklusiv für ein Team?” — Taint auf die Nodes + Toleration in den Team-Workloads, plus nodeAffinity/nodeSelector, damit die Team-Pods auch wirklich nur dort landen.
  • „Unterschied Taint vs. nodeAffinity?” — Taint ist node-seitiger Schutz (alle ohne Toleration werden abgewiesen), Affinity ist pod-seitiger Wunsch; Exklusivität braucht beide.
  • „Was passiert bei Preemption genau?” — Scheduler findet keinen Node, wählt Opfer mit niedrigerer Priorität, evicted sie graceful (PDB wird berücksichtigt), platziert dann den hochprioren Pod.
  • „Pod bleibt Pending — wie findest du die Ursache?” — kubectl describe pod: Die Scheduler-Events zählen pro Ausschlussgrund auf, wie viele Nodes am Taint, an Requests oder an Affinity gescheitert sind.

Zusammenfassung

Der Scheduler filtert und bewertet — und du gibst die Leitplanken vor: nodeSelector/nodeAffinity ziehen Pods auf passende Nodes, Taints und Tolerations schützen Nodes vor unpassenden Pods (Exklusivität = beides kombinieren), podAntiAffinity und topologySpreadConstraints verteilen Replicas gegen gemeinsame Ausfälle, PriorityClasses regeln per Preemption, wer bei Knappheit weicht. Und wenn nichts geht: describe pod erklärt jede Pending-Entscheidung im Klartext.

Scheduling bestimmt, wo Workloads laufen — die nächste Lektion klärt, wer im Cluster überhaupt was tun darf: RBAC und ServiceAccounts.