Microservices bauen: Schnitt, Kommunikation und die Muster, die zählen

Microservices bauen: Schnitt, Kommunikation und die Muster, die zählen

Microservices bauen: Schnitt, Kommunikation und die Muster, die zählen

Artikel-Übersicht

Die Entscheidung ist gefallen, jetzt entscheidet der Schnitt, ob das System in zwei Jahren noch änderbar ist. Dieser Artikel beginnt bei den Schnittkriterien — fachlich statt technisch, mit dem Antimuster, das am häufigsten auftritt — und klärt die Frage, wie groß ein Dienst sein sollte. Danach die eigene Datenhaltung, das Merkmal, an dem sich Microservices von verteilten Modulen unterscheiden, mit dem Problem, das daraus folgt: Auswertungen über Dienstgrenzen. Es folgen die zwei Kommunikationswege mit ihrem Fehlerverhalten, verteilte Transaktionen mit Saga, Outbox und Idempotenz, und eine Mustertabelle, in der bei mehreren Einträgen ausdrücklich „selten nötig" steht. Zum Schluss Skalierung, warum Ende-zu-Ende-Tests im verteilten System nicht funktionieren, und was der Betrieb zusätzlich verlangt.

Ob die Zerlegung überhaupt die richtige Entscheidung ist, behandelt der vorangehende Artikel Microservices oder Monolith.

Der Schnitt

Ein Dienst wird entlang einer fachlichen Grenze geschnitten, nicht entlang einer technischen Schicht.

Das klingt selbstverständlich und wird trotzdem regelmäßig anders gemacht. Das häufigste Antimuster: ein Frontend-Dienst, ein Geschäftslogik-Dienst, ein Datenzugriffs-Dienst. Das ist die klassische Schichtung, nur mit Netzwerk zwischen den Schichten — jede fachliche Änderung berührt alle drei Dienste, und man hat sich Latenz eingehandelt, ohne Unabhängigkeit zu gewinnen.

Die Kriterien für einen tragfähigen Schnitt:

  • Eigene Sprache. Wenn ein Begriff in zwei Bereichen Verschiedenes bedeutet, verläuft dort eine Grenze.
  • Eigener Änderungsrhythmus. Was sich gemeinsam ändert, gehört zusammen.
  • Eigene Daten. Wenn zwei Bereiche dieselben Daten besitzen wollen, sind sie einer.
  • Ein Team. Ein Dienst, für den sich zwei Teams zuständig fühlen, hat kein Team.

Die ersten drei Kriterien sind genau die, mit denen Domain-Driven Design Bounded Contexts schneidet — das ist kein Zufall, sondern der Grund, warum die beiden Themen ständig zusammen genannt werden. Wie man dabei vorgeht und woran man merkt, dass ein Schnitt falsch war, steht in Domain-Driven Design in der Praxis.

Wie groß ist ein Microservice?

Keine Zeilenzahl. Drei brauchbare Maße:

  • Er passt in einen Kopf — eine Person kann erklären, was er tut und warum.
  • Er gehört einem Team, und das Team kann ihn allein ausrollen.
  • Ein Neuzugang versteht ihn in wenigen Tagen.

Wenn ein Dienst so klein ist, dass jede zweite Anforderung zwei Dienste berührt, ist er zu klein. Das ist der häufigere Fehler.

Die eigene Datenhaltung

Das Merkmal, das Microservices von verteilten Modulen unterscheidet — und das am häufigsten geopfert wird. Solange zwei Dienste auf dieselbe Tabelle schreiben, kann keiner sein Schema ändern, ohne den anderen zu brechen.

Daraus folgt ein Problem, für das es keine bequeme Lösung gibt: Auswertungen über Dienstgrenzen. Ein JOIN über zwei Datenbanken existiert nicht. Die drei gangbaren Wege:

Aufrufender Dienst. Ein Dienst sammelt die Teile bei den anderen ein und setzt sie zusammen. Einfach, aber langsam und anfällig — die Antwortzeit ist die Summe aller Aufrufe, und ein ausgefallener Dienst kippt die ganze Abfrage.

Repliziertes Lesemodell. Jeder Dienst veröffentlicht Ereignisse über seine Änderungen, ein Auswertungsdienst baut daraus eine für Abfragen optimierte Sicht. Schnell und ausfallrobust, aber nicht sofort aktuell. Für Auswertungen ist das fast immer in Ordnung — es gehört nur abgestimmt.

Datenexport in ein Auswertungssystem. Für Berichte und Kennzahlen der pragmatischste Weg, weil dort ohnehin niemand Aktualität in Sekunden erwartet.

Und ein Punkt, der überrascht: Referenzielle Integrität endet an der Dienstgrenze. Es gibt keinen Fremdschlüssel über zwei Datenbanken. Jeder Dienst muss damit umgehen, dass eine referenzierte Entität anderswo verschwunden ist.

Kommunikation zwischen Diensten

