Veröffentlicht am

Kubernetes Skalierung: HPA, VPA und KEDA kombinieren

Teilen:
Authors

Kubernetes Scalability Patterns: Horizontale und vertikale Skalierung fuer Microservices

TL;DR

  • HPA skaliert horizontal (mehr Pods), VPA skaliert vertikal (groessere Pods), Cluster Autoscaler skaliert die Infrastruktur (mehr Nodes)
  • KEDA erweitert HPA um Event-basierte Skalierung und ermoeglicht Scale-to-Zero fuer selten genutzte Workloads
  • HPA und VPA auf derselben Metrik ist das haeufigste Anti-Pattern und fuehrt zu Skalierungsschleifen -- VPA im Off-Modus ist die sichere Alternative
  • Multi-Dimensional Autoscaling kombiniert alle drei Ebenen: HPA fuer Pods, VPA fuer Sizing, Cluster Autoscaler fuer Nodes
  • Eine Scaling Decision Matrix hilft bei der Wahl des richtigen Patterns je nach Workload-Typ

Die drei Dimensionen der Kubernetes Skalierung

Kubernetes bietet Skalierung auf drei Ebenen. Jede Ebene loest ein anderes Problem:

Horizontal (HPA) -- Mehr Pods desselben Typs. Geeignet fuer stateless Services, die Last durch Parallelisierung bewaeltigen.

Vertikal (VPA) -- Groessere Pods mit mehr CPU und Memory. Geeignet fuer stateful Workloads, die nicht einfach parallelisiert werden koennen.

Infrastruktur (Cluster Autoscaler) -- Mehr Nodes im Cluster. Reagiert automatisch, wenn Pods nicht gescheduled werden koennen.

Das Ziel: Alle drei Ebenen so kombinieren, dass der Cluster automatisch auf Last reagiert und gleichzeitig Kosten minimiert.

Horizontal Pod Autoscaler (HPA)

CPU-basierte Skalierung

Der klassische Einstieg. HPA beobachtet die CPU-Auslastung und skaliert Pods entsprechend:

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: web-frontend-hpa
  namespace: production
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: web-frontend
  minReplicas: 3
  maxReplicas: 30
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 65
  behavior:
    scaleUp:
      stabilizationWindowSeconds: 30
      policies:
        - type: Pods
          value: 4
          periodSeconds: 60
        - type: Percent
          value: 50
          periodSeconds: 60
      selectPolicy: Max
    scaleDown:
      stabilizationWindowSeconds: 300
      policies:
        - type: Percent
          value: 20
          periodSeconds: 120
      selectPolicy: Min

Die behavior-Section ist entscheidend. Ohne sie skaliert der HPA zu aggressiv herunter und erzeugt Flapping. Die Konfiguration oben bedeutet:

  • Scale-Up: Maximal 4 Pods oder 50% mehr (der groessere Wert gilt), alle 60 Sekunden
  • Scale-Down: Maximal 20% weniger Pods alle 2 Minuten, mit 5 Minuten Stabilisierung

Multi-Metric HPA

Fuer komplexere Workloads reicht eine einzelne Metrik nicht aus. Der HPA kann mehrere Metriken gleichzeitig ueberwachen und skaliert auf den hoechsten errechneten Wert:

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: api-gateway-hpa
  namespace: production
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: api-gateway
  minReplicas: 3
  maxReplicas: 50
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70
    - type: Resource
      resource:
        name: memory
        target:
          type: Utilization
          averageUtilization: 80
    - type: Pods
      pods:
        metric:
          name: http_requests_per_second
        target:
          type: AverageValue
          averageValue: "1000"

Custom Metrics mit Prometheus Adapter

# Prometheus Adapter ConfigMap fuer Custom Metrics
apiVersion: v1
kind: ConfigMap
metadata:
  name: prometheus-adapter-config
  namespace: monitoring
data:
  config.yaml: |
    rules:
      - seriesQuery: 'http_requests_total{namespace!="",pod!=""}'
        resources:
          overrides:
            namespace:
              resource: namespace
            pod:
              resource: pod
        name:
          matches: "^(.*)_total$"
          as: "${1}_per_second"
        metricsQuery: 'rate(<<.Series>>{<<.LabelMatchers>>}[2m])'
      - seriesQuery: 'rabbitmq_queue_messages{namespace!=""}'
        resources:
          overrides:
            namespace:
              resource: namespace
        name:
          as: "rabbitmq_queue_depth"
        metricsQuery: 'sum(<<.Series>>{<<.LabelMatchers>>}) by (<<.GroupBy>>)'

Vertical Pod Autoscaler (VPA)

VPA im Recommender-Modus (sicher)

