Zum Inhalt springen

Leitfaden zu Ressourcen-Requests und -Limits in Kubernetes

Wie man CPU- und Speicher-Requests und -Limits richtig setzt und dabei OOMKills, CPU-Drosselung und verschwendete Cluster-Kapazität vermeidet.

5 Min. Lesezeit
Ressourcenzuteilung eines Kubernetes-Pods, die die Grenzen zwischen Request und Limit für CPU und Speicher zeigt

Ressourcen-Requests und -Limits sind die am häufigsten falsch konfigurierten Einstellungen in Kubernetes. Werden sie falsch gesetzt, enden Pods in der Produktion als OOMKilled, werden bis zur Unbrauchbarkeit gedrosselt oder verschwenden teure Cluster-Kapazität, weil sie Ressourcen reservieren, die sie nie nutzen. Die Standardwerte sind nicht sicher: Pods ohne Requests werden als Erste verdrängt, und Pods ohne Limits können einen ganzen Knoten auslasten.

Zu verstehen, wie der Scheduler die Requests nutzt und wie der Kernel die Limits durchsetzt, macht den Unterschied zwischen einem stabilen Cluster und einem, der unter Last abstürzt.

Requests vs. Limits

Requests und Limits erfüllen unterschiedliche Zwecke. Requests teilen dem Scheduler mit, wie viel Kapazität ein Pod benötigt. Limits teilen dem Kernel das Maximum mit, das ein Pod nutzen darf.

ymlyaml
# Pod resource configuration
apiVersion: v1
kind: Pod
metadata:
  name: api-server
spec:
  containers:
    - name: api
      image: api-server:latest
      resources:
        requests:
          cpu: "250m"      # Scheduler reserves 0.25 CPU cores
          memory: "256Mi"  # Scheduler reserves 256 MiB
        limits:
          cpu: "1000m"     # Kernel throttles above 1 CPU core
          memory: "512Mi"  # Kernel OOMKills above 512 MiB
ymlyaml
# ❌ No requests or limits — dangerous
apiVersion: v1
kind: Pod
spec:
  containers:
    - name: api
      image: api-server:latest
      # No resource constraints!
      # - Scheduler doesn't know how much capacity this needs
      # - Pod can consume unlimited CPU and memory
      # - Pod is BestEffort QoS — first to be evicted
      # - One runaway pod can crash an entire node
 
# ✅ Properly configured resources
apiVersion: v1
kind: Pod
spec:
  containers:
    - name: api
      image: api-server:latest
      resources:
        requests:
          cpu: "250m"
          memory: "256Mi"
        limits:
          cpu: "1000m"
          memory: "512Mi"
      # Scheduler places pod on a node with enough capacity
      # Pod is Burstable QoS — better eviction priority
      # Memory is capped — no OOM on the node itself

Wie CPU-Requests und -Limits funktionieren

CPU ist eine komprimierbare Ressource. Überschreitet ein Pod sein CPU-Limit, drosselt der Kernel ihn – der Pod läuft langsamer, wird aber nicht beendet. Liegt ein Pod unter seinem Request, sollte der Scheduler ihn auf einem Knoten mit genug freier CPU platziert haben.

ymlyaml
# CPU is measured in millicores
# 1000m = 1 full CPU core
# 250m = 0.25 cores (25% of one core)
# 100m = 0.1 cores (10% of one core)
 
resources:
  requests:
    cpu: "250m"    # Guaranteed 25% of a core
  limits:
    cpu: "1000m"   # Can burst to 100% of a core when available
tstypescript
// The CPU throttling problem — a real production issue
 
// Application handles 100 requests/second at 250m CPU
// Under load spike: 400 requests/second needs 800m CPU
// CPU limit is 1000m — no problem
 
// But CFS (Completely Fair Scheduler) enforces limits per 100ms period
// At 1000m limit: pod gets 100ms of CPU per 100ms period
// A single request that takes 15ms blocks the entire quota for 15ms
// Other requests wait, causing latency spikes
 
// This is "CPU throttling" — the pod has burst capacity
// but the CFS period enforcement creates micro-pauses
 
// Monitoring commands:
// kubectl top pod api-server
// kubectl describe pod api-server | grep -A5 "Limits"

Wie Speicher-Limits funktionieren

Speicher ist nicht komprimierbar. Überschreitet ein Pod sein Speicher-Limit, beendet der Kernel ihn sofort per OOMKill. Für Speicher gibt es kein Drosseln – entweder man bleibt innerhalb der Limits, oder der Prozess ist tot.

ymlyaml
# Memory is measured in bytes with suffixes
# Mi = mebibytes (1 Mi = 1,048,576 bytes)
# Gi = gibibytes
# M = megabytes (1 M = 1,000,000 bytes)
# Use Mi/Gi to be precise
 
resources:
  requests:
    memory: "256Mi"   # Scheduler reserves this much
  limits:
    memory: "512Mi"   # OOMKill above this threshold
shbash
# Diagnosing OOMKill events
kubectl describe pod api-server
 
# Look for:
# Last State: Terminated
#   Reason: OOMKilled
#   Exit Code: 137
 
# Check memory usage over time
kubectl top pod api-server --containers
 
# Check node memory pressure
kubectl describe node <node-name> | grep -A5 "Conditions"
tstypescript
// Common causes of OOMKill in Node.js applications
 
// 1. Memory leaks — unbounded caches, event listener accumulation
const cache = new Map(); // Grows forever without eviction
 
// 2. Large request payloads loaded entirely into memory
app.post('/upload', (req, res) => {
  const body = req.body; // 500MB JSON payload = OOMKill
});
 
