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

Resources, Limits und QoS

CPU/Memory Requests, Limits und Quality of Service.

📝 Meine Notizen

Lernziele

Nach dieser Lektion kannst du den Unterschied zwischen Requests (Scheduling-Garantie) und Limits (hartes Maximum) präzise erklären und weißt, warum CPU-Überschreitung gedrosselt, Memory-Überschreitung aber mit einem OOMKill beantwortet wird. Du kannst die drei QoS-Klassen Guaranteed, Burstable und BestEffort herleiten und ihre Rolle bei Node-Pressure-Eviction erklären, und du kennst LimitRange und ResourceQuota als Namespace-Leitplanken für Multi-Team-Cluster.

Das Problem: Nachbarn auf demselben Node

Kubernetes packt Pods verschiedener Teams auf dieselben Maschinen — das ist der Effizienzgewinn der ganzen Plattform. Aber ohne Regeln frisst der speicherhungrigste Container den Node leer, und der Scheduler hätte keine Grundlage für seine Platzierung: Wie viel „Platz” braucht ein Pod überhaupt? Das Noisy-Neighbor-Problem ist kein Randfall, sondern der Normalzustand jedes Multi-Tenant-Clusters — und Requests, Limits und QoS sind die Antwort darauf.

Requests sind die Reservierung: Der Scheduler (Lektion 3) platziert Pods ausschließlich anhand der Summe der Requests gegen das Allocatable des Nodes — der tatsächliche Verbrauch ist ihm egal. Auf dem Node übersetzt das kubelet die CPU-Requests außerdem in cgroup-Gewichte: Unter Konkurrenz bekommt jeder Container CPU proportional zu seinem Request — die Reservierung wirkt also auch zur Laufzeit, nicht nur beim Scheduling.

Limits sind die Obergrenze: mehr bekommt der Container nicht, egal wie leer der Node gerade ist.

CPU rechnet in Millicores (500m = halber Core), Memory in Binärsuffixen (256Mi, 1Gi).

CPU wird gedrosselt, Memory wird gekillt

Der konzeptionelle Kern der Lektion — und eine Frage, die in fast jedem Kubernetes-Interview auftaucht — ist die Asymmetrie der beiden Ressourcen:

CPU ist compressible: Man kann einem Prozess jederzeit Rechenzeit entziehen, ohne ihn zu zerstören — er wird nur langsamer. Erreicht ein Container sein CPU-Limit, greift Throttling: Der CFS-Scheduler des Kernels teilt die Zeit in Perioden (100 ms) und pausiert den Container für den Rest der Periode, wenn seine Quote aufgebraucht ist. Symptom: Latenz-Spikes ohne einen einzigen Fehler im Log. Genau deshalb gibt es die ernstzunehmende Praxis-Position, auf CPU-Limits ganz zu verzichten und nur Requests zu setzen — die Requests garantieren ja bereits den fairen Anteil unter Last, und ungenutzte Zyklen dürfen Bursts bedienen. Der Gegenpol (Limits überall für Vorhersagbarkeit) ist ebenso vertretbar; entscheidend ist, dass du den Trade-off benennen kannst.

Memory ist incompressible: Belegten Speicher kann man einem Prozess nicht „wegdrosseln” — er ist vergeben, bis der Prozess ihn freigibt. Überschreitet ein Container sein Memory-Limit, bleibt dem Kernel nur eine Reaktion: der OOMKill. Der Container stirbt mit Exit-Code 137, das kubelet startet ihn gemäß Restart Policy neu (Lektion 6) — bei wiederholtem Treffer als CrashLoopBackOff. Ein OOMKill ist also kein Bug von Kubernetes, sondern die designierte Durchsetzung des Limits. Konsequenz: Memory-Limits brauchen realistischen Headroom über dem echten Spitzenverbrauch, und bei Memory gilt — anders als bei CPU — die klare Empfehlung Request = Limit, weil Overcommit bei einer nicht komprimierbaren Ressource auf Node-Ebene gefährlich ist.

QoS-Klassen: die Opferreihenfolge bei Knappheit

