Die Artikel dieser Reihe
Einleitung
Im ersten Teil dieser Artikelserie wurde das in dem Projekt auftretende Problem umrissen und schon einige Hinweise auf die Lösung gegeben. Stichworte wie Transactional Outbox und Dual Write Problem wurden kurz erläutert.
Sie haben es richtig bemerkt, von einem EventBus (siehe Überschrift) im Kontext Microservices sind wir noch weit entfernt; be patient!
Zur Erinnerung: der Gesamtkontext
Die gestrichelte Linie - insbes. die Kommunikationsbeziehungen - bildete das initiale Problem ab, aus diesem Problem sollte ein Ansatz geschaffen werden, der allgemeingültig für die anderen Kommunikationsverbindungen ist. Hierbei wurde kundenseitig ausschließlich auf JMS gesetzt.
Bleiben wir zunächst auf der Seite des Sendens von Nachrichten, hier kann aus der sehr allgemeinen Einführung durchaus noch Verbesserungspotential hergeleitet werden. Be patient, Themen wie das Senden und Empfangen kommen noch.
So let`s start.
Trennen wir die Dinge zunächst einmal auf:
Es existieren Kommunikationsbeziehungen (vulgo Schnittstellen) unterschiedlichsten Charakters. Da haben wir:
Eine bedeutsame Änderung - ein Ereignis - ist in einem der Systeme aufgetreten, andere Systeme sollten auf diese Änderung reagieren.
Ein Beispiel hierfür wäre: Der Produktionsauftrag ist abgeschlossen, der Kundenauftrag ist erfüllt, die Rechnung kann gestellt werden.Ein System benötigt weitere Informationen aus einem anderen System um seine Aufgabe zu erfüllen.
Ein Beispiel hierfür wäre: Um den Produktionsauftrag abzuschließen werden Detailinformationen aus dem Kundenauftrag benötigt.…
Im Grunde genommen wird schnell klar, dass es sich hier um verschiedene Dinge handelt. Im zweiten Fall ist eine direkte Kommunikation zwischen zwei Systemen gegeben und es existiert eine Abhängigkeit zwischen dem Prozess im aufrufenden System und der Antwort des aufgerufenen Systems und die Systeme “kennen“ einander - sie sind eng gekoppelt. Diese Art der Kommunikation ist “unproblematisch“, es existieren eine ganze Reihe von Pattern und Best Practices um mit dieser Form der Kommunikation umzugehen. Hier war das Problem auch nicht zu suchen.
Im zweiten Fall ist es so, dass es dem Sender eben nicht bekannt ist, wer alles Empfänger der Nachricht ist - sie sind lose gekoppelt. Im Fokus steht also das zuverlässige Senden an eine unbekannte Anzahl von Empfängern.
Event-Sourcing und EventMessage Log
Zentraler Begriff ist der Begriff des Events:
An event is defined as a change of state of some key business system. For instance, somebody buys a product, someone else checks in for a flight or a bus is late arriving somewhere. And if one thinks about it, events exist everywhere and are constantly happening, no matter what industry. [… ]The event is separate from the message, because while the event is the occurrence, the message is the traveling notification that relays the occurrence. In event-driven architecture, an event will likely trigger one or more actions or processes in response to its occurrence.
— Tibco
Dem aufmerksamen Leser des ersten Teil ist mit Sicherheit aufgefallen, dass das “Handling“ des Sendens und die Statusänderungen (die Events) des Produktionsauftrages ein fröhliches Potpuri in der Tabelle Event-Log sind: weder schön noch pragmatisch.
Trennen wir die Dinge - divide et impera (Machiavelli)!
Der Sinn des Event-Sourcing
Event Sourcing (siehe Martin Fowler) ist ein Software-Entwurfsmuster, das bei der Entwicklung von Anwendungen verwendet wird, insbesondere bei solchen, die eine Verfolgung und Verwaltung der Zustandsänderungen von Entitäten oder Objekten im Laufe der Zeit erfordern - es wird häufig im Zusammenhang mit verteilten Systemen und domänenorientiertem Design verwendet. Die grundlegende Idee hinter Event Sourcing ist die Speicherung einer Sequenz von unveränderlichen Ereignissen, die Änderungen am Zustand eines Domänenobjektes oder einer Anwendung darstellen. Diese Ereignisse (Events) können als ein historisches Protokoll aller Ereignisse im System (innerhalb eines Zeitraumes) betrachtet werden.
Summa summarum werden zu jeder Statusänderung (Event) eines Domänenobjektes ein entsprecher Eintrag erzeugt. Irgendetwas in dem Sinne von:
Am [DATUM/ZEIT] änderte sich der Zustand des Domänenobjekts [hier: Produktionsauftrag] von [Ursprünglicher Zustand] zu [neuer Zustand].
Dieses Event wird identifiziert über [EventID] und steht im Zusammenhang mit Geschäftsvorfall [ID als Klammer über zusammenhängende Events].
Dieses Event wird klassifiziert, bspw. in der Form: System.Domänenobjekt.Vorfall
Also sowas wie PRODUCTION.PRODUCT.FINISHED
Der Sinn dieser Klassifikation ergibt sich aus der Idee, dass diese Events natürlich in den verschiedensten Systemen vorkommen; also ein Event der Form CUSTOMER.ORDER.RECEIVED leicht vorstellbar ist.Weitere Informationen hierzu sind [Informationen von Belang im konkreten Kontext zum Ereignis]
Wie man diese Events speichert (normalisiert in einer relationalen Datenbank oder als JSON-Struktur) ist eigentlich unerheblich, solange man diese zugreifbar über geeignete Zeiträume hält.
Natürlich ist es möglich, über diese Events statistische Auswertungen zu fahren, Events auftragsbezogen zu korrelieren, … - dazu in der Artikelserie später mehr.
Das Message-Log
Wir haben uns entscheiden, zwischen dem Event und der zugehörigen Nachricht und dem Senden zu unterscheiden. Events bilden ab, was am Domänenobjekt geschieht; das Message-Log beinhaltet Informationen darüber:
was,
wann,
mit welchem Erfolg
gesendet wurde.
Am Ende des Tages haben wir jetzt sowas wie:
Nehmen wir den fiktiven Prozess innerhalb des Produktionssystem innerhalb dessen die einzelnen Positionen des Produktionsauftrages abgearbeitet werden, dann wird sicherlich geprüft, ob der Produktionsauftrag abgeschlossen wurde. Ist dieses der Fall, dann → finishProductionOrder.
Die ProductionOrder wird aktualisiert:
Auf Basis der Daten der ProductionOrder wird ein EventLog-Eintrag hinzugefügt:
Dieser EventLog-Eintrag beinhaltet alle signifikanten Daten die im Zusammenhang mit dem Event stehen.
Schlussendlich wird ein initialer Eintrag zum EventMessageLog hinzugefügt:
Takeaways:
Wie man den (exemplarischen) Einträgen in den Tabellen entnehmen kann, steht das Event in engem Zusammenhang mit dem Domänenobjekt (man denke sich ein Event der Event-Klassifikation “PRODUCTION_ORDER_STARTED“ hinzu) und die Summe der Events zu einem Domänenobjekt bildet die Statushistorie ab.
Aus dem Eintrag im EventLog wird eine Event-Nachricht erzeugt (und später über eine beliebige Technologie an die Umsysteme / -services verschickt.
Es existiert eine eindeutige Transaktions-ID, diese ermöglicht eine Korrelation aller Events über alle Systeme; ganz allgemein gesprochen kann man über diese ID vom Auftragseingang über die Auftragsprüfung und weiter über die Produktion bis hin zur Rechnungsstellung den gesammten Geschäftsprozess verfolgen und analysieren oder Bottelenecks identifizieren.
Das Modell lässt sich noch beliebig erweitern (Attribute etc.) und verbessern (CQRS-Pattern bspw.), einen sehr interessanten Artikel hierzu finden Sie hier.
Did I told ya? (die Nachrichten)
Was haben wir denn jetzt: wir speichern Events, wir speichern ein Protokoll über den Versand - vielleicht ist es an der Zeit sich den Nachrichten als solchen zuzuwenden.
Start small, thing big: es erscheint doch recht sinnvoll, die Nachrichten, die zwischen den Applikationen / Services (you name it) versendet werden zu standardisieren. Es macht ja irgendwie wenig Sinn, wenn der Austausch von Event-Nachrichten zwischen System / Service A und System / Service B sich hinsichtlich des Formats grundlegend von dem Format des Nachrichtenaustausches zwischen System / Service X und System / Service Y unterscheidet.
Wohlgemerkt, wir reden jetzt darüber, was wir senden; (noch) nicht darüber wie wir es senden.
Messaging ist jetzt ja nicht die neueste Innovation und jeder von uns hat schon einen Brief in den Händen gehalten. Was macht man, wenn man einen Brief in die Hände nimmt? Die Fragenstellungen lassen sich ungefähr wie folgt zusammenfassen:
Wer ist der Absender?
Bin ich der Empfänger?
Worum dreht es sich, muss mich das interessieren?
Wenn es mich interessiert, dann lese ich den Inhalt und reagiere (unter Umständen).
Also setzen wir die Nachricht doch einfach aus einem Header und Body zusammen:
{
"messageHeader" : {
"TRANSACTION_ID" : "19d2f155-9fa0-4707-bc71-307c2665163f",
"CREATED_WHEN" : "2024-05-19T15:33:12.502379",
"EVENT_CLASSIFICATION" : "PRODUCTION_ORDER_FINISHED",
"REPLY_TO" : "Some JMS destination or URI",
"MESSAGE_DATATYPE" : "JSON",
"MESSAGE_SCHEMA_URI" : "Some URI to a schema",
"SENDER_IDENTIFIER" : "PRODUCTION",
"EVENT_ID" : "97a0d238-6e55-42d0-b978-868ff409650f",
},
"messageBody" : {
"ProductQuantity" : 99,
"DomainId" : "25",
"DomainClass" : "de.brockhaus.production.domain.entity.product.ProductionOrder",
"ProductId" : "PRODUCT_456",
"CustomerId" : "CUSTOMERID_123"
}
}
Versetzen wir uns kurz auf die Empfängerseite:
Zu welchem Geschäftsprozess die Nachricht gehört erkenne ich über die TRANSACTION_ID
Was in dem Geschäftsprozess geschehen ist, sehe ich über die EVENT_CLASSIFICATION
Wer gesendet hat sehe ich an SENDER_IDENTIFIER
Was gesendet wird sehe ich am MESSAGE_DATATYPE, ggf. gibt es auch einen Hinweis auf ein Schema (MESSAGE_SCHEMA_URI)
Jetzt kann ich doch entscheiden, ob:
die Nachricht für mich überhaupt interessant ist (Sie ahnen es, es läuft auf MessageSelectors raus)
wie ich mit dem Inhalt der Nachricht umgehen muss (hier also einen JSON-Parser verwenden).
Ich will mich nicht mit fremden Federn schmücken, dahinter steckt cloudevents.io in einer “angepassten“ Form
CloudEvents ist eine Open-Source-Initiative, die von der Serverless Working Group der CNCF ins Leben gerufen wurde. Die Idee hinter CloudEvents besteht darin, einen einheitlichen Standard für das Format und die Übertragung von Ereignissen zu schaffen, unabhängig von der zugrunde liegenden Technologie und Cloud-Plattform.
Selbstredend ist die Event-Nachricht eine eigene Java-Klasse, hier nur grob dargestellt:
public class EventMessage {
// header data keys
public enum MessageHeaderProperties {TRANSACTION_ID, EVENT_ID, EVENT_CLASSIFICATION, DOMAINOBJECT_ID, DOMAIN_CLASS, SENDER_IDENTIFIER, REPLY_TO, MESSAGE_DATATYPE, MESSAGE_SCHEMA_URI, CREATED_WHEN };
// header data
private HashMap<MessageHeaderProperties, String> messageHeader = new HashMap<>();
// payload as key/value pairs
private HashMap<String, Object> messageBody = new HashMap<>();
...
Wie man dem Obigen entnehmen kann, besteht eine Event-Message aus einem Header und einem Body. Warum nun dieses? Die Antwort ist simple: Will man von der konkreten Sende-Technologie abstrahieren, dann macht es Sinn, eigene Header-Felder zu haben. Diese können dann entsprechend in bspw. JMS-Nachrichten gesetzt werden:
Der Weg zu einer eigenen MoM?
Kritische Stimmen merkten an, dass das, was mit dem Speichern der Event-Nachrichten und dem Message-Log vorgeschlagen wurde doch genau die Funktionalität einer Message-oriented Middleware abbilden würde und dieses doch bereits in Form des JMS-gestützten Messaging Systems bereits existieren würde.
Da ist schon etwas dran, aber es gibt doch eine Reihe gravierender Unterschiede und Vorteile aus dem Ansatz.
Zuallererst sollten wir uns vor Augen führen, dass das Kernproblem war, das Senden von Events und das Festhalten des Sendungsstatus in einer atomaren Operation das Kernproblem war und daher einen Teil des Problems zur Lösung zu erklären nicht wirklich hilfreich ist.
Weiterhin beinhaltet der Ansatz eine Reihe signifikanter Vorteile, insbesondere die Historisierung der Änderung von Domänenobjekten respektive der systemübergreifenden Auswertbarkeit auf der Ebene des Geschäftsprozesse über die TransactionID.
Zusammengefasst
D
ie Senderseite macht soweit erst einmal einen ordentlichen Eindruck, doch da war etwas mit Dual Writes, Transaktionen, doppeltem Senden. Einem Teil dieser Aspekte soll sich der folgende Artikel widmen.
Bleiben Sie mir gewogen.