Von Silas Graffy

Voraussichtliche Lesedauer: 9 Minuten

Techblog

Event Sourcing, Part 2/2: Sinnvolle Einsatzszenarien und das Persistieren von Events

Nachdem ich im ersten Teil dieser Serie erläutert habe, was Event Sourcing überhaupt ist, es von CQRS abgegrenzt und Modellierungsbeispiele gegeben habe, beschäftige ich mich in diesem Teil mit typischen Anwendungsfällen für Event Sourcing und Möglichkeiten zur Implementierung der Persistenzschicht. Am Schluss verlinke ich auf weiterführende Informationen. Sinnvolle Einsatzszenarien Nachvollziehbarkeit Einige Anwendungsfälle von Event Sourcing drängen…

Techblog

Nachdem ich im ersten Teil dieser Serie erläutert habe, was Event Sourcing überhaupt ist, es von CQRS abgegrenzt und Modellierungsbeispiele gegeben habe, beschäftige ich mich in diesem Teil mit typischen Anwendungsfällen für Event Sourcing und Möglichkeiten zur Implementierung der Persistenzschicht. Am Schluss verlinke ich auf weiterführende Informationen.

Sinnvolle Einsatzszenarien

Nachvollziehbarkeit

Einige Anwendungsfälle von Event Sourcing drängen sich geradezu auf, andere erschließen sich erst auf den zweiten Blick. Zu ersteren gehören Audit Logs. Manchmal fordert der Kunde / die Kundin, zum Beispiel aus Compliance-Gründen, ein sogenanntes Audit Log, in dem festgehalten wird, wer wann welchen Wert wie geändert hat. Ein Event Sourcing Domain Model liefert dieses Log ganz automatisch. Es sind ja die Events selbst, die diese Informationen vorhalten. Eine Manipulation des Systemzustands „am Log vorbei“ ist nicht möglich, schließlich lässt sich der Systemzustand nur über die Einträge im Log, die Events, verändern. Auch bestimmte Fachlichkeiten, darunter Bankkonten und verwandte Anwendungen, aber auch Sagas, passen sehr gut zu Event Sourcing (siehe dazu auch den ersten Teil dieser Miniserie).

Rückgängig machen und wiederherstellen

Auch einen Undo/Redo-Mechanismus bekommen Entwickler_innen und Anwender_innen beim Einsatz von Event Sourcing frei Haus, geht es hier doch lediglich darum, ein Event mehr oder weniger abzuspielen bzw. ein inverses Event zu einem vorherigen in den Stream zu legen.

Verteilte Systeme

Weniger augenscheinlich profitieren verteilte Systemarchitekturen, zum Beispiel Microservices, von Subsystemen, die Event Sourcing betreiben. Beim genaueren Hinsehen zeigt sich jedoch, dass sich Events anderen abonnierenden Komponenten als Nachrichten zur Verfügung stellen (publish/subscribe) lassen. Das ist besonders sinvoll, wenn etwas Relevantes passiert ist, sprich: das Event gespeichert wurde. Somit können die Zustände aller beteiligten Systeme synchron gehalten werden. In aller Regel geschieht dies asynchron, eventual consistency ist die Folge.

„Snapshots“ von Datenständen innerhalb der Domäne und alternative Wirklichkeiten

Aber auch speziellere Anforderungen sind denkbar. So ist es in unserem aktuellen Projekt wichtig, dass bestimmte Datenstände einer breiteren Öffentlichkeit zur Verfügung gestellt werden („veröffentlicht“), während eine spezialisierte User-Gruppe weiter auf dem global aktuellen Datenstand arbeitet. Dafür erzeugen wir ein „Veröffentlicht“-Event, das im Wesentlichen aus dem Zeitpunkt der Veröffentlichung besteht. Greift nun ein anonymer Nutzer oder eine anonyme Nutzerin auf das System zu, werden sämtliche Event-Streams immer nur bis zum Zeitpunkt der letzten Veröffentlichung abgespielt, er bzw. sie sieht alle Daten so, wie sie zu diesem Zeitpunkt vorlagen. Die spezialisierte Gruppe dagegen sieht immer den jetzt aktuellen Zustand und kann ihn weiterbearbeiten, bis auch er veröffentlicht wird. Diese Funktionalität entspricht im Wesentlichen dem Taggen von Versionsständen in Versionsverwaltungssystemen wie Git plus Rechtesteuerung für die einzelnen Tags und den Hauptzweig.

