Der Scheduler interessiert sich nicht dafür, wie viel CPU ein Pod tatsächlich nutzt. Er interessiert sich für das, was im Manifest steht. Das vergesse ich regelmäßig, auch nach Jahren mit Clustern. Dann hängt ein Pending-Pod eine Stunde lang fest, ein Node läuft heiß oder etwas wird im schlechtesten Moment OOMKilled, und ich erinnere mich: Requests sind Versprechen, Limits sind Leitplanken, und Ehrlichkeit steht in keinem Helm-Chart-Kommentar.

Foto von Lukas auf Pexels
Ich tue nicht so, als würde ich Workloads perfekt dimensionieren. Ich habe 256Mi aus einem Blogpost kopiert, Limits „zur Sicherheit“ auf dieselben Werte wie Requests gesetzt und beides weggelassen, weil es in Dev „funktionierte“. Jede dieser Entscheidungen kam irgendwann zurück und hat mir etwas beigebracht. Das hier hätte ich gern früher verstanden, ungefähr so, wie ich es einem müden Teamkollegen erklären würde, der nur will, dass das Deployment grün wird.
Woran der Scheduler wirklich glaubt
Wenn man resources.requests.cpu und resources.requests.memory setzt, sagt man Kubelet und Scheduler: Reserviert so viel auf einem Node, bevor dieser Pod dort landet. Der Scheduler summiert Requests über alle Pods auf einem Node und vergleicht sie mit der verfügbaren Kapazität. Er schaut nicht in die Grafana-Kurve der letzten Woche, außer man ergänzt etwas wie VPA oder einen eigenen Scheduler.
Limits funktionieren anders. CPU-Limits drosseln den Container über cgroup-Quotas. Man kann an die Decke stoßen und langsamer werden, ohne zu sterben. Speicherlimits sind härter: Wenn man sie überschreitet, kann der Linux-OOM-Killer den Prozess beenden. Kubernetes zeigt das als OOMKilled. Es gibt keine sanfte Verhandlung.
Diese Asymmetrie ist wichtig. Hohe Requests, niedrige Limits machen Scheduling schmerzhaft und erlauben trotzdem Bursts, bis der Speicher platzt. Niedrige Requests, hohe Limits lassen den Cluster geräumig wirken, bis alle Pods auf dem Node um echtes RAM konkurrieren und der Kernel Opfer auswählt. Keine Requests bedeuten BestEffort-QoS — okay auf einem lokalen kind-Cluster, riskant für Produktionsnachbarn, die davon ausgehen, dass der Scheduler sie schützt.
Quality-of-Service-Klassen — Guaranteed, Burstable, BestEffort — sind nicht akademisch. Unter Speicherdruck auf einem Node werden BestEffort- und Burstable-Pods ohne passende Limits eher evicted oder OOMKilled. QoS ist für mich kein Abzeichen. Es beschreibt, wer geopfert wird, wenn die Rechnung nicht mehr aufgeht.
Die Lücke zwischen Nutzung und Requests
Der häufigste Fehlermodus, den ich sehe, ist nicht böswillig. Er ist optimistisch.
Ein Java-Service liegt im Leerlauf bei 400Mi und springt beim Batch-Import auf 1,2Gi. Jemand setzt requests.memory: 512Mi, weil „er meistens so viel braucht“. Scheduling wirkt effizient. Um zwei Uhr nachts läuft der Import, der Pod wächst, der Node füllt sich, und entweder stirbt der Pod oder sein Nachbar. Das Dashboard war grün, bis es das nicht mehr war.
Das Gegenteil verschwendet Geld: Requests am Spitzenwert, Limits doppelt so hoch „nur für den Fall“, und der Cluster Autoscaler löst nie aus, weil die angeforderte Kapazität riesig wirkt, während die genutzte Kapazität halb leer ist. Die Finanzabteilung fragt, warum zwölf Nodes nötig sind. Der Bereitschaftsdienst fragt, warum HPA nicht skaliert. Beide haben aus ihrer Perspektive recht.
Ehrliche Dimensionierung beginnt mit Messung über Zeit, nicht mit einem einzelnen Moment:
Dauerhafte Nutzung betrachten, nicht nur Spitzen. Prometheus, metrics-server, Cloud-Monitoring — eines wählen und CPU/Speicher pro Pod über Tage und Wochen grafisch ansehen. Ich will p50 und p95, nicht den einmaligen Peak aus einem schlechten Deploy.
Leerlauf von Lastpfaden trennen. CronJobs, Queue-Consumer und Admin-Endpoints haben andere Profile als gleichmäßiger API-Traffic. Ein Deployment mit einem Request-Wert für alle Container ist manchmal okay. Oft ist es das nicht.
Sidecars einrechnen. Istio, Log-Shipper und Vault-Agenten brauchen ebenfalls Speicher. Ich habe „mysteriöse“ OOMs untersucht, die am Ende nur das Limit des Hauptcontainers plus ein 200Mi-Sidecar waren, das niemand in die Tabelle geschrieben hatte.
Nach Releases neu prüfen. Speicherlecks crashen nicht immer lokal. Ein langsamer Anstieg über drei Releases kann aus einem bequemen 512Mi-Request eine Lüge machen.
Ich runde trotzdem etwas auf. Die Luftfahrt hat mir beigebracht: Wer jede Kraftstoffzahl nach unten rundet, kommt irgendwann überrascht an. Ein moderater Puffer zwischen p95-Nutzung und Request ist keine Verschwendung; er ist Scheduling-Ehrlichkeit mit Reserve.
Limits ohne Aberglauben
Limits sind kein moralisches Gut. Sie sind ein Werkzeug.
Für CPU setze ich Limits, wenn ich laute Nachbarn schützen muss: Multi-Tenant-Nodes, Batch neben latenzsensiblen APIs. Ich akzeptiere, dass Throttling Latenz erhöhen kann, ohne dass metrics-server laut schreit. Für latenzkritische Services auf dedizierten Nodes lasse ich CPU-Limits manchmal weg und verlasse mich auf Requests plus Node-Placement. Das ist eine Abwägung, keine Regel.
Für Speicher setze ich Limits, wenn der Kernel diesen Container beenden soll statt zufällige Prozesse auf dem Node. Das Limit sollte über dem beobachteten Peak liegen und Reserve haben, sonst entscheidet man sich für Crash Loops. Limit gleich Request zu setzen, „damit Kubernetes weiß, was wir brauchen“, missversteht den Mechanismus. Gleiche Requests und Limits mit engen Zahlen ergeben Guaranteed QoS, was bei der Eviction-Reihenfolge hilft, aber kein RAM auf dem Node erschafft.
Ich habe Teams gesehen, die limits.memory exakt auf den Request setzen und sich wundern, warum JVM- oder Node-Heaps nicht atmen können. Runtime-Overhead, Page-Cache-Verhalten und kurze Allokationsspitzen brauchen Spielraum. OOMKilled-Events sollte man lesen. Sie sagen die Wahrheit unverblümt:
Last State: Terminated
Reason: OOMKilled
Exit Code: 137
137 ist nicht subtil. Wenn ich es sehe, prüfe ich echte Nutzung gegen Limit, bevor ich Anwendungscode anfasse. In der Hälfte der Fälle war das Limit falsch. In der anderen Hälfte war die Anwendung falsch. Ohne Daten zu raten, welche Hälfte es ist, verschwendet einen Freitag.
Scheduling-Schmerz ist oft ein Requests-Problem
Pending-Pods mit Insufficient cpu oder Insufficient memory bedeuten: Der Scheduler lehnt das Versprechen ab, weil der Node es nicht unterbringt. Das ist kein Bug. Das ist der Cluster, der sagt: Die Manifeste passen nicht zur Physik.
Typische Ursachen, in die ich noch hineintappe:
Requests zu hoch für die Node-Größe. Ein Pod mit 8Gi Request auf Nodes mit 7Gi allocatable bleibt ewig Pending. Auf dem Papier offensichtlich. Leicht zu übersehen, wenn Node-Pools nach einer Migration gemischte Größen haben.
Aggregierte Requests über Namespaces. Quotas, LimitRanges und Taints blockieren Scheduling mit Meldungen, die nach Kapazität aussehen, obwohl es um Policy geht.
Fragmentierung. Im Cluster ist insgesamt genug CPU frei, aber kein einzelner Node hat einen zusammenhängenden 4Gi-Platz, weil viele mittlere Pods das Puzzle gefüllt haben. Konsolidierung, Größenanpassung oder ein zweiter Node-Pool lösen manchmal, was „Nodes hinzufügen“ allein nicht löst.
Vergessene DaemonSets. Sie verbrauchen ebenfalls allocatable. Ich habe Kapazität für Anwendungspods geplant und den Monitoring-Stack auf jedem Node ignoriert.
Wenn jemand mich bittet, es „einfach zu schedulen“, übersetze ich: Request senken, Node hinzufügen oder aufhören so zu tun, als bräuchte der Pod wirklich so viel. Es gibt keine vierte Option, die dauerhaft hält.
OOM, Eviction und der Mensch am anderen Ende
OOMKilled und evicted Pods fühlen sich wie Anwendungsfehler an. Oft sind es Kapazitätsprobleme mit Anwendungsmaske.
Speicherdruck auf einem Node löst Kubelet-Eviction nach QoS und Nutzung über Request aus. Man sieht es in Events:
The node was low on resource: memory. Container X was using Y, request is Z, limit is W.
Diese Zeile genau zu lesen spart Zeit. Using sagt, was passiert ist. Request sagt, was der Scheduler dachte. Limit sagt, wo die cgroup-Decke war. Wenn diese drei Zahlen verschiedene Geschichten erzählen, ist das Größenkonzept falsch, nicht Kubernetes.
Im Bereitschaftsdienst triagiere ich in dieser Reihenfolge:
- Ist es isoliert auf einen Pod oder viele auf demselben Node?
- Hat sich etwas geändert — Deploy, Config, Traffic, Cron?
- Was zeigen Metriken für den Speichertrend — Sprung oder langsamer Anstieg?
- Passen Limits und Requests zur Realität aus der letzten Größenprüfung?
Wenn viele Pods auf einem Node zusammen scheitern, schaue ich mir den Node an, bevor ich Anwendungscode zurückrolle. Wenn ein Pod allein scheitert, schaue ich auf Limits und Release. Diese Trennung ist langweilig und funktioniert.
Wie ich richtig dimensioniere, ohne Urteil zu automatisieren
Tools helfen. Die Verantwortung für das Ergebnis bleibt trotzdem bei der Entscheidung.
Vertical Pod Autoscaler im Recommendation-Modus hat mich vor Tabellen-Selbstbetrug gerettet. Ich wende VPA-Änderungen in Produktion nicht blind an. Empfehlungen sind statistisch. Black Friday ist kein durchschnittlicher Dienstag.
Goldilocks und ähnliche Dashboards, die Request, Limit und Nutzung nebeneinander zeigen, sind gut für vierteljährliche Hygiene. Ich blocke dafür einen wiederkehrenden Kalendereintrag, weil ich es nicht spontan mache.
LimitRanges in Namespaces fangen „Resources vergessen“ vor Produktion ab. Default Requests und Limits sind grobe Instrumente: Sie stoppen die schlimmsten Fälle, ersetzen aber kein Nachdenken pro Workload.
Lasttests zählen immer noch. Metriken aus normalem Traffic verpassen den ersten Moment, in dem ein neues Export-Feature ein halbes Gigabyte in den Speicher lädt. Ich sage nicht, Gatling löst Dimensionierung. Ich sage: Die Spitze sollte man sehen, bevor Kunden sie sehen.
Wenn ich Resources für einen neuen Service schreibe, ist meine grobe Checkliste:
- Requests nahe anhaltender p95-Nutzung mit moderater Reserve.
- Speicherlimit über dem beobachteten Peak; limit = request vermeiden, außer Guaranteed ist beabsichtigt und das Heap-Verhalten verstanden.
- CPU-Limit nach Nachbarrisiko entscheiden, nicht nach Gewohnheit.
- Im PR dokumentieren, warum diese Zahlen existieren — nicht nur im Kopf.
- Nach 30 Tagen in Produktion erneut prüfen.
Der letzte Schritt ist der, den ich am häufigsten überspringe. Ich versuche, besser zu werden.
Ehrliche Kapazität ist Teamsport
Plattformteams verantworten Nodes und Defaults. Anwendungsteams kennen die Nutzungsmuster. Wenn diese Gruppen nicht miteinander reden, werden Requests zur Folklore.
Ich habe Tabellen für „Standardgrößen“ gesehen — small: 256Mi/512Mi, medium: 512Mi/1Gi — die Onboarding erleichtert und Produktion lauter gemacht haben. Vorlagen sind als Startpunkt okay, wenn man abweichen darf, sobald Metriken widersprechen.
Die Finanzabteilung sieht angeforderte Kapazität auf der Rechnung. Engineering sieht genutzte Kapazität im Dashboard. Bis jemand diese Sichtweisen zusammenführt, streitet man in Meetings über Node-Anzahlen, obwohl ein gemeinsames Grafana-Board gereicht hätte.
Die bescheidene Version: Ich habe Manifeste ausgeliefert, die gelogen haben. Weil ich beschäftigt war, weil das Tutorial 500m CPU sagte, weil das Kopieren des YAML vom Vorjahr sicher wirkte. Der Cluster glaubte mir, bis die Realität es nicht mehr tat. Requests und Limits sind der Punkt, an dem Anwendungsarchitektur auf Infrastrukturphysik trifft. Sie als Dekoration zu behandeln ist wie ein Flugplan mit Kraftstoffzahlen, die zur Hoffnung passen statt zum Verbrauch.
Ich liege manchmal immer noch falsch. Der Unterschied ist: Ich rechne damit, messe früher und korrigiere die Zahlen, bevor OOMKilled sie für mich korrigiert. Wer heute auf einen Pending-Pod oder Exit Code 137 starrt, sollte mit einer Frage anfangen: Was haben wir dem Scheduler versprochen, und was haben wir wirklich gebraucht? Die Lücke dazwischen ist meistens der ganze Vorfall.