Veröffentlicht am

Microservices Decomposition: Monolith aufteilen

Teilen:
Authors

TL;DR

Einen Monolith zerlegt man nicht auf einen Schlag, sondern schrittweise mit dem Strangler Fig Pattern. Bounded Contexts aus Domain-Driven Design liefern die Schnittgrenzen. Jeder extrahierte Service bekommt ein eigenes Kubernetes Deployment und eine eigene Datenbank. Starte mit dem Service, der den geringsten Kopplungsgrad hat und den hoechsten Business-Value liefert. Drei bis fuenf Services im ersten Schritt reichen.


Wann ein Monolith zum Problem wird

Nicht jeder Monolith muss zerlegt werden. Aber wenn Deployments laenger als 30 Minuten dauern, Teams sich gegenseitig blockieren und ein Fehler im Zahlungsmodul die gesamte Applikation lahmlegt -- dann ist es Zeit.

Typischer Monolith (E-Commerce):
+--------------------------------------------------+
|  monolith.jar / monolith-app                     |
|                                                  |
|  +------------+  +-----------+  +--------------+ |
|  | Bestellung |  | Produkt-  |  | Benutzer-    | |
|  | (Orders)   |--| katalog   |--| verwaltung   | |
|  +------------+  +-----------+  +--------------+ |
|        |               |              |          |
|  +--------------------------------------------------+
|  |         Gemeinsame Datenbank (PostgreSQL)         |
|  +--------------------------------------------------+
+--------------------------------------------------+

Das Kernproblem: Alle Module teilen sich eine Datenbank, einen Deployment-Zyklus und einen Laufzeitprozess. Eine Aenderung am Produktkatalog erfordert ein Deployment der gesamten Applikation.


Schritt 1: Bounded Contexts identifizieren

Domain-Driven Design liefert das Werkzeug fuer die Zerlegung. Ein Bounded Context ist ein fachlicher Bereich mit eigener Sprache, eigenen Regeln und eigenen Daten. Im E-Commerce-Beispiel:

Bounded ContextKernentitaetenDaten-OwnershipKopplungsgrad
BestellungenOrder, OrderItem, PaymentBestellhistorie, ZahlungsstatusMittel
ProduktkatalogProduct, Category, PriceProduktdaten, Preise, BilderNiedrig
BenutzerverwaltungUser, Role, AddressLogin-Daten, Profile, AdressenNiedrig
VersandShipment, TrackingVersandstatus, AdressenHoch (zu Bestellungen)
LagerverwaltungInventory, WarehouseBestaende, ReservierungenHoch (zu Produkten)

Die Faustregel: Wenn zwei Kontexte unterschiedliche Begriffe fuer dasselbe Konzept verwenden, gehoeren sie getrennt. Der Produktkatalog kennt einen "Artikel", die Bestellung kennt eine "Position". Unterschiedliche Sprache = unterschiedlicher Service.


Schritt 2: Strangler Fig Pattern anwenden

Das Strangler Fig Pattern ersetzt den Monolith schrittweise. Neue Requests werden an den neuen Service geroutet, der Monolith wird Stueck fuer Stueck kleiner. Kein Big-Bang-Rewrite.

Phase 1: Proxy vor den Monolith
                                    +------------------+
                               /-*/}| Produktkatalog   |
+--------+    +-----------+   /     | (neuer Service)  |
| Client |-*/}| API       |--/      +------------------+
+--------+    | Gateway / |
              | Ingress   |--\      +------------------+
              +-----------+   \--*/}| Monolith         |
                                    | (Bestellungen +  |
                                    |  Benutzer)       |
                                    +------------------+

Phase 2: Weiterer Service extrahiert
                                    +------------------+
                               /-*/}| Produktkatalog   |
+--------+    +-----------+   /     +------------------+
| Client |-*/}| API       |--/
+--------+    | Gateway / |----*/}  +------------------+
              | Ingress   |        | Benutzerverwaltung|
              +-----------+--\     +------------------+
                              \
                               \-*/}+------------------+
                                    | Monolith         |
                                    | (nur Bestellung) |
                                    +------------------+

Der Ingress Controller routet basierend auf URL-Pfaden. Das ist der Kern der Strangler Fig: Der Client merkt nichts von der Migration.


Schritt 3: Kubernetes Deployments erstellen

Jeder extrahierte Service bekommt ein eigenes Deployment, einen eigenen Service und eigene Ressourcen-Limits.

Produktkatalog-Service

apiVersion: apps/v1
kind: Deployment
metadata:
  name: product-catalog
  namespace: shop
  labels:
    app: product-catalog
    team: catalog-team
spec:
  replicas: 2
  selector:
    matchLabels:
      app: product-catalog
  template:
    metadata:
      labels:
        app: product-catalog
    spec:
      containers:
        - name: product-catalog
          image: registry.internal/product-catalog:1.0.0
          ports:
            - containerPort: 8080
          resources:
            requests:
              cpu: 200m
              memory: 256Mi
            limits:
              cpu: 500m
              memory: 512Mi
          readinessProbe:
            httpGet:
              path: /health/ready
              port: 8080
            initialDelaySeconds: 5
          livenessProbe:
            httpGet:
              path: /health/live
              port: 8080
            initialDelaySeconds: 15
          env:
            - name: DB_HOST
              valueFrom:
                secretKeyRef:
                  name: catalog-db-credentials
                  key: host
---
apiVersion: v1
kind: Service
metadata:
  name: product-catalog
  namespace: shop
spec:
  selector:
    app: product-catalog
  ports:
    - port: 80
      targetPort: 8080

Benutzer-Service und Bestell-Service (Monolith-Rest)

apiVersion: apps/v1
kind: Deployment
metadata:
  name: user-service
  namespace: shop