Was passiert, wenn ein Node insgesamt unter Memory-Druck gerät — auch ohne dass ein einzelner Container sein Limit reißt? Dann muss das kubelet Pods opfern (Node-Pressure Eviction, schon in Lektion 4 gestreift), und die Reihenfolge bestimmt die QoS-Klasse, die Kubernetes aus deinen Requests und Limits ableitet — man setzt sie nicht direkt:

  • Guaranteed: Jeder Container hat Requests = Limits für CPU und Memory. Maximale Vorhersagbarkeit, letzte Wahl bei Eviction.
  • Burstable: Mindestens ein Request gesetzt, aber nicht durchgängig Request = Limit. Der pragmatische Mittelweg für die meisten Workloads.
  • BestEffort: Keinerlei Requests oder Limits. Erste Wahl bei Eviction — und der Scheduler kann solche Pods auf beliebig volle Nodes packen, weil sie „nichts brauchen”.

Die Interview-taugliche Kurzform: Guaranteed für latenzkritische Kernservices, Burstable als Default, BestEffort höchstens für komplett verzichtbare Hintergrundjobs — in Produktion praktisch nie absichtlich.

Leitplanken pro Namespace: LimitRange und ResourceQuota

In einem Cluster mit zwanzig Teams kannst du dich nicht darauf verlassen, dass jedes Manifest saubere Requests trägt. Zwei Admission-Mechanismen (durchgesetzt in der Pipeline aus Lektion 2) geben dem Plattform-Team Kontrolle:

LimitRange wirkt pro Objekt im Namespace: Es setzt Default-Requests und -Limits für Container, die keine angeben (aus BestEffort wird automatisch Burstable), und kann Min/Max-Grenzen pro Container erzwingen — niemand fordert versehentlich 64 Gi an.

ResourceQuota wirkt auf die Summe im Namespace: maximal X CPU-Requests, Y Memory-Limits, Z PVCs für das ganze Team. Damit wird Cluster-Kapazität budgetierbar — die Grundlage für Fairness und für Showback/Chargeback. Wichtige Kopplung: Ist eine Quota für eine Ressource aktiv, müssen alle Pods im Namespace entsprechende Requests/Limits deklarieren — sonst lehnt der API Server sie ab. Deshalb gehören LimitRange (liefert Defaults) und ResourceQuota (deckelt die Summe) zusammen ausgerollt.

Ehrlicher Hinweis: Die richtigen Zahlen findest du nur über Messung — kubectl top, Prometheus-Historie, oder der Vertical Pod Autoscaler im Recommendation-Modus.

Praxis: Throttling, OOMKill und QoS live

kind create cluster --name qoslab
# metrics-server für kubectl top (in kind mit unsicherem TLS-Flag)
kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml
kubectl patch deployment metrics-server -n kube-system --type=json \
  -p='[{"op":"add","path":"/spec/template/spec/containers/0/args/-","value":"--kubelet-insecure-tls"}]'

# 1. Einen OOMKill provozieren: 50Mi Limit, Prozess will mehr
kubectl apply -f - <<EOF
apiVersion: v1
kind: Pod
metadata:
  name: oomdemo
spec:
  containers:
    - name: hog
      image: polinux/stress
      command: ["stress", "--vm", "1", "--vm-bytes", "100M", "--vm-hang", "0"]
      resources:
        requests: { memory: "50Mi" }
        limits:   { memory: "50Mi" }
EOF
sleep 20
kubectl get pod oomdemo            # OOMKilled / CrashLoopBackOff
kubectl describe pod oomdemo | grep -E "OOMKilled|Exit Code"   # Exit Code 137

# 2. QoS-Klassen ableiten lassen
kubectl run guaranteed --image=nginx \
  --overrides='{"spec":{"containers":[{"name":"guaranteed","image":"nginx","resources":{"requests":{"cpu":"100m","memory":"64Mi"},"limits":{"cpu":"100m","memory":"64Mi"}}}]}}'
kubectl run besteffort --image=nginx
kubectl get pod guaranteed besteffort -o custom-columns=NAME:.metadata.name,QOS:.status.qosClass

# 3. LimitRange: Defaults greifen automatisch
kubectl create namespace team-a
kubectl apply -n team-a -f - <<EOF
apiVersion: v1
kind: LimitRange
metadata:
  name: defaults
spec:
  limits:
    - type: Container
      defaultRequest: { cpu: "100m", memory: "128Mi" }
      default:        { cpu: "500m", memory: "256Mi" }