Synchron (HTTP, gRPC) Asynchron (Ereignisse)
Aufrufer wartet ja nein
Kopplung zeitlich gekoppelt — der andere muss erreichbar sein zeitlich entkoppelt
Fehlerverhalten sofort sichtbar, Aufrufer muss reagieren verzögert sichtbar, braucht Überwachung
Reihenfolge garantiert nur je Partition/Kanal
Komplexität liegt im Aufruf in der Fehlerbehandlung
Gut für Abfragen, wenn eine Antwort gebraucht wird Benachrichtigungen, lange Abläufe, mehrere Empfänger

Der Grundsatz: Synchron koppelt, asynchron entkoppelt und verschiebt die Komplexität in die Fehlerbehandlung. Beides ist legitim. Die häufigste Fehlentscheidung ist, alles synchron zu machen, weil es sich einfacher anfühlt — und dann eine Aufrufkette über fünf Dienste zu haben, deren Verfügbarkeit sich multipliziert.

Wie der Entwurf einer synchronen Schnittstelle aussieht, steht in API-Design: Schnittstellen entwerfen, die man nicht bereut. Die Ereignisplattform behandle ich in Event-Driven Architecture mit Apache Kafka.

Transaktionen über Dienstgrenzen

Zwei-Phasen-Commit ist in verteilten Systemen keine praktikable Option — er koppelt genau das, was entkoppelt werden sollte. Stattdessen:

Das Saga-Muster zerlegt eine fachliche Transaktion in mehrere lokale Transaktionen, jede mit einem Kompensationsschritt. Wenn Schritt vier scheitert, laufen die Kompensationen für Schritt drei, zwei und eins.

Zwei Ausprägungen:

  • Choreografie: Jeder Dienst hört auf Ereignisse und reagiert. Keine zentrale Instanz, dafür ist der Gesamtablauf nirgends aufgeschrieben und beim Fehlersuchen schwer nachvollziehbar.
  • Orchestrierung: Ein Ablaufdienst steuert die Schritte. Der Ablauf ist an einer Stelle sichtbar, dafür gibt es wieder eine zentrale Komponente.

Für alles, was mehr als drei Schritte hat, ist Orchestrierung meist die wartbarere Wahl.

Das Outbox-Muster löst ein verwandtes Problem: Man kann nicht in derselben Transaktion die Datenbank ändern und ein Ereignis veröffentlichen. Die Lösung ist ein Ausgangs-Postfach — das Ereignis wird in derselben Transaktion in eine Tabelle geschrieben, ein separater Prozess liest sie aus und veröffentlicht. Damit ist die Zusage: Wenn die Änderung gespeichert ist, wird das Ereignis auch veröffentlicht.

Idempotenz ist die Voraussetzung für beides. In verteilten Systemen kommen Nachrichten doppelt an — nicht als Ausnahme, sondern als Normalfall. Jeder Empfänger muss dieselbe Nachricht zweimal verarbeiten können, ohne dass etwas zweimal passiert.

Und der ehrliche Satz zum Schluss dieses Abschnitts: Wer viele verteilte Transaktionen braucht, hat vermutlich falsch geschnitten. Eine Saga über fünf Dienste ist ein Hinweis darauf, dass diese fünf fachlich einer sind.

Die Muster, die man wirklich braucht

Muster Problem Wann nötig Kosten
API-Gateway ein Eingang für viele Dienste, Authentifizierung an einer Stelle ab externem Zugriff eine Komponente mehr
Dienstverzeichnis wo läuft welcher Dienst meist von der Plattform gelöst gering
Retry mit Backoff vorübergehende Netzfehler immer gering, aber Idempotenz nötig
Circuit Breaker verhindert Kaskadenausfälle ab spürbarer Aufrufkette Konfiguration, Fehlersuche
Outbox Datenänderung und Ereignis atomar sobald Ereignisse veröffentlicht werden Tabelle plus Relay-Prozess
Saga fachliche Transaktion über Dienste wenn unvermeidbar — siehe oben hoch
Bulkhead Ressourcen je Aufrufziel trennen selten — erst bei vielen Abhängigkeiten Konfiguration
CQRS Lese- und Schreibmodell trennen selten — bei stark unterschiedlicher Last zwei Modelle, Synchronisation
Sidecar Querschnittsfunktionen auslagern wenn die Plattform es ohnehin vorsieht Betriebskomplexität
Aggregator mehrere Dienste für eine Sicht zusammenfassen bei Übersichten Latenzsumme
Strangler Fig Altsystem schrittweise ablösen bei Migration doppelte Pflege auf Zeit

Die Spalte „Wann nötig" ist der Unterschied zu jedem Musterkatalog. Drei Einträge tragen ausdrücklich selten — und das ist die wichtigste Information für jemanden, der zum ersten Mal schneidet. Ein vollständiger Katalog ohne Gewichtung führt dazu, dass alles eingebaut wird.

Skalierung und Leistung

Was tatsächlich skaliert: zustandslose Dienste. Man startet mehr Instanzen, ein Lastverteiler verteilt.

Was nicht skaliert: die gemeinsame Datenbank, die man nicht haben sollte. Und Aufrufketten — wenn eine Anfrage durch fünf Dienste läuft, addieren sich fünf Latenzen und fünf Ausfallwahrscheinlichkeiten.