Die zugrundeliegende Idee, das sog. Parallel Model kann auch eingesetzt werden, um alternative Datenszenarien herzustellen oder Verarbeitungsschritte im Nachhinein als „Replay“ abzuspielen, beispielsweise zu Schulungs- und Analysezwecken.

Performance bei Schreibzugriffen und differenzielle Updates

Ich erinnere mich an ein Sanierungsprojekt, an dem ich mal gearbeitet habe. Damals hatte ein Kunde eine Art Hybrid aus Editor und Versionskontrollsystem für Artefakte aus seiner Domäne erstellt. Als Software Crafter kennen wir solche Versionskontrollsysteme sehr gut. Anders als auf Textdateien arbeitete das System jedoch auf ziemlich großen und komplexen Objektmodellen, die serverseitig per objekt-relationalem Mapper in eine relationale Datenbank persistiert wurden. Dabei wurde der jeweils aktuelle Stand plus Änderungslog gespeichert, nicht als Event Stream.

Wir wurden im Projekt um Hilfe gebeten, da vor allem Schreibzugriffe auf das System mit steigender Komplexität der Anwendung und Datenstrukturen sehr langsam geworden waren. Der Grund dafür lag, neben dem O/R-Mapper selbst, in den vielen Diffs, die client- und serverseitig während des Speichervorgangs erzeugt, übertragen und angewendet werden mussten. Event Sourcing hätte all das überflüssig gemacht und weiterhin das eigentliche Persistieren zu einer trivialen und schnellen Operation werden lassen.

Apropos Persistenz – zum Schluss der Serie möchte ich darauf noch kurz eingehen.

Persistenz im Detail

Es gibt zahlreiche Möglichkeiten, einen Event Store zu implementieren. Zum einen existieren fertige Lösungen wie die gleichnamige Open Source-Datenbank. Weiterhin gibt es auch Bibliotheken wie NEventStore, die eine API auf Basis unterschiedlicher Storage Engines anbieten. Anderseits können auch Dokumentendatenbanken wie RavenDB oder MongoDB, oder andere NoSQL-Datenbanken, etwa Cassandra für Event Sourcing, direkt genutzt werden. Schließlich kommt oft auch eine gute alte relationale Datenbank zum Einsatz. 

Die Gründe hierfür können zahlreich sein: Manchmal geben der Kunde oder die Kunden es schlicht vor (wie bei uns), mal liegt es am verfügbaren Know-How in Entwicklung oder Vertrieb, mal an den zahlreicheren Hostingmöglichkeiten, der einfachen Nutzung desselben Systems für zusätzliche relationale Read Models, oder oder oder…

In jedem Fall ist das Schema denkbar einfach: Wir nutzen beispielsweise genau eine Event-Tabelle, sie enthält neben einer globalen Sequenz noch den Typ des Events, die ID und Version der Entität, auf die es sich bezieht (die Stream-ID), den Zeitpunkt des Auftretens und schließlich den eigentlichen Payload, in unserem Fall als JSON. Andere Serialisierungsformate sind selbstverständlich denkbar, meine Präferenz läge aber immer auf einem menschenlesbaren Format. Anderenfalls wären alle Daten der Anwendung unwiederbringlich verloren, sollte einmal keine Deserialisierungsmöglichkeit mehr bestehen, etwa weil eine alte exotische Binärserialisierungs-Library nicht mehr zu bekommen ist (und deren Doku auch nicht).

