Ein einzelner Container ohne Memory-Limit kann den Arbeitsspeicher eines kompletten Worker-Nodes belegen und dazu führen, dass das kubelet fremde Pods evictet. Requests und Limits sind die Stellschrauben, mit denen du das verhinderst. Dieses Tutorial zeigt, wie du sie pro Container setzt, welche QoS-Klasse daraus folgt und wie du ganze Namespaces per LimitRange und ResourceQuota absicherst.
Was sind Resource Limits in Kubernetes?
Resource Limits in Kubernetes sind Obergrenzen für CPU und Arbeitsspeicher, die pro Container im Feld resources.limits definiert werden. Das kubelet setzt sie über cgroups durch und beendet Container, die ihr Memory-Limit überschreiten.
Davon zu trennen ist der Request. Der Request ist die Menge, die der Scheduler beim Platzieren des Pods reserviert. Ein Node nimmt nur so lange neue Pods auf, wie die Summe der Requests in seine allocatable-Kapazität passt. Das Limit spielt beim Scheduling keine Rolle, es wirkt erst zur Laufzeit.
| Aspekt | requests |
limits |
|---|---|---|
| Wirkt beim | Scheduling | Betrieb (cgroups) |
| CPU-Überschreitung | erlaubt, wenn frei | Throttling |
| Memory-Überschreitung | erlaubt, Eviction-Risiko | OOMKill |
| Beeinflusst QoS-Klasse | ja | ja |
Voraussetzungen
- Ein laufender Kubernetes-Cluster ab Version 1.28, zum Beispiel als Managed-Kubernetes-Cluster mit kostenloser Control Plane
kubectlmit Schreibrechten auf einen Test-Namespace- Der
metrics-serverim Cluster, damitkubectl topfunktioniert - Grundkenntnisse in YAML-Manifesten
Lege für die Beispiele einen eigenen Namespace an:
$ kubectl create namespace team-a
namespace/team-a createdRequests und Limits im Pod setzen
Requests und Limits werden pro Container unter spec.containers[].resources gesetzt, getrennt nach cpu, memory und optional ephemeral-storage. Speichere das folgende Manifest als /root/k8s/api-deployment.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
namespace: team-a
spec:
replicas: 2
selector:
matchLabels:
app: api
template:
metadata:
labels:
app: api
spec:
containers:
- name: api
image: nginx:1.27-alpine
resources:
requests:
cpu: "250m"
memory: "256Mi"
limits:
cpu: "500m"
memory: "256Mi"Zwei Einheiten musst du sauber auseinanderhalten. CPU wird in Millicores angegeben: 250m entspricht einem Viertel eines Kerns, 1 entspricht einem vollen Kern. Arbeitsspeicher rechnet mit Binärpräfixen, wenn du Mi und Gi verwendest: 1Gi sind 1024 MiB, 1G dagegen 1000 MB. Mische die beiden Schreibweisen nicht innerhalb eines Projekts.
$ kubectl apply -f /root/k8s/api-deployment.yaml
deployment.apps/api createdPassende Infrastruktur bei centron
Vom Container zum Cluster: Managed Kubernetes mit cStack – Control Plane, Autoscaler und Traffic inklusive. Managed Kubernetes ansehen →
Was passiert, wenn ein Container sein Limit überschreitet?
Überschreitet ein Container sein CPU-Limit, drosselt der Kernel die Prozesse über die CFS-Quota; überschreitet er sein Memory-Limit, beendet der OOM-Killer den Container sofort mit Exit-Code 137. CPU ist komprimierbar, Arbeitsspeicher nicht.
graph TD
A["Container fordert Ressourcen an"] --> B{"Limit gesetzt?"}
B -- nein --> C["Verbrauch nur durch Node-Kapazität begrenzt"]
B -- ja --> D{"CPU oder Arbeitsspeicher?"}
D -- CPU --> E["Kernel drosselt auf das Limit, Latenz steigt"]
D -- Arbeitsspeicher --> F{"Über dem Limit?"}
F -- nein --> G["Container läuft unverändert weiter"]
F -- ja --> H["OOMKill mit Exit-Code 137"]
H --> I["kubelet startet den Container gemäß restartPolicy neu"]
Daraus folgt eine praktische Regel: Ein zu niedriges Memory-Limit erzeugt CrashLoops, ein zu niedriges CPU-Limit erzeugt schwer auffindbare Latenzspitzen. Für latenzkritische Dienste ist es üblich, den CPU-Request sorgfältig zu setzen und auf ein CPU-Limit zu verzichten, das Memory-Limit aber immer zu definieren.
Welche QoS-Klassen gibt es?
Kubernetes kennt die drei QoS-Klassen Guaranteed, Burstable und BestEffort und leitet sie automatisch aus den gesetzten Requests und Limits ab. Die Klasse steht in status.qosClass und entscheidet, welche Pods das kubelet bei Speicherdruck auf dem Node zuerst evictet.
| QoS-Klasse | Bedingung | Eviction-Reihenfolge |
|---|---|---|
| Guaranteed | requests == limits für CPU und Memory in allen Containern | zuletzt |
| Burstable | mindestens ein Request gesetzt, aber nicht überall gleich dem Limit | mittig |
| BestEffort | keine Requests und keine Limits | zuerst |
Prüfe die Klasse direkt am Objekt:
$ kubectl get pod -n team-a -l app=api -o jsonpath='{.items[*].status.qosClass}'
Burstable BurstableDas Beispiel-Deployment landet in Burstable, weil CPU-Request und CPU-Limit unterschiedlich sind. Setzt du cpu in Request und Limit ebenfalls auf denselben Wert, wird der Pod Guaranteed. Für Datenbanken, Message-Broker und alles, was einen Neustart schlecht verkraftet, ist Guaranteed die richtige Wahl.
Standardwerte per LimitRange erzwingen
Ein LimitRange setzt Default-Werte sowie harte Ober- und Untergrenzen für jeden Container eines Namespace. Damit landet kein Pod mehr versehentlich in BestEffort, auch wenn ein Team die resources-Sektion vergisst.
apiVersion: v1
kind: LimitRange
metadata:
name: default-container-limits
namespace: team-a
spec:
limits:
- type: Container
default:
cpu: "500m"
memory: "512Mi"
defaultRequest:
cpu: "100m"
memory: "128Mi"
max:
cpu: "2"
memory: "2Gi"
min:
cpu: "50m"
memory: "64Mi"default füllt fehlende Limits, defaultRequest fehlende Requests. max und min lehnen Pods ab, die außerhalb des Korridors liegen. Wichtig: Ein LimitRange wirkt nur auf neu erstellte Pods. Bestehende Pods musst du neu ausrollen, damit die Defaults greifen.
$ kubectl apply -f /root/k8s/limitrange.yaml
limitrange/default-container-limits created
$ kubectl rollout restart deployment/api -n team-a
deployment.apps/api restartedNamespace-Budget mit ResourceQuota begrenzen
Eine ResourceQuota begrenzt die Summe aller Requests und Limits eines Namespace. Sobald eine Quota requests.cpu oder limits.memory enthält, muss jeder neue Pod den entsprechenden Wert deklarieren, sonst weist der API-Server ihn ab.
apiVersion: v1
kind: ResourceQuota
metadata:
name: team-a-quota
namespace: team-a
spec:
hard:
requests.cpu: "8"
requests.memory: 16Gi
limits.cpu: "16"
limits.memory: 32Gi
pods: "50"LimitRange und ResourceQuota gehören zusammen. Die Quota definiert das Budget, der LimitRange sorgt dafür, dass Pods ohne explizite Angaben trotzdem zulässige Werte bekommen und nicht an der Quota scheitern.
Passende Werte ermitteln
Die Ausgangswerte für Requests stammen aus dem realen Verbrauch, nicht aus Schätzungen. Miss über mindestens einen vollständigen Lastzyklus, typischerweise 24 Stunden inklusive Batch-Jobs und Backup-Fenstern:
$ kubectl top pod -n team-a --containers
POD NAME CPU(cores) MEMORY(bytes)
api-7d9c5b8f4-2xkzq api 12m 38Mi
api-7d9c5b8f4-lm4vn api 9m 36MiAls Faustregel setzt du den Request auf das 95. Perzentil des beobachteten Verbrauchs und das Memory-Limit auf denselben oder einen leicht höheren Wert. Wenn du die Nodes selbst dimensionierst, plane pro Node 10 bis 15 Prozent Reserve für kubelet, Container-Runtime und System-Daemons ein. Auf skalierbaren Linux-VMs mit Shared oder Dedicated CPU lässt sich diese Reserve über die Instanzgröße gezielt einstellen, statt sie über zu knappe Requests zu erzwingen.
Der Vertical Pod Autoscaler liefert Empfehlungen, ohne einzugreifen, wenn du updateMode: "Off" setzt. Die Werte liest du dann per kubectl describe vpa <name> aus und übernimmst sie kontrolliert ins Manifest. Ab Kubernetes 1.33 ist zusätzlich In-Place Pod Resize als Beta standardmäßig aktiviert. Damit lassen sich CPU- und Memory-Ressourcen eines laufenden Pods über die resize-Subresource anpassen, ohne ihn neu zu starten.
Verifikation
Prüfe zuerst, wie stark die Nodes durch Requests belegt sind:
$ kubectl describe node <node-name> | grep -A 6 "Allocated resources"
Allocated resources:
(Total limits may be over 100 percent, i.e., overcommitted.)
Resource Requests Limits
-------- -------- ------
cpu 850m (21%) 1500m (37%)
memory 768Mi (9%) 1280Mi (16%)Die Prozentwerte beziehen sich auf allocatable, nicht auf die rohe Node-Kapazität. Requests über 100 Prozent sind nicht möglich, Limits über 100 Prozent sind zulässig und bedeuten Overcommitment.
Kontrolliere danach die Quota-Auslastung:
$ kubectl describe quota team-a-quota -n team-a
Name: team-a-quota
Namespace: team-a
Resource Used Hard
-------- ---- ----
limits.cpu 1 16
limits.memory 512Mi 32Gi
pods 2 50
requests.cpu 500m 8
requests.memory 512Mi 16GiTroubleshooting
Pod startet immer wieder neu, Status OOMKilled. Der Container hat sein Memory-Limit überschritten. Den Beleg liefert der letzte Terminierungsgrund:
$ kubectl get pod <pod-name> -n team-a -o jsonpath='{.status.containerStatuses[0].lastState.terminated.reason}{"\n"}'
OOMKilledErhöhe das Memory-Limit schrittweise oder begrenze den Heap der Anwendung selbst. Bei der JVM ist -XX:MaxRAMPercentage=75 der saubere Weg, damit der Heap unterhalb des Container-Limits bleibt.
Pod bleibt in Pending mit 0/3 nodes are available: Insufficient cpu. Die Summe der Requests passt auf keinen Node. Verkleinere den Request, gib dem Cluster einen weiteren Node oder prüfe mit kubectl describe node, ob Taints Kapazität blockieren.
Deployment wird abgelehnt mit forbidden: failed quota. Der Pod deklariert keine Requests oder Limits, obwohl die Quota sie verlangt. Ergänze die resources-Sektion oder lege den passenden LimitRange an.
Anwendung ist langsam, obwohl die CPU-Auslastung niedrig aussieht. Das deutet auf CPU-Throttling hin. Die Metrik container_cpu_cfs_throttled_periods_total zeigt, wie oft der Kernel den Container gedrosselt hat. Steigt sie kontinuierlich, ist das CPU-Limit zu niedrig.
Fazit
Setze für jeden produktiven Container mindestens einen Memory-Request und ein Memory-Limit. Ergänze pro Namespace einen LimitRange für sinnvolle Defaults und eine ResourceQuota als Budgetgrenze. Kontrolliere die Werte anschließend mit kubectl top gegen den realen Verbrauch und korrigiere sie, statt sie einmalig zu raten.
Testen Sie Ihr Setup auf ccloud³
Registrieren Sie sich in der ccloud³ und erhalten Sie 200 € Startguthaben für Ihr Projekt – z. B. für eine PostgreSQL-VM mit automatischen Backups.