Der Rat, der überall gilt und hier besonders: Messen vor Optimieren. In verteilten Systemen liegt der Engpass fast nie dort, wo man ihn vermutet — meist ist es eine einzelne synchrone Abhängigkeit in einer sonst unauffälligen Kette.

Testen im verteilten System

Die Testpyramide verschiebt sich, und zwar deutlich.

Ende-zu-Ende-Tests über viele Dienste skalieren nicht. Sie sind langsam, sie brauchen alle Dienste in passenden Versionen, und sie schlagen aus Gründen fehl, die nichts mit dem Testfall zu tun haben. Ein Testsatz, der eine Stunde läuft und dreimal die Woche grundlos rot ist, wird ignoriert — und dann hat man ihn umsonst.

Vertragstests sind der Ersatz. Der Aufrufer beschreibt, was er von der Schnittstelle erwartet; der Anbieter prüft in seinem eigenen Testlauf, dass er das erfüllt. Beide Seiten testen unabhängig, keiner braucht den anderen laufend. Das ist der Mechanismus, der unabhängiges Ausrollen überhaupt erst sicher macht.

Dazu kommen ganz gewöhnliche Einheitentests je Dienst und wenige, sorgfältig ausgewählte Ende-zu-Ende-Tests für die zwei bis drei wichtigsten Abläufe.

Betrieb: was dazukommt

Kurz, weil jeder Punkt ein eigenes Thema ist:

  • Korrelations-IDs durch alle Aufrufe — ohne sie ist eine Anfrage über fünf Dienste nicht nachvollziehbar.
  • Verteiltes Tracing, damit man sieht, wo die Zeit verloren geht.
  • Zentrale Protokollierung, weil fünfzehn Logdateien niemand liest.
  • Konfiguration und Geheimnisse außerhalb der Artefakte.
  • Gesundheitsprüfungen, damit die Plattform kaputte Instanzen ersetzen kann.

Wie man den Einstieg dazu findet, ohne alles auf einmal zu bauen, steht in Monitoring und Observability: womit man anfängt; die Plattform darunter behandle ich in Kubernetes-Architektur verstehen.

Häufige Fragen

Wie schneidet man Microservices? Entlang fachlicher Grenzen: eigene Sprache, eigener Änderungsrhythmus, eigene Daten, ein Team. Nicht entlang technischer Schichten.

Synchron oder asynchron kommunizieren? Synchron, wenn eine Antwort gebraucht wird und der Aufruf kurz ist. Asynchron, wenn mehrere Empfänger interessiert sind oder der Ablauf länger dauert. Synchron koppelt zeitlich, asynchron verschiebt die Komplexität in die Fehlerbehandlung.

Wie geht man mit Transaktionen über Dienstgrenzen um? Mit Sagas aus lokalen Transaktionen und Kompensationsschritten, dem Outbox-Muster für die atomare Veröffentlichung von Ereignissen und durchgängiger Idempotenz. Zwei-Phasen-Commit ist keine praktikable Option.

Welche Muster braucht man wirklich? Retry mit Backoff und Idempotenz immer. API-Gateway ab externem Zugriff, Outbox sobald Ereignisse im Spiel sind, Circuit Breaker ab spürbaren Aufrufketten. Bulkhead, CQRS und Sidecar sind seltener nötig, als Kataloge nahelegen.

Wie testet man ein verteiltes System? Mit Vertragstests statt umfangreicher Ende-zu-Ende-Tests. Letztere sind langsam, brüchig und werden nach kurzer Zeit ignoriert.

Wie viele Dienste sind zu viele? Wenn regelmäßig jede zweite Anforderung mehrere Dienste berührt, ist zu fein geschnitten. Die richtige Granularität ist die der Organisation.

Zusammenfassung und praktische Tipps

Der Schnitt entscheidet über alles Weitere, und die Muster sind Werkzeug, nicht Programm. Die wichtigsten Punkte:

  • Fachlich schneiden, nicht technisch. Der Schnitt nach Schichten ist das häufigste Antimuster.
  • Eigene Datenhaltung ist nicht verhandelbar. Ohne sie hat man einen verteilten Monolithen.
  • Auswertungen über Dienstgrenzen sind ein eigenes Problem — meist mit einem replizierten Lesemodell zu lösen.
  • Idempotenz ist Pflicht, nicht gute Praxis. Nachrichten kommen doppelt an.
  • Viele Sagas sind ein Hinweis auf einen falschen Schnitt, keine Auszeichnung.
  • Vertragstests statt Ende-zu-Ende-Tests. Nur so bleibt unabhängiges Ausrollen sicher.
  • Drei Muster aus der Tabelle brauchst du selten — bau sie nicht vorsorglich ein.

Mein Rat für den Einstieg: Schneide zuerst zwei Dienste, nicht zehn, und bau von Anfang an Korrelations-IDs und Idempotenz ein. Die beiden kosten am Anfang je einen halben Tag und sind später kaum noch nachzurüsten — alles andere aus der Mustertabelle kannst du hinzufügen, wenn du den Schmerz spürst, für den es gemacht ist.

Ressourcen