Alle Beiträge

Vom Monolithen zu Services: REST, gRPC, Kafka und Container-Infrastruktur

Microservices-Architektur — REST, gRPC, Kafka, Kubernetes
Microservices-Architektur — REST, gRPC, Kafka, Kubernetes

Die Entscheidung, einen Monolithen aufzuteilen, ist in erster Linie keine technische — sie ist eine organisatorische. Unabhängige Deploybarkeit, unterschiedliche Skalierungsprofile und klare Teamverantwortung sind die eigentlichen Treiber. Ist die Entscheidung gefallen, werden die Fragen architektonischer Natur: Wie kommunizieren Services miteinander, wie deployen sie zuverlässig, und wie bleibt das verteilte System observierbar?

Wann sich die Aufteilung lohnt

Vor der Zerlegung sollte man sich ehrlich fragen, welches Problem man löst. Modularisierung rechtfertigt ihren Aufwand bei konkretem Bedarf nach unabhängigen Deployments — Teams blockieren einander —, bei deutlich unterschiedlichen Skalierungsprofilen, echter Domänentrennung mit klar abgegrenzter Teamverantwortung oder wenn mehrere Teams an derselben Codebasis arbeiten und der Koordinationsaufwand die Liefergeschwindigkeit senkt.

Ohne einen dieser konkreten Treiber erhöht die Aufteilung die operative Komplexität ohne Gewinn. Das häufigste Scheitern ist der verteilte Monolith: Services, die für jede Anfrage synchron aufeinander warten müssen, eine gemeinsame Datenbankstruktur teilen und trotzdem gemeinsam deployed werden — mit der Netzwerklatenz von Microservices, aber ohne die Unabhängigkeit.

Die richtige Reihenfolge: erst Domänengrenzen modellieren, dann Infrastruktur aufteilen. Module innerhalb derselben Codebasis extrahieren, bevor separate Services entstehen — prozessinterne Grenzen sind kostenlos umstrukturierbar, Service-Grenzen sind es nicht. Das Strangler-Fig-Muster — spezifischen Traffic an den neuen Service routen, während der Monolith den Rest bedient — ist das zuverlässigste Migrationsmuster für HTTP-basierte Systeme.

REST — die universelle Ausgangsbasis

REST über HTTP/JSON ist aus gutem Grund der Standard: universell verständlich, in jeder Sprache und Runtime verfügbar, menschenlesbar und ohne Tooling-Aufwand nutzbar. Browser-Clients, externe Partner und interne Teams können eine REST-API ohne besondere Vorbereitung ansprechen.

GET /orders/12345
Authorization: Bearer <token>

200 OK
Content-Type: application/json

{ "id": "12345", "status": "shipped", "total": 149.90, "items": [...] }

Die Nachteile sind bekannt: JSON ist ausführlich — 1 KB JSON-Payload entsprechen oft 200 Byte binär. HTTP/1.1 erfordert ohne Keep-alive eine neue Verbindung pro Anfrage. Es gibt keinen eingebauten Schema-Vertrag, sodass API-Drift zwischen Produzent und Konsument erst zur Laufzeit auffällt. Versionierung erfordert Disziplin (/v1/, Accept-Header oder Feature-Flags).

Einsatz von REST: externe APIs, Browser-zugängliche Endpunkte, Ad-hoc-Integrationen und einfaches synchrones Request/Response, wo Payload-Größe und Aufruffrequenz kein Engpass sind.

gRPC und Protocol Buffers — für internen Hochdurchsatz

gRPC läuft über HTTP/2, nutzt binäre Protocol-Buffer-Serialisierung und generiert typsicheren Client- und Server-Code aus einer .proto-Definition. Die Proto-Datei ist der Vertrag — beide Seiten werden daraus generiert, was eine ganze Klasse von Schnittstellenfehlern ausschließt.

syntax = "proto3";

service OrderService {
  rpc GetOrder    (OrderRequest) returns (Order);
  rpc WatchOrders (OrderFilter)  returns (stream Order);
}

message OrderRequest { string order_id = 1; }