spec:
  replicas: 2
  selector:
    matchLabels:
      app: user-service
  template:
    metadata:
      labels:
        app: user-service
    spec:
      containers:
        - name: user-service
          image: registry.internal/user-service:1.0.0
          ports:
            - containerPort: 8081
          resources:
            requests:
              cpu: 150m
              memory: 256Mi
            limits:
              cpu: 400m
              memory: 512Mi
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: order-service
  namespace: shop
spec:
  replicas: 3
  selector:
    matchLabels:
      app: order-service
  template:
    metadata:
      labels:
        app: order-service
    spec:
      containers:
        - name: order-service
          image: registry.internal/order-service:1.0.0
          ports:
            - containerPort: 8082
          resources:
            requests:
              cpu: 300m
              memory: 512Mi
            limits:
              cpu: "1"
              memory: "1Gi"

Ingress fuer Strangler Fig Routing

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: shop-routing
  namespace: shop
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /$2
spec:
  ingressClassName: nginx
  rules:
    - host: shop.example.de
      http:
        paths:
          - path: /api/products(/|$)(.*)
            pathType: ImplementationSpecific
            backend:
              service:
                name: product-catalog
                port:
                  number: 80
          - path: /api/users(/|$)(.*)
            pathType: ImplementationSpecific
            backend:
              service:
                name: user-service
                port:
                  number: 80
          - path: /api/orders(/|$)(.*)
            pathType: ImplementationSpecific
            backend:
              service:
                name: order-service
                port:
                  number: 80

Schritt 4: Datenbank aufteilen

Die Datenbank-Dekomposition ist der schwierigste Teil. Drei Strategien:

Shared Database (Uebergang): Alle Services lesen noch aus derselben Datenbank. Schnell umgesetzt, aber keine echte Entkopplung. Nur als temporaere Loesung.

Database per Service (Ziel): Jeder Service hat seine eigene Datenbank-Instanz. Der Produktkatalog besitzt die products- und categories-Tabellen, der Bestell-Service die orders-Tabellen.

Database View (Kompromiss): Services greifen ueber Views auf Daten anderer Services zu. Besser als Direct Table Access, aber immer noch gekoppelt.

Zielarchitektur: Database per Service
+------------------+    +------------------+    +------------------+
| product-catalog  |    | order-service    |    | user-service     |
+------------------+    +------------------+    +------------------+
        |                       |                       |
+------------------+    +------------------+    +------------------+
| catalog-db       |    | orders-db        |    | users-db         |
| (PostgreSQL)     |    | (PostgreSQL)     |    | (PostgreSQL)     |
| - products       |    | - orders         |    | - users          |
| - categories     |    | - order_items    |    | - roles          |
| - prices         |    | - payments       |    | - addresses      |
+------------------+    +------------------+    +------------------+

Fuer Abfragen, die Daten aus mehreren Services benoetigen (z.B. "Zeige alle Bestellungen mit Produktnamen"), gibt es zwei Ansaetze: API Composition (der aufrufende Service fragt beide APIs) oder Event-basierte Datensynchronisation (CQRS).


Entscheidungskriterien: Welchen Service zuerst extrahieren?

KriteriumGewichtProduktkatalogBestellungenBenutzerverwaltung
Geringe KopplungHoch9/105/108/10
Eigenstaendige DatenHoch9/106/109/10
Team-AutonomieMittel8/105/107/10
Business ValueMittel7/109/104/10
AenderungshaeufigkeitNiedrig8/106/103/10
Gesamt413131

Der Produktkatalog gewinnt: geringe Kopplung, eigene Daten, haeufige Aenderungen. Starte dort.


FAQ

Wie lange dauert eine Monolith-zu-Microservices-Migration?

Rechne mit 3-6 Monaten pro extrahiertem Service, je nach Kopplungsgrad und Teamgroesse. Eine vollstaendige Migration eines mittelgrossen Monoliths dauert typischerweise 12-24 Monate. Der Strangler Fig Ansatz erlaubt aber produktive Nutzung der neuen Services ab Tag 1.

Wie viele Microservices sollte man haben?

Nicht mehr als ein Team pro Service verantworten kann. Die Faustregel "Two-Pizza Team" gilt: Ein Service gehoert einem Team von 3-8 Personen. 50 Services fuer 10 Entwickler sind ein Anti-Pattern.

Was passiert mit Transaktionen ueber Service-Grenzen hinweg?

Verteilte Transaktionen (Two-Phase Commit) funktionieren in Microservices schlecht. Setze auf Sagas: Eine Kette von lokalen Transaktionen mit kompensierenden Aktionen im Fehlerfall. Der Bestell-Service reserviert Bestand, bucht Zahlung und bestaetigt -- bei Fehler wird jeder Schritt rueckgaengig gemacht.

Brauche ich sofort Database per Service?

Nein. Starte mit Shared Database und trenne die Daten schrittweise. Wichtiger ist, dass der Service-Code keine direkten Tabellenzugriffe auf Daten anderer Services macht. Erzwinge den Zugriff ueber APIs, auch wenn die Datenbank noch gemeinsam ist.

Welche Tools helfen bei der Analyse des Monolithen?

Fuer Java: jDepend, ArchUnit, Structure101. Fuer allgemeine Codeanalyse: CodeScene (Change Coupling), SonarQube (Abhängigkeiten). Fuer Laufzeitanalyse: Distributed Tracing mit Jaeger zeigt, welche Module wie oft miteinander kommunizieren.


Legacy zu Kubernetes migrieren?

Wir begleiten Ihre Migration von VMs zu Containern – ohne Produktionsausfall. Erfahrung aus 50+ Migrationsprojekten.

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