Kubernetes in Production: Was wir gelernt haben
Vom Tutorial zur Produktion
Vor gut zwei Jahren haben wir unsere ersten Workloads auf Kubernetes deployt. Minikube lief, die ersten YAML-Dateien waren geschrieben, kubectl apply funktionierte. Es fühlte sich an, als hätten wir Kubernetes verstanden. Die Realität belehrte uns schnell eines Besseren.
Der Unterschied zwischen einem funktionierenden Cluster und einem produktionsreifen Cluster ist gewaltig. Diesen Unterschied haben wir in den vergangenen zwei Jahren Schritt für Schritt -- und manchmal schmerzhaft -- kennengelernt. Was folgt, sind die Lektionen, die uns am meisten geprägt haben.
Resource Requests und Limits: Die unterschätzte Grundlage
Unsere erste Lektion kam in Form von OOMKill. Ein Spring-Boot-Service ohne konfigurierte Resource Limits fraß auf einem Node den gesamten Speicher auf und riss dabei drei andere Pods mit in den Abgrund. Der Node wurde unresponsive, Kubernetes schaltete ihn als NotReady -- und plötzlich mussten alle Pods auf den verbliebenen Nodes untergebracht werden. Kaskadeneffekt.
Die Lösung klingt simpel: Resource Requests und Limits für jeden Pod definieren. In der Praxis ist das Finden der richtigen Werte aber alles andere als trivial.
Requests bestimmen, wie viel Ressourcen Kubernetes bei der Scheduling-Entscheidung reserviert. Limits definieren die harte Obergrenze. Setzt man Requests zu hoch an, verschwendet man Cluster-Kapazität. Setzt man sie zu niedrig, landen zu viele Pods auf einem Node und konkurrieren um Ressourcen.
Für Java-Anwendungen hat sich bei uns folgende Faustregel bewährt: Der Memory-Request sollte dem tatsächlichen Durchschnittsverbrauch unter Last entsprechen. Das Memory-Limit setzen wir circa 20 bis 30 Prozent darüber, um kurzzeitige Spitzen abzufangen, aber einen unkontrollierten Speicherverbrauch zu verhindern. Entscheidend: Die JVM-Heap-Einstellung (-Xmx) muss unterhalb des Container-Limits liegen, mit ausreichend Puffer für Metaspace, Thread-Stacks und nativen Speicher.
resources:
requests:
memory: "512Mi"
cpu: "250m"
limits:
memory: "768Mi"
cpu: "1000m"
env:
- name: JAVA_OPTS
value: "-Xms256m -Xmx512m -XX:+UseG1GC"
Ein wichtiger Hinweis zu CPU-Limits: Wir haben gelernt, dass zu enge CPU-Limits bei Java-Anwendungen kontraproduktiv sein können. Die JVM braucht beim Start und bei Garbage Collection kurzzeitig deutlich mehr CPU. Ein zu niedriges Limit führt zu CPU-Throttling, das den Start extrem verlangsamt und Probes fehlschlagen lässt.
Liveness und Readiness Probes: Die Kunst der richtigen Konfiguration
Probes falsch zu konfigurieren war unser zweithäufigstes Problem. Die typische Falle: Liveness Probe zu aggressiv eingestellt, kombiniert mit einer Java-Anwendung, die unter Last langsamer antwortet. Kubernetes interpretiert die Timeout-Überschreitung als "Container ist tot" und startet ihn neu. Der Neustart dauert bei Spring Boot 30 bis 60 Sekunden, in denen noch mehr Last auf die verbliebenen Pods fällt. Ergebnis: eine Kaskade von Neustarts.
Die Lösung: Liveness und Readiness Probes haben unterschiedliche Aufgaben und brauchen unterschiedliche Konfigurationen.
Die Liveness Probe beantwortet die Frage "Ist der Prozess noch grundsätzlich am Leben?" -- sie sollte nur bei echten Deadlocks oder nicht behebbaren Zuständen fehlschlagen. Die Readiness Probe beantwortet "Kann der Pod gerade Traffic verarbeiten?" -- sie darf bei hoher Last oder während einer Datenbankverbindungs-Unterbrechung fehlschlagen.
Für Spring-Boot-Anwendungen mit Actuator hat sich bei uns folgende Konfiguration bewährt:
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
initialDelaySeconds: 60
periodSeconds: 15
timeoutSeconds: 5
failureThreshold: 5
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 3
Die großzügigen Werte bei der Liveness Probe sind Absicht. Ein Container, der 75 Sekunden lang nicht antwortet, hat ein ernstes Problem. Aber ein Container, der einmal 5 Sekunden braucht, weil der Garbage Collector zugeschlagen hat, sollte nicht sofort neu gestartet werden. Der failureThreshold von 5 gibt dem Pod Luft zum Atmen.
Monitoring: Ohne Sichtbarkeit kein Betrieb
In den ersten Monaten haben wir Probleme durch kubectl get pods und kubectl logs diagnostiziert. Das skaliert nicht. Ab dem zehnten Service im Cluster braucht man ein echtes Monitoring.
Prometheus und Grafana haben sich als De-facto-Standard im Kubernetes-Ökosystem etabliert -- zu Recht. Der Prometheus Operator macht die Installation erstaunlich einfach, und die Community stellt fertige Dashboards für nahezu alles bereit.
Welche Metriken man im Blick behalten sollte, hängt natürlich vom Kontext ab. Für uns haben sich aber einige als unverzichtbar herausgestellt:
- Container-Restarts: Ein schleichend steigender Restart-Counter ist ein Frühwarnsignal. Oft steckt ein Memory Leak dahinter, das zum OOMKill führt.
- CPU-Throttling: Zeigt an, dass CPU-Limits zu eng gesetzt sind. Besonders relevant für Java-Anwendungen.
- Request-Latenz (p95/p99): Durchschnittswerte lügen. Die Percentile zeigen, wie es den langsamsten Anfragen ergeht.
- Pod-Scheduling-Wartezeit: Wenn Pods lange im Pending-Status hängen, fehlt Cluster-Kapazität.
- Persistent-Volume-Auslastung: Volle Volumes sind überraschend häufig die Ursache für Ausfälle.
Wir exportieren zusätzlich JVM-Metriken über Micrometer direkt aus unseren Spring-Boot-Anwendungen. Heap-Auslastung, GC-Pausen und Thread-Counts sind Gold wert, wenn man Speicherprobleme diagnostizieren muss.
Namespace-Strategie und RBAC
Am Anfang haben wir alles im Default-Namespace deployt. Das funktioniert bei drei Services. Bei zwanzig nicht mehr.
Wir haben uns für eine Kombination aus umgebungsbasierten und teambasierten Namespaces entschieden: staging, production, dazu teamspezifische Namespaces für Entwicklung und Tests. Resource Quotas pro Namespace verhindern, dass ein Team versehentlich das gesamte Cluster belegt.
RBAC (Role-Based Access Control) gehört von Tag eins an dazu. Die Frage "Wer darf was im Cluster?" sollte nicht erst beantwortet werden, wenn jemand versehentlich Pods in Production löscht. Entwickler bekommen bei uns Read-Zugriff auf den Production-Namespace und vollen Zugriff auf ihre Entwicklungs-Namespaces. Deployments nach Production laufen ausschließlich über die CI/CD-Pipeline.
Rolling Updates und Graceful Shutdown
Kubernetes unterstützt Rolling Updates standardmäßig. Aber ein Rolling Update ohne Graceful Shutdown bedeutet abgebrochene Requests. Bei Java-Anwendungen ist das besonders relevant, weil die JVM ohne explizite Konfiguration laufende Requests nicht sauber abschließt.
Zwei Dinge haben das Problem für uns gelöst. Erstens: ein preStop-Hook, der Kubernetes ein paar Sekunden Zeit gibt, den Pod aus dem Service-Endpunkt zu entfernen, bevor das SIGTERM-Signal kommt. Zweitens: Spring Boots server.shutdown=graceful in Kombination mit einem Timeout, der laufenden Requests Zeit zum Abschließen gibt.
Pod Disruption Budgets (PDBs) sind das ergänzende Gegenstück. Sie garantieren, dass bei geplanten Wartungsarbeiten -- etwa einem Node-Upgrade -- immer eine Mindestanzahl an Pods eines Deployments verfügbar bleibt. Ohne PDB kann kubectl drain im schlimmsten Fall alle Pods eines Services gleichzeitig terminieren.
Fazit
Kubernetes in Produktion zu betreiben ist ein kontinuierlicher Lernprozess. Die Plattform bietet mächtige Werkzeuge, aber sie verzeiht keine Nachlässigkeit bei der Konfiguration. Resource Limits, korrekte Probes, Monitoring und durchdachte Namespace-Strategien sind keine optionalen Extras -- sie sind die Grundlage für einen stabilen Betrieb.
Die wichtigste Erkenntnis nach zwei Jahren: Kubernetes macht vieles einfacher, aber es verlagert die Komplexität, statt sie zu eliminieren. Wer vorher nicht wusste, wie viel Speicher seine Anwendung braucht, wird es mit Kubernetes lernen müssen -- oder die Quittung in Form von OOMKills bekommen.
Unser Rat: Nehmt euch Zeit für die operativen Grundlagen, bevor ihr die nächste Abstraktionsschicht (Service Meshes, Operators, Multi-Cluster) einführt. Ein gut konfiguriertes, gut überwachtes Cluster mit sauberen Probes und Resource Limits ist mehr wert als ein überfrachtetes Setup mit den neuesten Features.
Geschrieben von
Patrick HütterGründer & Software-Architekt
Softwarearchitekt, Ingenieur und Unternehmer. Patrick baut seit über einem Jahrzehnt Produkte und Plattformen — von Enterprise-Backends über Cloud-Native-Infrastruktur bis hin zu KI-gestützten Anwendungen. Als Gründer von encircle360 verbindet er technische Tiefe mit unternehmerischem Denken und treibt Open-Source-Projekte voran, die echten Impact haben.
Das könnte dich auch interessieren
CI-Runner in der microVM: Docker-Builds mit Kata Containers auf Kubernetes
31.08.2026 · 10 Min. Lesezeit
GitOps mit Helmfile und Kyverno: Unser Deployment-Workflow
10.03.2025 · 7 Min. Lesezeit
K3s und KubeVirt: Konvergente Infrastruktur auf Bare Metal
20.01.2025 · 6 Min. Lesezeit