Beliebte Suchanfragen
//

Wer Microservices richtig macht, braucht keine Workflow Engine und kein BPMN

31.8.2015 | 10 Minuten Lesezeit

Bernd Rücker und Daniel Meyer schreiben im Fazit von „Wie lässt sich Ordnung in einen Haufen (Micro)Services bringen“ , dass

gerade in einer Microservices-Architektur eine Workflow-Engine und BPMN einen festen Platz haben sollten.

Das ist eine pauschale Aussage. Man kann das zwar machen, aber es ist für mich alles andere als eine logische Konsequenz, so wie die Autoren es darstellen. Es ist ein Trade-Off, der einem wieder Nachteile einhandelt, die die wesentlichen Vorteile von Microservices zunichte machen können.
So, wie die beiden Microservices verstehen und darstellen, kommt man fast nicht umhin, ihnen Recht zu geben. Ich nehme mir jetzt mal den Geschäftsprozess aus obigem Artikel und zeige, wie man den mit Microservices so implementiert, dass sie wirklich Sinn machen. Und dann können wir am Ende die beiden Lösungen vergleichen.

Der Bestellprozess in einer Microservice-Architektur

Grundsätzlich sehe ich es auch so, dass Microservices möglichst asynchron kommunizieren sollten. Das Mittel der Wahl sollten aber nicht zielgerichtete Messages (Service A schickt eine Nachricht speziell an Service B), sondern Events ohne explizites Ziel sein. Services registrieren sich für Events, stoßen dann eine Verarbeitung an und erzeugen danach wieder ein Event. Service A weiß gar nicht, dass Service B existiert. Die Events müssen natürlich persistiert werden, damit Events im Sinne von Event Sourcing auch nachverarbeitet werden können. Soviel zur Theorie, schauen wir jetzt mal auf das BPMN-Diagramm des Geschäftsprozesses, den wir umsetzen wollen (ein Klick auf die Graphik vergrößert sie):



Ein typischer vereinfachter Bestellprozess. Das folgende Diagramm zeigt die Microservices des Gesamtsystems zusammen mit den Event-Typen, die geschrieben werden, für eine Umsetzung ohne Workflow Engine. Die Pfeile mit durchgezogenen Linien bedeuten dabei, dass ein Service ein Event schreibt, die Pfeile mit den dünnen gestrichelten Linien bedeuten, dass ein Service sich für das Event registriert hat. Zusätzlich dazu gibt es noch die dick gestrichelten Linien, die synchrone Aufrufe von der Benutzeroberfläche darstellen. Ein Klick auf die Graphik vergrößert sie.



Um das prinzipielle Vorgehen zu verdeutlichen, beschreibe ich im Folgenden zwei ausgewählte Services. Wer sich für weitere Details interessiert: die anderen Services und Events werden am Ende des Artikels im Anhang beleuchtet.

  • BestellEingangService. Wird synchron vom Kunden aufgerufen und hat im Wesentlichen die Aufgabe, eine eindeutige ID der Bestellung zu erzeugen, die im gesamten System verwendet wird. Schreibt die Bestellung dann mit erzeugter ID in ein BestellungErzeugtEvent.
  • WarenReservierungService. Dieser Service horcht auf BestellungErzeugtEvents und versucht dann, die bestellten Waren zu reservieren. Gelingt das, wird ein WarenReserviertEvent verschickt. Gelingt das nicht, wird ein WarenNichtReserviertEvent verschickt. Außerdem horcht dieser Service auf BestellungAbgebrochenEvents. Wurden die Waren für eine abgebrochene Bestellung bereits reserviert, so wird diese Reservierung gelöscht. Ansonsten merkt sich der Service, dass eine Bestellung mit der übergebenen ID abgebrochen wurde und verhindert das Reservieren der Ware, wenn ein BestellungErzeugtEvent mit der gleichen ID nachträglich noch hereinkommt.

Vergleich mit Lösungen aus Bernds und Daniels Artikel

Asynchrone Microservices mit Punkt-zu-Punkt-Verbindungen

Die erste, auf asynchronen Microservices basierende Lösung in Bernds und Daniels Artikel geht davon aus, dass wir Punkt-zu-Punkt-Verbindungen über Queues realisieren. Die Nachteile, die die beiden aufzählen, greifen dann auch. In meiner obigen, Event-basierten Lösung greifen sie nicht. Ich gehe sie mal der Reihe nach durch.

Punkt-zu-Punkt-Verbindungen

Bernd und Daniel bemängeln, dass in ihrer Lösung jeder Service den nächsten Schritt kennen muss, damit die Nachricht in die passende Queue zugestellt wird. Bei mir ist diese Kopplung nicht vorhanden.