Der sicherste Einstieg: VPA im Off-Modus analysiert den tatsaechlichen Verbrauch und gibt Empfehlungen, ohne etwas zu aendern:

apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
  name: order-service-vpa
  namespace: production
spec:
  targetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: order-service
  updatePolicy:
    updateMode: "Off"
  resourcePolicy:
    containerPolicies:
      - containerName: order-service
        minAllowed:
          cpu: 50m
          memory: 128Mi
        maxAllowed:
          cpu: 4
          memory: 8Gi
        controlledResources:
          - cpu
          - memory
# VPA-Empfehlungen abfragen
kubectl get vpa order-service-vpa -n production -o yaml | \
  grep -A 20 "recommendation:"

Die Ausgabe zeigt drei Werte:

  • lowerBound -- Minimum, unter dem der Pod wahrscheinlich Probleme bekommt
  • target -- Optimaler Wert basierend auf historischen Daten
  • upperBound -- Puffer fuer Lastspitzen

VPA im Auto-Modus

Im Auto-Modus passt VPA die Requests automatisch an. Der Pod wird dafuer neu gestartet:

apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
  name: batch-worker-vpa
  namespace: production
spec:
  targetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: batch-worker
  updatePolicy:
    updateMode: "Auto"
    minReplicas: 2
  resourcePolicy:
    containerPolicies:
      - containerName: worker
        minAllowed:
          cpu: 100m
          memory: 256Mi
        maxAllowed:
          cpu: 8
          memory: 16Gi
        controlledResources:
          - cpu
          - memory
        controlledValues: RequestsAndLimits

minReplicas: 2 stellt sicher, dass VPA immer mindestens 2 Pods aktiv laesst, bevor es einen Pod zum Neustart evicted.

Cluster Autoscaler

Grundkonfiguration

# Cluster Autoscaler Helm Values
autoDiscovery:
  clusterName: production-cluster
  tags:
    - k8s.io/cluster-autoscaler/enabled
    - k8s.io/cluster-autoscaler/production-cluster

extraArgs:
  scale-down-delay-after-add: 10m
  scale-down-delay-after-delete: 1m
  scale-down-unneeded-time: 10m
  scale-down-utilization-threshold: "0.5"
  max-graceful-termination-sec: 600
  balance-similar-node-groups: true
  expander: least-waste
  skip-nodes-with-local-storage: false
  max-node-provision-time: 5m

Node-Gruppen-Strategie

# Verschiedene Node-Gruppen fuer verschiedene Workload-Typen
# Node-Gruppe 1: General Purpose (On-Demand)
apiVersion: v1
kind: ConfigMap
metadata:
  name: cluster-autoscaler-priority-expander
  namespace: kube-system
data:
  priorities: |
    50:
      - ".*spot.*"
    30:
      - ".*general-purpose.*"
    10:
      - ".*high-memory.*"

KEDA: Event-Driven Autoscaling

KEDA (Kubernetes Event-Driven Autoscaler) erweitert den HPA um dutzende Event-Quellen und ermoeglicht Scale-to-Zero.

KEDA installieren

helm repo add kedacore https://kedacore.github.io/charts
helm repo update

helm install keda kedacore/keda \
  --namespace keda \
  --create-namespace \
  --set watchNamespace="" \
  --set resources.operator.requests.cpu=100m \
  --set resources.operator.requests.memory=128Mi

Queue-basierte Skalierung

# ScaledObject fuer RabbitMQ Queue
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: order-processor-scaler
  namespace: production
spec:
  scaleTargetRef:
    name: order-processor
  pollingInterval: 15
  cooldownPeriod: 120
  minReplicaCount: 0
  maxReplicaCount: 50
  fallback:
    failureThreshold: 3
    replicas: 2
  triggers:
    - type: rabbitmq
      metadata:
        host: amqp://rabbitmq.messaging:5672
        queueName: orders
        queueLength: "10"
        activationQueueLength: "1"

minReplicaCount: 0 ist der Schluessel: Wenn die Queue leer ist, skaliert KEDA den Workload komplett auf null. Sobald eine Nachricht eintrifft (activationQueueLength: 1), wird der erste Pod gestartet.

Cron-basierte Skalierung

Fuer vorhersehbare Last-Muster:

apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: web-shop-scaler
  namespace: production
spec:
  scaleTargetRef:
    name: web-shop
  minReplicaCount: 2
  maxReplicaCount: 30
  triggers:
    - type: cron
      metadata:
        timezone: Europe/Berlin
        start: "0 8 * * 1-5"
        end: "0 20 * * 1-5"
        desiredReplicas: "10"
    - type: cron
      metadata:
        timezone: Europe/Berlin
        start: "0 20 * * 1-5"
        end: "0 8 * * 2-6"
        desiredReplicas: "3"
    - type: prometheus
      metadata:
        serverAddress: http://prometheus-server.monitoring:9090
        metricName: http_requests_per_second
        query: sum(rate(http_requests_total{deployment="web-shop"}[2m]))
        threshold: "500"