message Order {
  string id          = 1;
  string customer_id = 2;
  float  total       = 3;
  Status status      = 4;

  enum Status {
    PENDING   = 0;
    SHIPPED   = 1;
    DELIVERED = 2;
  }
}

Payload-Größen sind typischerweise 5–10x kleiner als äquivalentes JSON. HTTP/2-Multiplexing ermöglicht mehrere gleichzeitige Anfragen über eine Verbindung. Nativer bidirektionaler Streaming-Support erlaubt Muster wie Live-Telemetrie oder Echtzeit-Statusupdates — Dinge, die mit REST Polling oder WebSockets erfordern würden.

Die Einschränkungen: gRPC wird im Browser nicht nativ unterstützt (ein grpc-web-Proxy ist erforderlich). Binäre Kodierung ist schwerer manuell zu prüfen — curl und Browser-Devtools helfen nicht. Proto-Schema-Evolution erfordert Disziplin: Feldnummern sind permanent, das Entfernen von Feldern ist ohne sorgfältige Deprecation ein Breaking Change.

Einsatz von gRPC: interne Service-zu-Service-Kommunikation, hohe Aufruffrequenz, bidirektionales Streaming und mehrsprachige Teams, die einen einzigen typisierten Vertrag benötigen.

Kafka — für entkoppelte asynchrone Workflows

Statt sich gegenseitig aufzurufen, veröffentlichen Produzenten Ereignisse in benannten Topics, auf die Konsumenten unabhängig reagieren. Der Event-Bus ist die einzige gemeinsame Abhängigkeit. Produzent und Konsument haben zur Laufzeit keinerlei Kenntnis voneinander und können unabhängig skalieren, deployen und ausfallen.

// Order-Service — veröffentlicht ein Ereignis
await producer.send({
  topic: 'order.placed',
  messages: [{
    key: order.id,
    headers: { correlationId: ctx.correlationId },
    value: JSON.stringify({
      orderId:    order.id,
      customerId: order.customerId,
      items:      order.items,
    }),
  }],
});

// Inventory-Service — unabhängige Consumer-Group
await consumer.subscribe({ topic: 'order.placed' });
consumer.run({
  eachMessage: async ({ message }) => {
    const order = JSON.parse(message.value.toString());
    await inventory.reserve(order.items);
  },
});

// Notification-Service — selbes Ereignis, separate Consumer-Group, keine Kopplung
await notifyConsumer.subscribe({ topic: 'order.placed' });

Fan-out ist nativ: einen dritten Konsumenten für order.placed hinzuzufügen erfordert keine Änderungen am Produzenten oder den bestehenden Konsumenten. Ereignisse werden auf Disk gespeichert und können wiederholt werden — nützlich, um den Zustand eines Services nach einer Migration neu aufzubauen oder fehlgeschlagene Nachrichten nach einem Bug-Fix erneut zu verarbeiten.

Die realen Kompromisse: Kafka führt zu eventueller Konsistenz. Für Operationen, die eine sofortige Antwort erfordern, ist es nicht geeignet. End-to-End-Tracing erfordert das Weiterleiten von Correlation-IDs in den Message-Headern. Kafka zuverlässig zu betreiben erfordert operative Aufmerksamkeit: Partitionierung, Retention-Policies, Consumer-Lag-Monitoring.

Einsatz von Kafka: Workflows über mehrere Services, Benachrichtigungen, Audit-Trails, Event-Sourcing, Datenpipelines und überall dort, wo Fan-out oder Replay benötigt wird.

Die richtige Protokollwahl

Die drei Ansätze schließen sich nicht gegenseitig aus. Eine typische Produktionsarchitektur nutzt alle:

Muster Primärer Einsatz
REST Externe API, Browser-Clients, einfaches CRUD, Ad-hoc-Integrationen
gRPC Interne Services, hohe Aufruffrequenz, Streaming, mehrsprachiger Vertrag
Kafka Asynchrone Workflows, Fan-out, Event-Sourcing, entkoppelte Pipelines

REST an der äußeren Grenze. gRPC zwischen internen Services, wo Performance zählt. Kafka für alles Asynchrone oder was über mehrere Konsumenten auffächern muss.

