
„Es funktioniert… Aber es fühlt sich falsch an“: So betreiben Sie einen Java-Monolithen auf Kubernetes, ohne sich den Kopf zu zerbrechen
Ihr Setup sieht so aus:
- Ubuntu-Host
- Apache2, das SSL übernimmt
kind-Cluster- Moqui + OpenSearch in Pods
- MySQL auf dem Host
- Service-Typ: NodePort
- Apache als Reverse-Proxy zu
172.x.x.x:30083
Es funktioniert, fühlt sich aber zusammengeklebt an, und dieser Instinkt trügt Sie nicht. Räumen wir das im Produktionsstil auf, selbstverwaltet, ohne Cloud-Load-Balancer und ohne Magie.
1. NodePort plus Apache-Reverse-Proxy, ist das schlecht?
Für die lokale Entwicklung ist das in Ordnung, für die Produktion fragil.
Aktuell proxyt Apache zu einer internen kind-IP. Das ist brüchig, weil kind nicht für die Produktion gedacht ist, Control-Plane-IPs keine stabilen Design-Grenzen sind und Sie damit das vorgesehene Ingress-Modell von Kubernetes umgehen.
Sie sagten, Sie wollen keine cloud-verwalteten Load Balancer und betreiben alles auf Ihrer eigenen Maschine. Das ist völlig in Ordnung. Ein sauberes, produktionsartiges Setup auf Bare Metal sieht dann so aus:
Empfohlene Architektur (selbstverwaltet)
Internet
↓
Apache (or Nginx) — TLS termination
↓
Kubernetes Ingress Controller
↓
Service (ClusterIP)
↓
Pods
Statt NodePort installieren Sie einen Ingress-Controller, etwa NGINX (nginx-ingress) oder Traefik. Danach proxyt Apache zum Ingress-Controller statt zu NodePort. Noch besser: Apache ganz entfernen und TLS über den Ingress mit cert-manager handhaben lassen.
Wenn Sie vollständig selbstverwaltet auf einer einzigen Maschine bleiben, ist die einfachste saubere Version: nginx-ingress installieren, hostNetwork oder MetalLB nutzen, falls Sie später auf mehrere Nodes skalieren, und TLS am Ingress terminieren.
An NodePort ist nichts verkehrt, es ist eben Verrohrung auf sehr niedriger Ebene. Ingress gibt es, damit Sie Ports nicht für immer von Hand verdrahten müssen.
2. Autoscaling eines Java-Monolithen und die harte Wahrheit
Sie sehen 400 bis 500 MB RAM pro Pod, und die Skalierung von 1 auf 3 Replicas kostet entsprechend ~1,5 GB Arbeitsspeicher. Genau so funktioniert horizontales Skalieren, denn Kubernetes teilt den JVM-Heap nicht magisch zwischen Replicas. Jede Replica ist eine vollständige JVM. Verantwortlich dafür ist die Java-Architektur, daran ändert Kubernetes nichts.
Optimieren können Sie an dieser Stelle trotzdem.
JVM-Tuning
Wenn Sie diese Werte nicht explizit setzen, kann die JVM zu viel Speicher allozieren:
-XX:MaxRAMPercentage
-Xms
-Xmx
Setzen Sie also Speicherlimits in Kubernetes:
resources:
requests:
memory: "512Mi"
limits:
memory: "512Mi"
Und stimmen Sie die JVM darauf ab, den Container-Speicher zu respektieren. Sonst verbraucht Ihr Pod mehr Speicher, als Sie denken.
HPA-Strategie
HPA funktioniert mit CPU besser als mit Speicher, denn Speicher-Skalierung ist träge, CPU-Skalierung reagiert schneller. Ihre Idee, 90 % CPU-Auslastung als Ziel zu nehmen, ist vernünftig.
Die eigentliche Optimierungsfrage lautet aber: Wie viele gleichzeitige Requests kann ein Moqui-Pod bedienen? Messen Sie das, denn das ist Ihre Skalierungseinheit.
Die größere Frage
Drei Replicas für Verfügbarkeit sind in Ordnung, drei Replicas für Traffic auch. Wenn Sie aber skalieren, weil der Speicher durch den Session-Storage wächst, riecht das nach einem Architekturproblem. Damit sind wir bei Ihrem eigentlichen Schmerzpunkt.
3. Sessions, Logs, Locks und PVC-Engpässe
Sie erwähnten:
logs/txlogs/sessions/- Mehrere Replicas
- RWX-PVC
- LOCK-Probleme
Das ist der eigentliche Blocker. Er kommt aus der Semantik gemeinsam genutzter Dateisysteme, und Kubernetes kann dafür wenig. Mehrere Java-Pods, die auf dasselbe RWX-Volume schreiben, erzeugen zuverlässig Lock-Contention, besonders bei Sessions.
Sauber darüber nachdenken kann man so:
Sessions gehören nicht auf die Disk
Disk-basierte Sessions skalieren nicht horizontal. Im richtigen Produktions-Setup nutzen Sie Redis, entweder extern, in Kubernetes oder als Redis-Cluster.
Der Session-Store wird damit zu einem zentralen In-Memory-Key-Value-Store. Kein File-Locking, keine RWX-Probleme, und niemand wird ausgeloggt, wenn der Pod wechselt. Für die Skalierung von Java-Monolithen ist das der Industriestandard.
Logs gehören nicht auf ein gemeinsames PVC
Logs gehen an stdout, und von dort holen sie Fluent Bit, Loki oder ein ELK-Stack ab. Das Logging-Modell von Kubernetes erwartet Logs auf stdout und nicht in gemeinsam genutzten Dateien. Wenn Sie Transaktions-Tracing brauchen, schicken Sie die Logs an ein zentrales Logging-System statt auf eine geteilte Disk.
Statische Dateien und Uploads
Trennen wir also die Zuständigkeiten: Sessions liegen in Redis, Logs im zentralen Logging, Uploads im Object Storage.
Object Storage ist dafür ideal, weil es replica-sicher ist, ohne Locking arbeitet, keine Datei-Korruption kennt und unabhängig skaliert.
Wenn Sie vollständig self-hosted arbeiten, betreiben Sie MinIO (S3-kompatibel) oder NFS, wobei NFS im Vergleich zu Object Storage Legacy ist. RWX-PVC ist hier die am wenigsten skalierbare Option.
4. Sollte MySQL innerhalb von Kubernetes laufen?
Sie betreiben MySQL derzeit auf dem Host, und das ist in Ordnung.
Datenbanken in Kubernetes zu betreiben ist möglich (StatefulSet), bringt aber zusätzliche Komplexität bei Volume-Management, Backup-Orchestrierung und HA.
Auf einer einzelnen Bare-Metal-Maschine ist es völlig vernünftig, MySQL außerhalb von Kubernetes zu halten. Gehen Sie später zu Multi-Node über, sollten Sie das neu bewerten.
5. Kubernetes über eine UI verwalten
Das geht, etwa mit Lens, dem Kubernetes Dashboard oder Portainer.
Für die Remote-Verwaltung eines Clusters bietet Lens mit Abstand das sauberste Erlebnis. Sie laden die kubeconfig herunter, verbinden sich, und fragile NodePort-Freigaben braucht niemand mehr.
Die echte Produktionsform für Ihren Fall
Sie arbeiten self-hosted auf lokaler Hardware und wollen volle Kontrolle, also sieht eine saubere Architektur so aus:
- Bare-Metal-Server
- Richtige Kubernetes-Distribution (nicht kind) RKE2, Talos, k3s usw.
- nginx-ingress installiert
- TLS am Ingress gehandhabt
- Moqui-Pods (kein gemeinsam genutztes Dateisystem für Sessions)
- Redis für Sessions
- Object Storage (MinIO) für Uploads
- MySQL entweder außerhalb von k8s oder als StatefulSet
- Zentralisiertes Logging
Damit verschwinden NodePort, Abhängigkeiten von Control-Plane-IPs und RWX-Locks.
Die harte, aber ehrliche Einschätzung
Das Problem ist nicht Kubernetes: Monolithen erwarten eine gemeinsame Disk, Kubernetes erwartet zustandslose Pods.
Sobald die Sessions in Redis liegen, die Logs an stdout gehen und die Uploads im Object Storage landen, wird Skalieren langweilig, und langweilig ist gut.
Ihre Architektur funktioniert im Moment, ist aber eng an ein Denkmodell für eine einzelne Maschine gekoppelt. Kubernetes spielt seine Stärken aus, sobald Sie diese Kopplung an das Dateisystem auflösen.