Orchestrierung von Microservice-Transaktionen mit dem Saga-Pattern
Wenhao Wang
Dev Intern · Leapcell

Einleitung
In der sich entwickelnden Landschaft der modernen Softwareentwicklung haben sich Microservices als dominanter Architekturstil etabliert, der unübertroffene Vorteile in Bezug auf Skalierbarkeit, Ausfallsicherheit und unabhängige Entwicklung bietet. Diese Modularität bringt jedoch eine erhebliche Herausforderung mit sich: die Verwaltung der transaktionalen Konsistenz über mehrere Dienste hinweg. Im Gegensatz zu monolithischen Anwendungen, bei denen ACID-Eigenschaften von einer einzigen Datenbank inhärent garantiert werden, basieren Microservices häufig auf separaten Datenbanken, was verteilte Transaktionen notorisch komplex macht. Stellen Sie sich einen E-Commerce-Bestellprozess vor: Erstellen einer Bestellung, Aktualisieren des Lagerbestands und Abwickeln der Zahlung. Wenn ein Schritt fehlschlägt, muss der gesamte Workflow korrekt rückgängig gemacht werden, um die Datenintegrität zu wahren. Dieser entscheidende Bedarf an robuster transaktionaler Integrität in einer verteilten Umgebung führt uns zum Saga-Pattern, einem leistungsstarken Ansatz, der entwickelt wurde, um genau dieses Problem zu lösen und sicherzustellen, dass Geschäftsprozesse auch dann konsistent bleiben, wenn sie verschiedene Dienste überspannen.
Das Dilemma verteilter Transaktionen und die Saga-Lösung
Bevor wir uns dem Saga-Pattern selbst widmen, lassen Sie uns einige grundlegende Konzepte klären, die seine Notwendigkeit und Funktionsweise untermauern.
Kernterminologie
- Microservice-Architektur: Ein Architekturstil, der eine Anwendung als Sammlung lose gekoppelter Dienste strukturiert, die jeweils unabhängig entwickelt, bereitgestellt und skaliert werden.
- Verteilte Transaktion: Eine Transaktion, die mehrere unabhängige Systeme oder Dienste umfasst und Operationen über diese hinweg ausführt. Im Gegensatz zu lokalen Transaktionen sind Standard-ACID-Garantien schwer oder unmöglich direkt aufrechtzuerhalten.
- CAP-Theorem: Ein grundlegendes Theorem in der verteilten Informatik, das besagt, dass es für einen verteilten Datenspeicher unmöglich ist, gleichzeitig mehr als zwei der folgenden drei Garantien zu bieten: Konsistenz, Verfügbarkeit und Fehlertoleranz (Partition Tolerance). Microservice-Architekturen priorisieren oft Verfügbarkeit und Fehlertoleranz, was zu eventual consistency führt.
- Eventual Consistency (Eventuelle Konsistenz): Ein Konsistenzmodell, bei dem, wenn keine neuen Updates für ein bestimmtes Datenelement vorgenommen werden, schließlich alle Zugriffe auf dieses Element den letzten Aktualisierungswert zurückgeben. Dies ist ein üblicher Kompromiss in verteilten Systemen.
- Kompensations-Transaktion: Eine Operation, die eine vorherige Operation semantisch rückgängig macht. Sie macht die Operation nicht unbedingt rückgängig, sondern erstellt eine neue Operation, die ihre Auswirkungen kompensiert. Wenn beispielsweise Geld von einem Konto abgebucht wurde, fügt eine Kompensations-Transaktion dieses Geld wieder hinzu.
Das Saga-Pattern erklärt
Das Saga-Pattern ist eine Methode zur Verwaltung verteilter Transaktionen, die mehrere Dienste umspannen, wobei jeder Dienst seine eigene Datenbank pflegt. Anstelle einer einzigen, atomaren Transaktion, die alle Dienste umfasst (was in Microservices problematisch ist), zerlegt eine Saga die Transaktion in eine Sequenz von lokalen Transaktionen. Jede lokale Transaktion aktualisiert die Datenbank ihres eigenen Dienstes und veröffentlicht ein Ereignis, das die nächste lokale Transaktion in der Sequenz auslöst. Wenn eine lokale Transaktion fehlschlägt, führt die Saga eine Reihe von Kompensations-Transaktionen aus, um die Auswirkungen der vorherigen erfolgreichen lokalen Transaktionen rückgängig zu machen.
Es gibt zwei Hauptmöglichkeiten, Sagas zu koordinieren:
-
Choreografie-basierte Saga:
- Jeder Dienst produziert und konsumiert Ereignisse, um zu entscheiden, ob und wann seine lokale Transaktion ausgeführt wird.
- Kein zentraler Koordinator. Dienste hören auf Ereignisse, führen ihre Arbeit aus und emittieren dann neue Ereignisse.
- Vorteile: Lose gekoppelt, einfacher zu implementieren für unkomplizierte Arbeitsabläufe.
- Nachteile: Kann schwierig zu überwachen und langlaufende Sagas zu debuggen sein, insbesondere wenn die Anzahl der Dienste zunimmt. Der Gesamtfluss ist weniger transparent.
-
Orchestrierungs-basierte Saga:
- Ein zentraler Orchestrator (ein dedizierter Dienst) ist für die Koordinierung der Saga verantwortlich. Er teilt jedem teilnehmenden Dienst mit, welche lokale Transaktion ausgeführt werden soll.
- Der Orchestrator pflegt den Zustand der Saga und entscheidet über den nächsten Schritt, einschließlich der Ausführung von Kompensations-Transaktionen.
- Vorteile: Bessere Kontrolle über den gesamten Prozess, einfachere Überwachung des Saga-Fortschritts und einfachere Verwaltung komplexer Arbeitsabläufe.
- Nachteile: Der Orchestrator kann zu einem Single Point of Failure oder einem Engpass werden, wenn er nicht sorgfältig konzipiert ist. Er führt auch einen zusätzlichen Dienst zur Verwaltung ein.
Praktisches Implementierungsbeispiel (Orchestrierungs-basiert)
Wir veranschaulichen eine orchestrationsbasierte Saga mit einem vereinfachten Bestellabwicklungsbeispiel mit Pseudocode im Python-Stil. Wir haben drei Dienste: Order Service
, Inventory Service
und Payment Service
.
Szenario: Kunde platziert eine Bestellung
- Order Service: Erstellt eine neue Bestellung im Status
PENDING
. - Inventory Service: Reserviert angeforderte Artikel.
- Payment Service: Verarbeitet die Zahlung.
- Order Service: Aktualisiert die Bestellung auf
APPROVED
oderREJECTED
.
Saga Orchestrator
# Annahme einer Message Queue wie Kafka oder RabbitMQ für die Kommunikation class OrderCreationOrchestrator: def __init__(self, order_id): self.order_id = order_id self.state = "INITIATED" self.context = {"order_id": order_id, "items": [], "total_amount": 0.0} # Bestellungsdetails speichern, die diensteübergreifend benötigt werden def start_saga(self, order_details): print(f"Orchestrator: Starting Saga for Order {self.order_id}") self.context.update(order_details) self.state = "CREATE_ORDER" self._send_command_to_order_service(self.context) def _send_command_to_order_service(self, payload): # Simulieren Sie das Senden eines Befehls an den Order Service print(f"Orchestrator: Sending 'create_order' command to Order Service with data: {payload}") # In einem echten System würde dies eine Nachricht in eine Warteschlange veröffentlichen self._simulate_order_service_response(payload) def _send_command_to_inventory_service(self, payload): # Simulieren Sie das Senden eines Befehls an den Inventory Service print(f"Orchestrator: Sending 'reserve_inventory' command to Inventory Service with data: {payload}") self._simulate_inventory_service_response(payload) def _send_command_to_payment_service(self, payload): # Simulieren Sie das Senden eines Befehls an den Payment Service print(f"Orchestrator: Sending 'process_payment' command to Payment Service with data: {payload}") self._simulate_payment_service_response(payload) def _simulate_order_service_response(self, payload): # Simulieren Sie, dass der Order Service die Bestellung erstellt und ein Ereignis veröffentlicht print(f"Order Service: Order {payload['order_id']} created in PENDING state.") # Bei Erfolg fährt der Orchestrator fort self.handle_event("order_created", {"order_id": payload["order_id"], "items": payload["items"]}) def _simulate_inventory_service_response(self, payload, success=True): if success: print(f"Inventory Service: Items {payload['items']} reserved for Order {payload['order_id']}.") self.handle_event("inventory_reserved", {"order_id": payload["order_id"]}) else: print(f"Inventory Service: Failed to reserve inventory for Order {payload['order_id']}!") self.handle_event("inventory_reservation_failed", {"order_id": payload["order_id"]}) def _simulate_payment_service_response(self, payload, success=True): if success: print(f"Payment Service: Payment processed for Order {payload['order_id']} with amount {payload['total_amount']}.") self.handle_event("payment_processed", {"order_id": payload["order_id"]}) else: print(f"Payment Service: Failed to process payment for Order {payload['order_id']}!") self.handle_event("payment_failed", {"order_id": payload["order_id"]}) def _send_compensate_order_service(self, payload): print(f"Order Service: Compensating - Canceling Order {payload['order_id']}.") # Im tatsächlichen System den Bestellstatus auf 'CANCELED' ändern pass def _send_compensate_inventory_service(self, payload): print(f"Inventory Service: Compensating - Unreserving items for Order {payload['order_id']}.") # Im tatsächlichen System reservierte Artikel freigeben pass def _send_compensate_payment_service(self, payload): print(f"Payment Service: Compensating - Refunding payment for Order {payload['order_id']}.") # Im tatsächlichen System eine Rückerstattung einleiten pass def handle_event(self, event_type, event_data): print(f"Orchestrator: Received event: {event_type} for Order {self.order_id}. Current state: {self.state}") if event_type == "order_created" and self.state == "CREATE_ORDER": self.state = "RESERVE_INVENTORY" self._send_command_to_inventory_service(self.context) elif event_type == "inventory_reserved" and self.state == "RESERVE_INVENTORY": self.state = "PROCESS_PAYMENT" self._send_command_to_payment_service(self.context) elif event_type == "payment_processed" and self.state == "PROCESS_PAYMENT": self.state = "SAGA_COMPLETED" print(f"Orchestrator: Saga for Order {self.order_id} completed successfully!") # Bestellung im Order Service abschließen (z.B. Status auf 'APPROVED' setzen) print(f"Order Service: Order {self.order_id} status updated to APPROVED.") elif event_type == "inventory_reservation_failed": self.state = "SAGA_FAILED_INVENTORY" print(f"Orchestrator: Inventory reservation failed. Initiating compensation.") self._send_compensate_order_service(self.context) # Erstellung der Bestellung kompensieren print(f"Orchestrator: Saga for Order {self.order_id} failed and compensated.") elif event_type == "payment_failed": self.state = "SAGA_FAILED_PAYMENT" print(f"Orchestrator: Payment failed. Initiating compensation.") self._send_compensate_inventory_service(self.context) # Reservierung der Bestandsdaten kompensieren self._send_compensate_order_service(self.context) # Erstellung der Bestellung kompensieren print(f"Orchestrator: Saga for Order {self.order_id} failed and compensated.") # --- Das Saga ausführen --- if __name__ == "__main__": order_id = "ORDER-XYZ-123" order_details = { "customer_id": "CUST-001", "items": [{"item_id": "ITEM-A", "quantity": 2}, {"item_id": "ITEM-B", "quantity": 1}], "total_amount": 150.00 } orchestrator = OrderCreationOrchestrator(order_id) orchestrator.start_saga(order_details) print("\n--- Simulation eines Fehlszenarios (z.B. Zahlungsausfall) ---") orchestrator_failure = OrderCreationOrchestrator("ORDER-XYZ-FAIL") order_details_failure = { "customer_id": "CUST-002", "items": [{"item_id": "ITEM-C", "quantity": 1}], "total_amount": 50.00 } # Manuelles Simulieren von Ereignissen zur Demonstration von Fehlschlag und Kompensation orchestrator_failure.start_saga(order_details_failure) # An dieser Stelle würde das Ereignis order_created normal behandelt # Dann würde das Ereignis inventory_reserved normal behandelt # Jetzt simulieren wir direkt einen Zahlungsausfall orchestrator_failure.handle_event("payment_failed", {"order_id": "ORDER-XYZ-FAIL", "reason": "Insufficient funds"})
Erklärung des Codebeispiels:
OrderCreationOrchestrator
: Dies fungiert als unser Saga-Orchestrator. Er verwaltet den Zustand der gesamten Transaktion (self.state
) und den Kontext, der für nachfolgende Schritte (self.context
) benötigt wird.start_saga
: Initiiert den Workflow durch Senden des ersten Befehls an denOrder Service
._send_command_to_X_service
: Diese Methoden simulieren das Senden von Nachrichten (Befehlen) an verschiedene Microservices. In einer realen Anwendung würde dies das Veröffentlichen von Nachrichten in einem Nachrichtenbroker (z. B. Kafka, RabbitMQ) beinhalten._simulate_X_service_response
: Diese Methoden simulieren die Antwort einzelner Microservices nach Abschluss ihrer lokalen Transaktion. Sie emittieren Ereignisse, die der Orchestrator dann verarbeitet.handle_event
: Dies ist die Kernlogik des Orchestrators. Basierend auf dem eingehenden Ereignis und dem aktuellen Saga-Zustand entscheidet er über die nächste Aktion: entweder die Fortsetzung zum nächsten Schritt, den Abschluss der Saga oder die Einleitung eines Kompensationsworkflows.- Kompensationslogik: Wenn ein Ereignis wie
payment_failed
empfangen wird, löst diehandle_event
-Methode eine Sequenz von_send_compensate_X_service
-Aufrufen aus. Diese Aufrufe weisen die zuvor erfolgreichen Dienste an, ihre Aktionen semantisch rückgängig zu machen.
Anwendungszenarien
Das Saga-Pattern eignet sich besonders gut für Szenarien in Microservice-Architekturen, in denen:
- Geschäftsprozesse mehrere Dienste und Datenbanken umspannen: E-Commerce-Bestellabwicklung, Hotelbuchungssysteme, Flugreservierungen.
- Starke Konsistenz über alle Dienste hinweg nicht streng in Echtzeit erforderlich ist, aber eventual consistency mit Atomizitätsgarantien von größter Bedeutung ist: Es ist akzeptabel, dass das System vorübergehend inkonsistent ist, solange es schließlich einen konsistenten Zustand erreicht oder vollständig zurückgerollt wird.
- Traditionelle verteilte Transaktionslösungen (XA-Transaktionen) nicht praktikabel sind oder zu viel Overhead verursachen: XA-Transaktionen sind oft eng gekoppelt und weisen in hochverteilten, autonomen Microservice-Umgebungen eine schlechte Leistung auf.
- Dienste lose gekoppelt bleiben müssen: Das Saga-Pattern ermöglicht es Diensten, sich unabhängig zu entwickeln, ohne ihre Datenbanktransaktionen direkt koordinieren zu müssen.
Wichtige Überlegungen
- Idempotenz: Alle Befehle und Kompensations-Transaktionen sollten idempotent sein. Das mehrmalige Senden desselben Befehls sollte denselben Effekt haben wie das einmalige Senden.
- Überwachung und Beobachtbarkeit: Sagas können langlaufend sein und viele Schritte umfassen. Robuste Überwachung, Protokollierung und Tracing sind unerlässlich, um den Zustand einer Saga zu verstehen und Fehler zu diagnostizieren.
- Fehlerbehandlung und Wiederholungsversuche: Berücksichtigen Sie, wie Dienste vorübergehende Fehler behandeln werden. Der Orchestrator oder einzelne Dienste benötigen möglicherweise Wiederholungsmechanismen.
- Zustandsverwaltung: Der Orchestrator muss seinen Saga-Zustand speichern, um Abstürze zu überstehen und die Ausführung fortzusetzen.
- Timeouts: Die Implementierung von Timeouts für jeden Schritt der Saga ist entscheidend, um zu verhindern, dass eine Saga unendlich lange hängt, wenn ein Dienst nicht antwortet.
Fazit
Das Saga-Pattern bietet eine praktische und leistungsstarke Lösung für die Verwaltung verteilter Transaktionen in komplexen Microservice-Architekturen. Indem atomare Operationen in eine Sequenz lokaler, unabhängiger Transaktionen unterteilt und robuste Kompensationsmechanismen bereitgestellt werden, gewährleistet es die Datenkonsistenz über disparate Dienste hinweg, ohne die Vorteile von Microservices zu opfern. Obwohl es eigene betriebliche Komplexitäten in Bezug auf Koordination und Fehlerbehandlung mit sich bringt, macht die Fähigkeit, transaktionale Integrität widerstandsfähig und skalierbar zu gewährleisten, Saga zu einem unverzichtbaren Werkzeug für den Aufbau robuster verteilter Systeme. Es ist ein Beweis dafür, wie intelligentes Design die inhärenten Herausforderungen verteilter Systeme überwinden kann, indem es Eventual Consistency und betriebliche Garantien in den Vordergrund der modernen Anwendungsentwicklung bringt.