// 3. Heap size exceeds container limit
// Node.js default max heap ≈ 1.7GB (V8 default)
// Container limit set to 512Mi → OOMKill before V8 GC kicks in
 
// Fix: set --max-old-space-size to ~75% of the container memory limit
// Container limit: 512Mi → --max-old-space-size=384
// This gives V8 room to garbage collect before hitting the limit

QoS-Klassen

Kubernetes weist jedem Pod anhand seiner Ressourcenkonfiguration eine Dienstgüteklasse (QoS) zu. Diese bestimmt die Verdrängungspriorität, wenn einem Knoten die Ressourcen ausgehen.

ymlyaml
# BestEffort — no requests or limits (evicted first)
resources: {}
 
# Burstable — requests set, different from limits
resources:
  requests:
    cpu: "250m"
    memory: "256Mi"
  limits:
    cpu: "1000m"
    memory: "512Mi"
 
# Guaranteed — requests equal limits (evicted last)
resources:
  requests:
    cpu: "500m"
    memory: "512Mi"
  limits:
    cpu: "500m"
    memory: "512Mi"
tstypescript
// Choosing the right QoS class
 
const qosGuide = {
  guaranteed: {
    useFor: [
      'Production databases',
      'Core API servers',
      'Anything where eviction means downtime',
    ],
    tradeoff: 'No bursting — you pay for reserved capacity',
  },
  burstable: {
    useFor: [
      'Most web applications',
      'Background workers',
      'Services with variable load',
    ],
    tradeoff: 'Can be evicted under node pressure (after BestEffort)',
  },
  bestEffort: {
    useFor: [
      'Development/staging namespaces',
      'Batch jobs that can be retried',
      'Non-critical cron jobs',
    ],
    tradeoff: 'First to be evicted — no guarantees',
  },
};

Dimensionierungsstrategie

Die richtigen Ressourcenwerte zu setzen erfordert Messung. Beginne mit großzügigen Limits, beobachte die tatsächliche Nutzung und verschärfe sie dann.

shbash
# Step 1: Deploy with generous limits, monitor for 1-2 weeks
resources:
  requests:
    cpu: "100m"
    memory: "128Mi"
  limits:
    cpu: "2000m"
    memory: "1Gi"
 
# Step 2: Check actual usage with metrics
kubectl top pod -l app=api-server --containers
 
# Step 3: Query Prometheus for P99 usage over 7 days
# CPU: rate(container_cpu_usage_seconds_total[5m])
# Memory: container_memory_working_set_bytes
 
# Step 4: Set requests to P95 usage, limits to P99 + 25% headroom
# If P95 CPU is 200m and P99 is 350m:
resources:
  requests:
    cpu: "200m"       # P95 — covers normal operation
    memory: "256Mi"
  limits:
    cpu: "500m"       # P99 + headroom — covers spikes
    memory: "384Mi"
ymlyaml
# ❌ Common sizing mistakes
resources:
  requests:
    cpu: "2000m"      # Massively over-provisioned
    memory: "2Gi"     # Wastes cluster capacity
  limits:
    cpu: "2000m"
    memory: "2Gi"
# Pod uses 100m CPU and 200Mi memory
# You're paying for 20x the CPU you need
 
# ✅ Right-sized based on observed usage
resources:
  requests:
    cpu: "150m"       # Based on P95 observed usage
    memory: "256Mi"   # Based on steady-state usage + 20%
  limits:
    cpu: "500m"       # Room for burst
    memory: "384Mi"   # Room for spikes, triggers GC before OOM

LimitRange und ResourceQuota

Erzwinge sinnvolle Standardwerte auf Namespace-Ebene, damit Teams nicht versehentlich Pods ohne Limits oder mit überzogenen Requests deployen können.

ymlyaml
# LimitRange — default limits for the namespace
apiVersion: v1
kind: LimitRange
metadata:
  name: default-limits
  namespace: production
spec:
  limits:
    - default:
        cpu: "500m"
        memory: "512Mi"
      defaultRequest:
        cpu: "100m"
        memory: "128Mi"
      max:
        cpu: "4000m"
        memory: "4Gi"
      min:
        cpu: "50m"
        memory: "64Mi"
      type: Container
 
---
# ResourceQuota — cap total resource usage per namespace
apiVersion: v1
kind: ResourceQuota
metadata:
  name: team-budget
  namespace: team-alpha
spec:
  hard:
    requests.cpu: "10"       # Total CPU requests across all pods
    requests.memory: "20Gi"
    limits.cpu: "20"
    limits.memory: "40Gi"
    pods: "50"               # Max number of pods

Die wichtigsten Erkenntnisse

  1. Setze immer sowohl Requests als auch Limits — Pods ohne beide laufen als BestEffort-QoS und werden unter Druck als Erste verdrängt
  2. CPU drosselt, Speicher tötet — das Überschreiten von CPU-Limits verlangsamt deinen Pod; das Überschreiten von Speicher-Limits beendet ihn sofort
  3. Setze das max-old-space-size von Node.js auf 75 % des Speicher-Limits — das gibt V8 Spielraum für die Garbage Collection, bevor der Kernel einen OOMKill auslöst
  4. Dimensioniere anhand der beobachteten Nutzung — deploye mit großzügigen Limits, beobachte P95/P99 für ein bis zwei Wochen und passe die Größe dann richtig an
  5. Nutze LimitRange für Namespace-Standardwerte — verhindert, dass Teams Pods ohne Ressourcenbeschränkungen deployen
  6. Guaranteed-QoS für kritische Workloads — setze Requests gleich den Limits für Datenbanken und Kerndienste, die nicht verdrängt werden dürfen
Wilfredo Rujel

Wilfredo Rujel

Full-Stack-Softwareentwickler

Diesen Beitrag teilenX