Dieses Setup kombiniert Cron (vorhersehbare Geschaeftszeiten) mit Prometheus-Metriken (unvorhergesehene Lastspitzen).

Multi-Dimensional Autoscaling

Die Kombination aller Skalierungs-Dimensionen ergibt das staerkste Setup. Aber die Kombination muss korrekt sein.

Sichere Kombination: HPA + VPA (Off) + Cluster Autoscaler

# 1. VPA im Off-Modus fuer Sizing-Empfehlungen
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
  name: api-vpa
  namespace: production
spec:
  targetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: api-service
  updatePolicy:
    updateMode: "Off"
  resourcePolicy:
    containerPolicies:
      - containerName: api
        controlledResources:
          - cpu
          - memory
---
# 2. HPA fuer horizontale Skalierung
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: api-hpa
  namespace: production
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: api-service
  minReplicas: 3
  maxReplicas: 20
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70
  behavior:
    scaleDown:
      stabilizationWindowSeconds: 300

Der Cluster Autoscaler laeuft separat und reagiert automatisch, wenn der HPA mehr Pods anfordert als auf bestehende Nodes passen.

VPA und HPA auf getrennten Metriken

Wenn VPA im Auto-Modus laufen soll, muessen VPA und HPA verschiedene Metriken steuern:

# VPA steuert nur Memory
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
  name: ml-inference-vpa
  namespace: ml
spec:
  targetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: ml-inference
  updatePolicy:
    updateMode: "Auto"
  resourcePolicy:
    containerPolicies:
      - containerName: inference
        controlledResources:
          - memory
        minAllowed:
          memory: 512Mi
        maxAllowed:
          memory: 16Gi
---
# HPA skaliert auf Custom Metric (nicht CPU/Memory)
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: ml-inference-hpa
  namespace: ml
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: ml-inference
  minReplicas: 2
  maxReplicas: 10
  metrics:
    - type: Pods
      pods:
        metric:
          name: inference_queue_depth
        target:
          type: AverageValue
          averageValue: "5"

Scaling Anti-Patterns

Anti-Pattern 1: HPA und VPA auf derselben Metrik

# FALSCH -- fuehrt zu Skalierungsschleifen
# VPA erhoeht CPU-Requests -> HPA sieht niedrigere CPU-Auslastung -> skaliert runter
# -> weniger Pods -> hoehere Last -> HPA skaliert hoch -> VPA senkt Requests -> Schleife

# VPA auf CPU im Auto-Modus
# + HPA auf CPU-Auslastung
# = Skalierungsschleife

Loesung: VPA im Off-Modus nutzen oder VPA und HPA auf unterschiedliche Ressourcen beschraenken.

Anti-Pattern 2: Fehlende Resource Requests

# FALSCH -- ohne Requests kann kein Autoscaler korrekt arbeiten
spec:
  containers:
    - name: app
      image: my-app:v1.0
      # Keine resources.requests definiert
# RICHTIG -- Requests sind die Grundlage fuer Skalierung
spec:
  containers:
    - name: app
      image: my-app:v1.0
      resources:
        requests:
          cpu: 200m
          memory: 256Mi
        limits:
          cpu: 1
          memory: 1Gi

Anti-Pattern 3: Zu enge Min/Max-Grenzen

# FALSCH -- HPA hat keinen Spielraum
spec:
  minReplicas: 5
  maxReplicas: 6
# RICHTIG -- genuegend Spielraum fuer Skalierung
spec:
  minReplicas: 3
  maxReplicas: 30

Anti-Pattern 4: Scale-Down ohne PodDisruptionBudget

# PodDisruptionBudget schuetzt vor zu aggressivem Scale-Down
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: api-pdb
  namespace: production
spec:
  minAvailable: "75%"
  selector:
    matchLabels:
      app: api-service

Scaling Decision Matrix

Welches Pattern passt zu welchem Workload?

Workload-TypHPAVPAKEDAEmpfehlung
Stateless Web APIJaOff-ModusOptionalHPA auf CPU + Request-Rate
Stateful DatabaseNeinAutoNeinVPA fuer Memory-Anpassung
Message ConsumerOptionalOff-ModusJaKEDA auf Queue-Tiefe
Batch/CronJobNeinOff-ModusJaKEDA Scale-to-Zero
ML InferenceJaMemory-onlyJaKEDA + HPA auf GPU/Queue
Event Stream ProcessorJaOff-ModusJaKEDA auf Kafka Lag
MonolithNeinAutoNeinVPA, da nicht horizontal skalierbar