Keine Lösung für Reihenfolge, gegenseitigen Ausschluss, Synchronisation und Timeouts

Hier geht es in unserem Fall um die Problematik des Bestellabbruchs. Bernd und Daniel schlagen vor, eine Kompensationsnachricht zu schicken, die jeder Service weiterreichen muss – hängt mit der Fehlannahme der Punkt-zu-Punkt-Verbindungen zusammen. Im Event-basierten System gibt es einen BestellAbbruchService, der auf Events horcht, die einen Bestellabbruch auslösen, und schreibt dann ein BestellungAbgebrochenEvent. In diesem steht natürlich auch die eindeutige ID der Bestellung. Alle Services, die das interessiert, weil sie eventuell Kompensation machen müssen, horchen auf dieses Event. Falls sie in ihrem Datenbestand noch keine Bestellung mit der ID haben, speichern sie trotzdem, dass die Bestellung abgebrochen wurde. Für den Fall, dass ein Event hereinkommt, bei dem der Service eigentlich aktiv werden würde, wird dann einfach nichts gemacht. Reihenfolge, Ausschluss und Synchronisation sind kein Problem.
Timeouts innerhalb eines Services sind auch kein Problem. In unserem Fall kann der GeldeinzugService die zwei Tage auf das ZahlungsdatenGeändertEvent warten und entsprechend aktiv werden, wenn es kommt, oder wenn es nach zwei Tagen nicht gekommen ist.

Fehlender Zustand einer Bestellung

Prinzipiell richtig, die einzelnen Microservices haben die Datenhoheit auf ihren Daten, und ein Weg, um den Zustand einer Bestellung zu erfahren, wäre, alle Services zu fragen. Ich würde da aber eher einen BestellStatusService schreiben, der alle Events zum Bestellprozess abonniert und die Daten bei sich dupliziert. Er hat keine Hoheit auf den Daten, er sammelt nur. Das ist auch für die Performance einer GUI besser. Für die Frage, ob eine Bestellung storniert werden kann, ist so ein BestellStatusService aber gar nicht notwendig, da reicht ein viel schlankerer StornierungService, der nur VersandVorbereitetEvents abonniert.
Wenn man darauf reagieren muss, wenn Bestellungen zu lang in einem Zustand verweilen (warum auch immer das passieren kann), kann man kleine, unabhängige Microservices bauen, die bestimmte Zustandsübergänge überwachen. Ein Service könnte beispielsweise BestellungErzeugtEvents, WarenReserviertEvents und WarenNichtReserviertEvents abonnieren. Immer, wenn nach einem BestellungErzeugtEvent eine Zeit X kein zugehöriges WarenReserviertEvent oder WarenNichtReserviertEvent kommt, löst der Service ein Alarm-Event aus. Darauf könnte dann wiederum ein NachrichtenService für Sachbearbeiter oder eventuell der BestellAbbruchService reagieren.

Steigende Komplexität

Ja, in der Realität sind viel mehr Services beteiligt, und da den Überblick zu behalten, ist eine Herausforderung. Ein BPMN-Diagramm, das genau so ausgeführt ist, ist eine perfekte Dokumentation, das ist charmant. Auch ein BPMN-Diagramm wird mit steigender Komplexität größer und größer, vor allem, wenn Fehlerbehandlungen und Asynchronität auch komplett abgebildet sind. Meine (wenigen) Erfahrungen mit BPMN sagen mir, dass diese Diagramme für Entwickler da sind und Sourcecode darstellen. Die Wartbarkeit leidet mit der Größe, dann muss man modularisieren mit Subprozessen, hat eventuell auch nicht mehr den guten Überblick. Für Diskussionen mit Fachbereichen muss man sie eh stark vereinfachen. Ich will damit nur sagen, dass die Welt der BPMN-Diagramme auch nicht so heil ist, wie sie vielleicht manchmal vermittelt wird.
In der Event-basierten Lösung ist es wichtig zu wissen, welche Services es gibt, welche Events sie auslösen und welche sie abonnieren. So eine Landkarte, vergleichbar mit einer Context Map aus DDD, kann automatisiert aus dem bestehenden System erstellt werden. Ein BPMN-Diagramm wird dagegen im Voraus erstellt und enthält zusätzliche Fachlichkeit, die im Event-basierten System den Services zugeordnet ist. Hier sehen wir schon die unterschiedlichen Denkweisen, die dahinter stehen, und wir kommen zum Thema Trade-Offs, auf das ich später noch eingehen möchte.

