Resource-Requests und Limits wirken wie kleine YAML-Felder. Ihre Wirkung ist es nicht. Sie beeinflussen, auf welchem Node ein Pod landet, ob ein Cluster Autoscaler neue Nodes hinzufügt, welche Workloads unter Druck verdrängt werden und ob ein Container langsamer wird oder beendet wird, wenn er zu viel verbraucht.
Für Einsteiger ist verwirrend, dass derselbe resources-Block in zwei verschiedenen Geschichten mitspielt. Die erste passiert, bevor der Pod läuft: Der Scheduler entscheidet, ob ein Node Platz hat. Die zweite passiert danach: Kubelet und Betriebssystem setzen Grenzen zur Laufzeit durch.
Wer diese Geschichten vermischt, erlebt Kubernetes als willkürlich. Ein Pod bleibt Pending, obwohl das Dashboard freie CPU zeigt. Ein Container wird OOMKilled, obwohl auf dem Node noch Memory frei ist. Ein Service wird langsam, aber kein Pod startet neu. Alle drei Fälle werden nachvollziehbar, sobald man Requests, Limits und Scheduling getrennt betrachtet.
Der resources-Block
Eine typische Ressourcen-Konfiguration für einen Container sieht so aus:
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
spec:
replicas: 2
selector:
matchLabels:
app: api
template:
metadata:
labels:
app: api
spec:
containers:
- name: api
image: nginx:1.27
resources:
requests:
cpu: "250m"
memory: "256Mi"
limits:
cpu: "500m"
memory: "512Mi"
250m CPU bedeutet 250 Millicores, also ein Viertel eines CPU-Kerns. 500m ist ein halber Kern. 1 steht für einen ganzen Kern. CPU ist in Kubernetes komprimierbar: Will ein Container mehr CPU, als er bekommt, läuft er meist langsamer.
256Mi Memory bedeutet 256 Mebibyte. Memory ist nicht auf dieselbe nachsichtige Weise komprimierbar. Braucht ein Prozess Speicher und bekommt ihn nicht, muss etwas nachgeben. Mit einem Container-Memory-Limit endet das oft damit, dass der Prozess beendet wird und Kubernetes OOMKilled meldet.
Das erste wichtige Modell: CPU-Druck führt meist zu Warten und Drosselung. Memory-Druck kann zur Beendigung führen.
Requests: woran der Scheduler glaubt
Ein Request ist die Ressourcenmenge, die Kubernetes fürs Scheduling reserviert. Er ist keine Vorhersagemaschine. Der Scheduler beobachtet die Anwendung nicht eine Woche lang und entscheidet dann klug, wo sie laufen soll. Er liest die Pod-Spezifikation.
Fordert ein Container 250m CPU und 256Mi Memory an, sucht der Scheduler einen Node mit mindestens so viel noch nicht verplanter angeforderter Kapazität — plus allen anderen Bedingungen. Bei einem Pod mit mehreren Containern werden die Requests addiert. Init-Container haben Sonderregeln; für den Einstieg reicht: Kubernetes braucht genug Platz für das, was der Pod deklariert.
Node-Kapazität prüft man mit:
kubectl describe node <node-name>
Weiter unten zeigt Kubernetes die verplanten Ressourcen. Beim Scheduling-Debugging ist das oft hilfreicher als eine Live-CPU-Kurve:
Allocated resources:
Resource Requests Limits
cpu 1750m (43%) 4200m (105%)
memory 6Gi (50%) 10Gi (83%)
Dem Scheduler gehen vor allem Requests. Ein Node kann aktuell wenig CPU nutzen und trotzdem einen neuen Pod ablehnen, weil die angeforderte CPU bereits ausgeschöpft ist. Das ist kein Sturheit von Kubernetes. Es hält die Zusagen ein, die schon in anderen Manifesten stehen.
Darum erzeugen schlechte Requests schlechtes Cluster-Verhalten. Zu niedrige Requests lassen den Cluster leerer wirken, als er ist. Zu hohe Requests lassen ihn zu früh voll aussehen.
Limits: was zur Laufzeit passiert
Ein Limit ist eine Grenze, während der Container läuft.
CPU-Limits setzt Kubernetes über CPU-Quotas durch. Will der Container mehr CPU nutzen, als das Limit erlaubt, wird er gedrosselt. Er stirbt normalerweise nicht — er wartet mehr. Bei Webservices kann das höhere Latenz bedeuten, ohne dass ein auffälliges Restart-Event entsteht.
Memory-Limits sind härter. Überschreitet der Prozess das Memory-Limit, kann der Kernel ihn beenden. Kubernetes zeigt dann einen beendeten Container mit dem Grund OOMKilled.
Nützliche Prüfungen:
kubectl describe pod <pod-name>
kubectl get pod <pod-name> -o jsonpath='{.status.containerStatuses[*].lastState}'
kubectl top pod <pod-name>
kubectl top braucht metrics-server oder eine ähnliche Metrik-Pipeline. Es zeigt die aktuelle Nutzung, nicht die historische Form des Workloads. Eine gute Taschenlampe — allein keine Sizing-Strategie.
Startet ein Pod neu, helfen die vorherigen Logs:
kubectl logs <pod-name> --previous
Steht im letzten Zustand OOMKilled, sollte man nicht sofort annehmen, die Anwendung sei kaputt. Vielleicht leakt sie Memory. Vielleicht war das Limit von vornherein unrealistisch.
Quality-of-Service-Klassen
Kubernetes weist Pods anhand ihrer Ressourceneinstellungen eine QoS-Klasse zu.
Guaranteed bedeutet: Jeder Container hat CPU- und Memory-Requests sowie Limits, und jeder Request ist gleich dem passenden Limit.
Burstable bedeutet: Mindestens ein Request oder Limit ist gesetzt, aber der Pod erfüllt die strengen Guaranteed-Regeln nicht.
BestEffort bedeutet: Es sind keine CPU- oder Memory-Requests und keine Limits gesetzt.
Prüfen lässt sich das so:
kubectl get pod <pod-name> -o jsonpath='{.status.qosClass}{"\n"}'
QoS zählt, wenn ein Node unter Druck gerät. Bei knappem Memory oder Disk muss Kubernetes entscheiden, was verdrängt wird. BestEffort-Pods sind am leichtesten zu evicten. Burstable-Pods liegen in der Mitte. Guaranteed-Pods haben den stärksten Schutz — sind aber nicht unsterblich.
Manchmal hört man „Guaranteed ist am besten“ und setzt jeden Request gleich jedem Limit. Für manche Workloads kann das passen, ist aber nicht gratis. Gleicher CPU-Request und CPU-Limit kann unnötiges Throttling erzeugen. Gleicher Memory-Request und Memory-Limit lässt kurzen Spitzen keinen Spielraum. Die passende Einstellung hängt vom Workload und der Node-Umgebung ab, nicht vom Namen der QoS-Klasse.
Wie Scheduling wirklich funktioniert
Der Scheduler macht mehr als CPU- und Memory-Rechnung. Er filtert Nodes, bewertet die verbleibenden und bindet den Pod an einen davon.
Der Filter-Schritt fragt: Welche Nodes sind überhaupt möglich?
Ressourcen zählen. Ebenso Node Selectors, Node Affinity, Taints und Tolerations, Topology Spread Constraints, Zonenregeln für Volumes und Policies. Ein Pod kann irgendwo genug CPU finden und trotzdem nicht planbar sein, weil er ein Label verlangt, das kein Node hat.
Der Bewertungs-Schritt fragt: Welcher der möglichen Nodes ist am besten?
Kubernetes kann gleichmäßigere Ressourcennutzung bevorzugen, Verteilungsregeln beachten oder andere Scheduler-Plugins einbeziehen. Als Einsteiger muss man nicht jedes Scoring-Detail kennen. Wichtig ist: „Der Node mit der meisten freien CPU“ ist nicht der ganze Algorithmus.
Schlägt Scheduling fehl, ist dieser Befehl der wichtigste:
kubectl describe pod <pod-name>
Dort stehen Events wie:
Warning FailedScheduling default-scheduler 0/3 nodes are available: 2 Insufficient memory, 1 node(s) had untolerated taint.
Diese Meldung ist Gold wert. Sie nennt den Grund des Schedulers. Darauf sollte man sich stützen, bevor man rät.
Ein einfaches Pending-Beispiel
Stell dir einen Cluster mit drei Nodes vor. Jeder Node hat 2 allocatable CPU-Kerne. Bestehende Pods fordern auf jedem Node bereits 1800m CPU an. Die aktuelle CPU-Nutzung ist niedrig, weil die Services gerade wenig tun.
Nun wird ein Pod ausgerollt, der 500m CPU anfordert.
Das Dashboard kann viel freie Live-CPU zeigen. Der Scheduler lehnt den Pod trotzdem ab, weil auf jedem Node nur etwa 200m angeforderte CPU frei sind. Der neue Pod braucht 500m. Ergebnis: Pending.
Die Lösung ist nicht automatisch „Request senken“. Vielleicht ist der Request ehrlich. Vielleicht braucht der Cluster einen weiteren Node. Vielleicht gehört der Workload in einen anderen Node-Pool. Vielleicht haben alte Workloads überhöhte Requests. Das Event nennt nur den unmittelbaren Scheduling-Blocker. Ob Kapazität, Konfiguration oder Sizing falsch ist, bleibt eine menschliche Entscheidung.
Nützliche Befehle:
kubectl get pod <pod-name> -o wide
kubectl describe pod <pod-name>
kubectl describe node <node-name>
kubectl get events --sort-by=.lastTimestamp
Ist ein Cluster Autoscaler installiert, kann ein Pending-Pod mit sinnvollen Requests einen neuen Node auslösen. Ist der Request für jeden verfügbaren Node-Typ unmöglich, hilft der Autoscaler nicht. Ein Pod mit 64Gi Memory passt nicht in eine Node-Gruppe, deren größter Node 32Gi allocatable hat.
Namespaces, Quotas und Defaults
Produktionscluster haben oft Leitplanken. Zwei häufige Objekte sind ResourceQuota und LimitRange.
Eine ResourceQuota kann CPU, Memory oder Objektanzahlen in einem Namespace begrenzen. Ein Team kann im Cluster freie Nodes haben und trotzdem blockiert sein, weil seine Namespace-Quota voll ist.
kubectl get resourcequota
kubectl describe resourcequota <quota-name>
Eine LimitRange kann Standardwerte sowie Mindest- und Höchstwerte für Container-Requests und -Limits setzen.
kubectl get limitrange
kubectl describe limitrange <limit-range-name>
Das erklärt eine weitere Anfängerüberraschung: „Ich habe kein Limit gesetzt, aber mein Pod hat eins.“ Eine LimitRange kann es als Default ergänzt haben. Das kann hilfreich sein, erzeugt aber unsichtbares Verhalten, wenn Teams die Defaults nicht kennen.
Wird ein Pod abgelehnt, bevor er überhaupt Pending wird, prüft man:
kubectl describe replicaset <replicaset-name>
kubectl get events --sort-by=.lastTimestamp
Admission-Fehler erscheinen oft als Events am Controller oder im Namespace.
Praktische Startpunkte
Es gibt keine universell perfekte Zahl — aber vernünftige Gewohnheiten.
Für CPU sollte der Request die normale anhaltende Nutzung plus etwas Reserve widerspiegeln. Bei latenzsensiblen Services sind CPU-Limits mit Vorsicht zu setzen. Ein enges CPU-Limit kann den Service unter Last langsamer machen, ohne einen offensichtlichen Crash.
Für Memory sollte man vom beobachteten Working Set und Spitzenverhalten ausgehen. Platz für Runtime-Overhead, Caches und kurze Spitzen ist wichtig. Ein Memory-Limit knapp über Leerlaufniveau lädt zu OOMKilled ein.
Bei Batch-Jobs sollte der Peak ehrlich beschrieben werden. Ein Job mit zu niedrigem Request läuft vielleicht neben zu vielen Nachbarn und macht den Node ungesund. Ein Job mit zu hohem Request bleibt vielleicht ewig Pending.
Sidecars zählen mit. Service Meshes, Log-Agenten und Secret-Agenten verbrauchen ebenfalls Ressourcen. Der Gesamtrequest eines Pods umfasst alle Container.
Bei Java, Node.js und ähnlichen Laufzeiten muss das Speicherverhalten zur Container-Grenze passen. Glaubt die JVM, mehr Memory nutzen zu dürfen als das Container-Limit erlaubt, korrigiert der Kernel dieses Missverständnis irgendwann.
Was beim Troubleshooting zu prüfen ist
Bei einem Pending-Pod:
kubectl describe pod <pod-name>
kubectl get nodes
kubectl describe node <node-name>
kubectl get resourcequota
kubectl get limitrange
Das Scheduling-Event lesen. Auf Insufficient cpu, Insufficient memory, Taints, Node Affinity oder Volume-Binding-Hinweise achten.
Bei einem OOMKilled-Container:
kubectl describe pod <pod-name>
kubectl logs <pod-name> --previous
kubectl top pod <pod-name>
Memory-Limit mit beobachteter Nutzung und Anwendungsverhalten vergleichen. Wenn möglich, historische Metriken anschauen, nicht nur den aktuellen Wert.
Bei Verdacht auf CPU-Throttling:
kubectl top pod <pod-name>
kubectl describe pod <pod-name>
Kubernetes zeigt Throttling mit einfachen Befehlen nicht immer deutlich. In Produktion sind Prometheus-Metriken wie Container CPU Throttling Seconds besser. Trotzdem ist ein enges CPU-Limit neben Latenzspitzen ein wertvoller Hinweis.
Eine kleine Laborübung
Ein Deployment mit sehr niedrigem Memory-Limit erstellen und einen Prozess laufen lassen, der Speicher allokiert. Beobachten, wie der Pod neu startet. Dann das Limit erhöhen und den Unterschied sehen. Danach einen Pod mit absichtlich riesigem CPU-Request erstellen und in einem kleinen Cluster Pending bleiben lassen. Die Events lesen, bevor das YAML geändert wird.
Ziel ist nicht, Fehlermeldungen auswendig zu lernen. Ziel ist die Gewohnheit, nach der fehlgeschlagenen Phase zu fragen.
Hat Admission den Pod wegen Quota oder Policy abgelehnt?
Ist Scheduling fehlgeschlagen, weil kein Node zu Request und Constraints passt?
Hat die Laufzeitdurchsetzung den Container beendet oder gedrosselt?
Das sind unterschiedliche Probleme mit unterschiedlichen Lösungen. Sobald man sie trennt, sind resources.requests und resources.limits keine geheimnisvollen YAML-Dekorationen mehr. Sie werden zur Sprache, mit der ein Workload mit dem Cluster verhandelt.
Diese Verhandlung sollte ehrlich sein. Nicht perfekt, nicht übermütig — aber ehrlich genug, damit der Scheduler Pods sinnvoll platzieren kann und das Kubelet den Node schützt, wenn die Realität lauter wird.