EOF
kubectl run plain --image=nginx -n team-a
kubectl get pod plain -n team-a -o jsonpath='{.spec.containers[0].resources}'; echo

Der plain-Pod hat Requests und Limits bekommen, ohne dass sein Manifest welche enthielt — Admission Control bei der Arbeit. Aufräumen: kind delete cluster --name qoslab.

Typische Stolperfallen

Exit-Code 137 nicht als OOMKill erkennen: CrashLoopBackOff plus Exit 137 plus Reason: OOMKilled in kubectl describe ist die komplette Diagnose — die Lösung ist Limit erhöhen oder Memory-Leak fixen, nicht „Pod neu starten” (das tut das kubelet ja längst).

Requests nach Bauchgefühl überdimensionieren: Der Scheduler rechnet mit Requests, nicht mit Verbrauch (Lektion 3). Wer überall „sicherheitshalber” 2 CPU anfordert, füllt Nodes rechnerisch, die real leerlaufen — Pods hängen Pending, während kubectl top nodes 30 % Auslastung zeigt. Slack ist das teuerste Feature eines Clusters.

CPU-Throttling als Mysterium: P99-Latenz steigt, Fehlerrate bleibt null, Logs sind sauber — klassisches Limit-Throttling. Die Container-Metriken zum gedrosselten Anteil der CFS-Perioden (in Prometheus-Setups als container_cpu_cfs_throttled_periods_total) machen es sichtbar.

Quota ohne LimitRange ausrollen: Sobald die ResourceQuota aktiv ist, werden Pods ohne Requests/Limits abgelehnt — und Teams sehen kryptische forbidden-Fehler bei Deployments, die gestern liefen. Immer zuerst LimitRange mit Defaults, dann Quota.

Interview-Vorbereitung

Die Standardfrage „Erkläre Requests vs. Limits” beantwortest du in drei Sätzen plus Asymmetrie: Requests sind die Scheduling- und Fairness-Grundlage (Platzierung nach Requests, CPU-Anteile proportional), Limits das harte Maximum zur Laufzeit. Dann der Kern: CPU ist compressible → Throttling mit Latenzfolgen; Memory ist incompressible → OOMKill mit Exit 137. Abrunden mit QoS (abgeleitet, nicht gesetzt: Guaranteed/Burstable/BestEffort als Eviction-Reihenfolge) und der Plattform-Sicht (LimitRange für Defaults, ResourceQuota für Team-Budgets).

Typische Follow-ups:

  • „Pod wird ständig neu gestartet, Exit-Code 137 — was ist los?” — OOMKill am Memory-Limit; Verbrauch messen, Limit anheben oder Leak beheben.
  • „Sollte man CPU-Limits setzen?” — Umstritten: ohne Limits keine Throttling-Latenz und bessere Auslastung (Requests sichern Fairness); mit Limits mehr Vorhersagbarkeit. Position + Begründung zählen.
  • „Wer entscheidet, welcher Pod bei Memory-Druck zuerst fliegt?” — Das kubelet bei Node-Pressure-Eviction: BestEffort zuerst, dann Burstable (nach Verbrauch über Request), Guaranteed zuletzt.
  • „Wie verhinderst du, dass ein Team den Cluster leerfrisst?” — ResourceQuota pro Namespace für die Summen, LimitRange für Defaults und Min/Max pro Container — beides zusammen.

Zusammenfassung

Requests reservieren (Scheduling-Grundlage und CPU-Fairness), Limits begrenzen — und die Ressourcen-Natur bestimmt die Durchsetzung: CPU-Überschuss wird gedrosselt, Memory-Überschuss mit OOMKill (Exit 137) beendet. Aus den gesetzten Werten leitet Kubernetes die QoS-Klassen Guaranteed, Burstable und BestEffort ab, die bei Node-Pressure die Eviction-Reihenfolge bestimmen. LimitRange und ResourceQuota machen daraus Plattform-Governance: Defaults pro Container, Budgets pro Namespace und Team.

Damit ist der Workloads-Block komplett: Du weißt, wie Pods gebaut, verwaltet, konfiguriert und mit Ressourcen versorgt werden. Als Nächstes folgt die Netzwerkseite — beginnend mit Services: wie aus wechselnden Pod-IPs stabile, lastverteilte Endpunkte werden.