Microservices orchestriert durch eine Workflow Engine

Die Variante, die Bernd und Daniel empfehlen, beinhaltet einen Microservice, der die Workflow Engine und den ausführbaren Prozess enthält, und viele weitere Microservices, die die Fachlichkeit ausführen. Standardmäßig würden die Service-Aufrufe synchron durchgeführt (beispielsweise über eine REST-Schnittstelle), aber am Ende bringen die beiden noch die Variante der External Tasks ins Spiel, bei der sich Services Tasks zur Verarbeitung abholen können, wenn sie es wollen.
Dass man das so machen kann, steht außer Frage. Es steht auch außer Frage, dass es in einem bestimmten Kontext genau das richtige Vorgehen sein kann. Man kann darüber diskutieren, ob das am Ende tatsächlich eine Microservices-Architektur ist, aber auch das ist am Ende nur eine Spitzfindigkeit bei der Namensgebung, weil ja eigentlich nur zählt, dass man die richtige Lösung für das Problem findet, und nicht, wie die Lösung nun heißt. Kommen wir also nun zu den Trade-Offs, die ich schon mehrmals erwähnt habe.

Mit einer Microservices-Architektur möchte ich schnell werden. Ich will schnell neue Features in Produktion bringen.
Wie mache ich das?

  • Ich habe Full-Stack-Teams, die sich auf eine bestimmte Fachlichkeit konzentrieren können.
  • Ich minimiere Abstimmungsbedarf, indem ich die Laufzeit entkopple und synchrone Schnittstellen vermeide -> jeder kann in Produktion gehen, wann er will.
  • Ich minimiere Abstimmungsbedarf für Schnittstellen, indem ich Punkt-zu-Punkt-Verbindungen vermeide und Schnittstellen abwärtskompatibel mache. Wenn im BestellungErzeugtEvent alle relevanten Daten enthalten sind, die der BestellEingangService zu bieten hat, ist es seltener notwendig, dass jemand Anforderungen an den BestellEingangService stellt. Natürlich kann man den Abstimmungsbedarf nie auf Null senken.
  • Jeder Service hat die Hoheit über seine Daten, kann die speichern, wie er will, und verarbeiten, wie er will. Das minimiert Wartezeit und ebenfalls Abstimmungsbedarf.

Warum ist ein zentraler Prozess-Microservice ein Problem?
Der zentrale Prozess-Microservice wird zum Engpass.

  • Organisatorische Unklarheit. Das Team, das diesen Microservice betreut, implementiert Fachlichkeit aus verschiedensten Verantwortlichkeiten (und ja, ein BPMN-Modell beinhaltet Fachlichkeit, in unserem Beispiel mindestens die Wartezeit für die Zahlungsdatenänderung).
  • Änderbarkeit des Gesamtsystems. Sagen wir, der Marketing-Vorstand stürmt morgens das Büro und sagt, dass der Kunde aber unbedingt benachrichtigt werden muss, wenn die Zahlung erfolgreich ist. Das ist in meinem System eine lokale Änderung. Der KundenPostfachService muss GeldEingezogenEvents abonnieren und eine Nachricht an den Kunden schreiben. Es ist tatsächlich realistisch, das nachmittags in Produktion zu haben. Da die Events persistiert werden, können wir das sogar noch für x Tage nachfahren und nachträglich Nachrichten schreiben. Wenn eine neue Version des zentralen BPMN-Prozesses erzeugt und getestet werden muss, zusätzlich zu den Änderungen am PostfachService, ist man sicher nicht nachmittags in Produktion. Dafür sind zu viele Personengruppen und Verantwortlichkeiten beteiligt.
  • Datenhaltung. Welche Daten werden im Prozess gehalten? Alle für den Prozess relevanten Daten? Dann degenerieren die anderen Microservices zu zustandslosen SOA-Komponenten. Die Datenhaltung im Prozess ist dabei vermutlich generisch, es gibt keine Wahlmöglichkeit, Daten so zu speichern, wie es optimal wäre. Werden die Daten nicht im Prozess gehalten, sondern nur IDs weitergereicht, müssen die Microservices selbst aktiv weitere Microservices aufrufen, um sich die Daten zusammenzusammeln. Sehr wahrscheinlich muss das synchron geschehen und sorgt wieder für eine höhere Kopplung im Gesamtsystem. Ein Dilemma.

Fazit

Geschäftsprozesse sind immer da, ob man sie nun explizit macht mit BPMN oder implizit im Code. Workflow Engines und BPMN haben den Charme, dass der Prozess als ausführbares Diagramm in seiner Gesamtheit visualisiert wird und man genau sehen kann, was wann passiert. Gleichzeitig wird dieses ausführbare Diagramm zum Engpass und zur Herausforderung für die Änderbarkeit des Gesamtsystems.