Load Testing zur Verifizierung

Skalierung muss getestet werden, bevor sie in Produktion geht. Ein systematischer Ansatz:

k6 Load Test

# k6 als Kubernetes Job ausfuehren
kubectl apply -f - << 'LOADTEST'
apiVersion: batch/v1
kind: Job
metadata:
  name: scaling-loadtest
  namespace: testing
spec:
  template:
    spec:
      containers:
        - name: k6
          image: grafana/k6:latest
          command: ["k6", "run", "/scripts/test.js"]
          volumeMounts:
            - name: scripts
              mountPath: /scripts
      volumes:
        - name: scripts
          configMap:
            name: k6-scaling-test
      restartPolicy: Never
LOADTEST
# k6-Testskript als ConfigMap
apiVersion: v1
kind: ConfigMap
metadata:
  name: k6-scaling-test
  namespace: testing
data:
  test.js: |
    import http from 'k6/http';
    import { check, sleep } from 'k6';

    export const options = {
      stages: [
        { duration: '2m', target: 50 },
        { duration: '5m', target: 200 },
        { duration: '2m', target: 500 },
        { duration: '5m', target: 500 },
        { duration: '5m', target: 50 },
        { duration: '2m', target: 0 },
      ],
      thresholds: {
        http_req_duration: ['p(95)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\<500'],
        http_req_failed: ['rate\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\<0.01'],
      },
    };

    export default function () {
      const res = http.get('http://api-service.production:8080/health');
      check(res, { 'status 200': (r) => r.status === 200 });
      sleep(0.1);
    }

Skalierung waehrend des Tests beobachten

# HPA-Status live beobachten
kubectl get hpa -n production -w

# Pod-Anzahl verfolgen
kubectl get pods -n production -l app=api-service -w

# Node-Anzahl bei Cluster Autoscaler
kubectl get nodes -w

Monitoring von Skalierungs-Entscheidungen

# PrometheusRule fuer Autoscaling-Monitoring
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
  name: autoscaling-monitoring
  namespace: monitoring
spec:
  groups:
    - name: autoscaling-alerts
      rules:
        - alert: HPAAtMaxReplicas
          expr: |
            kube_horizontalpodautoscaler_status_current_replicas
            == kube_horizontalpodautoscaler_spec_max_replicas
          for: 15m
          labels:
            severity: warning
          annotations:
            summary: "HPA {{ $labels.namespace }}/{{ $labels.horizontalpodautoscaler }} am Maximum seit 15 Minuten"

        - alert: HPAScaleDownBlocked
          expr: |
            kube_horizontalpodautoscaler_status_current_replicas
            > kube_horizontalpodautoscaler_spec_min_replicas * 2
            and changes(kube_horizontalpodautoscaler_status_current_replicas[30m]) == 0
          for: 1h
          labels:
            severity: info
          annotations:
            summary: "HPA {{ $labels.namespace }}/{{ $labels.horizontalpodautoscaler }} skaliert seit 1 Stunde nicht herunter"

        - alert: PendingPodsNoScaleUp
          expr: |
            sum(kube_pod_status_phase{phase="Pending"}) by (namespace) > 0
          for: 10m
          labels:
            severity: critical
          annotations:
            summary: "Pods in {{ $labels.namespace }} seit 10 Minuten Pending -- Cluster Autoscaler moeglicherweise blockiert"

Verwandte Artikel

Fazit

Kubernetes skalierung ist kein einzelnes Feature, sondern ein Zusammenspiel aus HPA, VPA, Cluster Autoscaler und KEDA. Das richtige Pattern haengt vom Workload-Typ ab: Stateless Services profitieren von HPA, Datenbanken von VPA, Event-Driven Workloads von KEDA.

Der sicherste Einstieg: VPA im Off-Modus auf alle Deployments, HPA mit konservativem Behavior auf die wichtigsten Services, Cluster Autoscaler mit genug Spielraum nach oben. Testen mit echten Lastprofilen, bevor die Skalierung produktiv geht.

Falls Sie Unterstuetzung bei der Skalierungs-Strategie fuer Ihre Kubernetes-Umgebung brauchen, stehen wir unter /kontakt fuer ein Beratungsgespraech zur Verfuegung.

Kubernetes-Beratung gesucht?

Wir helfen deutschen Unternehmen bei der Kubernetes-Implementierung, Migration und Optimierung. DSGVO-konform und praxiserprobt.

📖 Verwandte Artikel

Weitere interessante Beiträge zu ähnlichen Themen