„Schema“-Änderungen

Wenn sich Event-Formate im Lebenszyklus einer Anwendung weiter entwickeln, entspricht das einer Schema-Änderung. Lassen sich zuvor persistierte Events mit der neuen Version der Anwendung nicht mehr deserialisieren, wird auch hier eine Art Migration erforderlich. Dafür gibt es mehrere Möglichkeiten. Natürlich könnte man während des Updates den persistierten Event Stream migrieren, also auf das neue Format heben. Da wie oben beschrieben Events per definitionem unveränderlich sein sollten, versucht man das zu vermeiden.

Eine Alternative wäre, das Domain Model abwärtskompatibel zu halten. Im einfachsten Fall hat das Event in der neuen Version nur ein zusätzliches, optionales Feld erhalten. Dann könnten auch zuvor persistierte Events weiterhin verwendet werden. Im komplizierteren Fall hat das Event selbst breaking changes erfahren. Aber selbst dann kann der Rest des Systems so gestaltet werden, dass er beide Versionen des jeweiligen Event-Typs verarbeiten kann. Der Nachteil einer solchen Lösung springt jedoch schnell ins Auge: Mit der Zeit „vermüllt“ unser Domain Model. Der jeweils aktuelle Teil ist schwerer zu erkennen, da es eine ganze Menge Klassen gibt, die nur dem Lesen und Verarbeiten alter Event-Formate dienen.

Daher besteht eine weitere Möglichkeit darin, nicht das ganze Domain Model abwärtskompatibel zu halten, sondern nur den Event Store. In diesem Fall kennt dieser alte und aktuelle Versionen aller Events und kann somit beim Auslesen eines alten Events zur Laufzeit eine Migration auf den aktuellen Stand durchführen, und das oder die aktuelle(n) Event(s) an das Domain Model der Anwendung übergeben. Damit findet die eigentliche Migration dort statt, wo sie hingehört, nämlich in der Persistenz-Komponente. Zur Anwendung hin verhält sie sich als Blackbox und das Domain Model bleibt sauber. Weitere Möglichkeiten gibt’s in dem Buch von Greg Young (siehe unten).

Mein Fazit:

  • Event Sourcing ist ein Persistenzmechanismus, bei dem der aktuelle Zustand einer Entität nicht direkt gespeichert wird, sondern in Form eines chronologischen Event Streams.
  • Die Kombination mit dem Architekturmuster CQRS ist häufig sinnvoll, aber keineswegs zwingend. Die Konzepte sind orthogonal zueinander.
  • Es gibt viele sinnvolle Anwendungsszenarien für Event Sourcing, darunter Sagas, Systeme mit hohen Anforderungen an Nachvollziehbarkeit und verteilte Systeme. Die bekanntesten Beispiele beinhalten Versionskontrollsysteme wie Git.
  • Als „Storage Engine“ für Event Sourcing bieten sich fertige Server-Lösungen und Frameworks sowie NoSQL- und relationale Datenbanken gleichermaßen an. Besonderes Augenmerk legen umsichtige Teams auf „Schema“-Änderungen, also Anpassungen der Event-Formate.

Wer mehr lesen möchte…

… dem lege ich diese Bücher, Blog-Artikel und Folien ans Herz:


Über den Autor

Von Silas Graffy

IT-Sanierung 

Silas stieß 2015, nach 15 Jahren Produktentwicklung & Forschung, zu MaibornWolff. Schwerpunkte des Informatikers aus Leidenschaft bilden Software-Architekturen, agile Softwareentwicklung und System- & Architektur-Audits. Software- und Code-Qualität sind ihm ebenso ein Herzensanliegen wie Teamkultur und Collective Ownership. Er hat Abschlüssse in angewandter und Wirtschaftsinformatik. In der Freizeit gelten für ihn die wichtigen 4C: Code, Coffee, Cocktails — and Climbing.