Container — die Runtime standardisieren

Docker verpackt die OS-Schicht, Runtime, Abhängigkeiten und Anwendung in ein unveränderliches Image, das in Entwicklung, CI und Produktion identisch läuft. Multi-Stage-Builds halten das finale Image klein:

FROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
RUN npm run build

FROM node:22-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
EXPOSE 8080
USER node
CMD ["node", "dist/main.js"]

Base-Images an einen konkreten Digest pinnen für Reproduzierbarkeit. Als Nicht-Root-User ausführen. Images zustandslos halten — persistenter Zustand gehört in externe Speicher.

Kubernetes — Services im Großen orchestrieren

Kubernetes plant Container, verwaltet den Gesundheitszustand und führt Rolling-Deployments über einen Cluster durch. Drei Primitive sind im Alltag am wichtigsten:

Liveness- und Readiness-Probes trennen „läuft der Prozess" von „ist er bereit, Traffic zu bedienen":

livenessProbe:
  httpGet: { path: /health, port: 8080 }
  initialDelaySeconds: 10
  periodSeconds: 15
readinessProbe:
  httpGet: { path: /ready, port: 8080 }
  initialDelaySeconds: 5
  periodSeconds: 5
  failureThreshold: 3

Resource Requests und Limits verhindern, dass ein fehlerhafte Service seine Nachbarn aushungert:

resources:
  requests: { memory: "128Mi", cpu: "100m" }
  limits:   { memory: "256Mi", cpu: "500m" }

Horizontal Pod Autoscaler skaliert die Replikat-Anzahl anhand von CPU, Arbeitsspeicher oder Custom-Metriken — Kafka-Consumer-Lag ist ein besonders nützlicher Trigger für ereignisgesteuerte Services:

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
spec:
  scaleTargetRef: { kind: Deployment, name: order-service }
  minReplicas: 2
  maxReplicas: 20
  metrics:
  - type: Resource
    resource:
      name: cpu
      target: { type: Utilization, averageUtilization: 70 }

Observability — das Nicht-Verhandelbare

Verteilung macht Debugging standardmäßig schwierig. Das Minimum:

  • Correlation-IDs — eine UUID an jedem Einstiegspunkt generieren (API-Gateway, Kafka-Nachricht, geplante Jobs) und als Header durch alle nachgelagerten Aufrufe und Logzeilen weiterleiten
  • Strukturiertes Logging — JSON-Logs mit konsistenten Feldern (service, correlationId, traceId, duration, statusCode) lassen sich in jeder Log-Plattform sauber aggregieren
  • Distributed Tracing — OpenTelemetry-Spans, die durch REST-Header, gRPC-Metadaten und Kafka-Message-Header propagiert werden, ermöglichen die vollständige Rekonstruktion des Pfads einer Anfrage über alle Services

Ohne Correlation-IDs und strukturiertes Logging vor dem ersten Produktionsvorfall ist das Debugging über zehn Services hinweg im Wesentlichen Raten.

Das ehrliche Fazit

Microservices verteilen operative Komplexität. Man gewinnt unabhängige Deploybarkeit, isolierte Fehlerbereiche und Service-spezifische Skalierung — und zahlt dafür mit Netzwerkaufrufen, die fehlschlagen können, verteiltem Zustand, der schwer konsistent zu halten ist, und einem Observability-Stack, der funktionieren muss, bevor alles andere nützlich ist. Mit einem modularen Monolithen beginnen. Services extrahieren, wenn der organisatorische Druck dazu konkret ist, nicht aspirativ. Die Architektur, die am zuverlässigsten ausliefert, ist meist die richtige.

Teilen

Gedanken dazu? Einfach direkt schreiben.

Artikel diskutieren
Logo

IT-Beratung, Softwareentwicklung und digitale Transformation für Unternehmen in Regensburg, Oberpfalz, Bayern und DACH-weit.

Kontakt

Möchten Sie ein Projekt, einen Workflow oder eine Modernisierungsinitiative besprechen?

verixon kontaktieren

Alle Rechte vorbehalten. © 2026 verixon