Microservices haben das Ziel, die Änderbarkeit des Gesamtsystems zu optimieren. Man will möglichst schnell neue Features in Produktion geben. Ein zentraler BPMN-Service verringert die Änderbarkeit des Gesamtsystems wieder. Wenn aber die gute Änderbarkeit gar nicht benötigt wird, wird unter Umständen auch gar keine Microservices-Architektur benötigt.

Was ist uns also wichtiger? Zentrale Kontrolle oder Änderbarkeit? Beides können wir nicht optimieren. Diese Frage lässt sich nur Fall für Fall klären und hat viel mit der Mentalität und Organisationsstruktur des Unternehmens zu tun.
Die Aussage, dass eine Workflow Engine zu jeder Microservices-Architektur dazugehören sollte, ist auf keinen Fall richtig.

Anhang

Der Vollständigkeit halber hier die noch fehlenden Service-Beschreibungen.

  • GeldeinzugService. Dieser Service horcht auf WarenReserviertEvents und versucht dann, Geld einzuziehen. Gelingt das, wird ein GeldEingezogenEvent geschrieben, gelingt das nicht, wird zunächst ein GeldeinzugFehlgeschlagenEvent geschrieben. Folgt in den nächsten zwei Tagen kein ZahlungsdatenGeändertEvent, das die Bestellung betrifft, wird ein GeldeinzugNichtMöglichEvent geschrieben. Folgt ein passendes ZahlungsdatenGeändertEvent, so wird wieder versucht, das Geld einzuziehen und mit dem Ergebnis wie oben beschrieben umgegangen. Außerdem horcht dieser Service auf BestellungAbgebrochenEvents. Falls das Geld bereits eingezogen wurde, wird es zurückgebucht. Falls es noch keinen Eintrag zur Bestellungs-ID gibt, so merkt sich der Service die ID, damit zukünftig hereinkommende Events mit dieser ID nicht zu einem Geldeinzug führen.
  • VersandVorbereitungService. Dieser Service horcht auf GeldEingezogenEvents, bereitet den Versand vor und feuert abschließend ein VersandVorbereitetEvent. Außerdem horcht dieser Service auf BestellungAbgebrochenEvents, um bei zukünftig hereinkommenden Events mit der Bestellungs-ID die Versandvorbereitung gar nicht erst zu starten.
  • StornierungService. Horcht auf VersandVorbereitetEvents, um eine Stornierung direkt zu verhindern, wenn der Versand einer Bestellung bereits vorbereitet wurde. Hat eine synchrone Schnittstelle für das Auslösen einer Stornierung, die bei erlaubter Stornierung ein BestellungStorniertEvent schreibt.
  • BestellAbbruchService. Horcht auf BestellungStorniertEvents, WarenNichtReserviertEvents und GeldeinzugNichtMöglichEvents und feuert jeweils ein BestellungAbgebrochenEvent. Denkbar ist hier auch eine synchrone Schnittstelle für Sachbearbeiter.
  • KundenPostfachService. Horcht auf GeldeinzugFehlgeschlagenEvents und stellt dem Kunden eine Nachricht ins Postfach, die ihn darauf hinweist, innerhalb von zwei Tagen seine Zahlungsdaten zu korrigieren.
  • ZahlungsdatenService. Hat eine synchrone Schnittstelle für den Kunden, der hier Daten ändern und ansehen kann. Bei Datenänderung wird ein ZahlungsdatenGeändertEvent gefeuert.
  • BestellStatusService. Dieser Service horcht auf alle Events, die für den Bestellprozess relevant sind, und bereitet die Daten in einer eigenen Datenhaltung so auf, dass sie optimal von der GUI verwendet werden können. Es wird immer nur lesend auf die Daten zugegriffen. Natürlich könnte die GUI sich die Informationen auch bei allen Microservices einzeln zusammensammeln, dieser Service ist also streng genommen nicht notwendig. Das Horchen auf Events ist für diesen Service der Übersicht halber im Diagramm nicht dargestellt.

Beitrag teilen

//

Gemeinsam bessere Projekte umsetzen.

Wir helfen deinem Unternehmen.

Du stehst vor einer großen IT-Herausforderung? Wir sorgen für eine maßgeschneiderte Unterstützung. Informiere dich jetzt.

Hilf uns, noch besser zu werden.

Wir sind immer auf der Suche nach neuen Talenten. Auch für dich ist die passende Stelle dabei.