Event Sourcing Frameworks im Vergleich: Axon, Eventuate & Spring Cloud Stream

Von: Thomas Bayer, Oliver Weiler
Datum: 4. April 2018

Eventsourcing ermöglicht die lose Kopplung von Microservices. Auf der Basis eines Message Brokers wie z.B. Apache Kafka kann Eventsourcing selbst implementiert werden. Eine interessante Alternative ist die Verwendung eines speziellen Frameworks für das Eventsourcing. Diese Frameworks können die Umsetzung erleichtern, die Middleware austauschbar machen oder zusätzliche Features wie Snapshotting bieten.

Dieser Artikel vergleicht Eventsourcing Frameworks für die Java Plattform und dient als Entscheidungshilfe bei der Auswahl.

1 Probleme verteilter Microservices

Microservices ermöglichen es, Services getrennt entwickeln und deployen zu können. Neben diversen Vorteilen bringen Microservices auch eine Reihe von Nachteilen mit sich. Der verteilte Zustand stellt dabei die größte Herausforderung dar.

1.1 Verteile Daten

Jeder einzelne Microservice kümmert sich selbst um seine Datenhaltung und kann die für ihn optimale Datenbanktechnologie verwenden.Je nach Anforderung können sowohl SQL als auch NoSQL Technologien zum Einsatz kommen.

Dies macht den Austausch von Daten zwischen Services schwierig, da beispielweise Daten aus einem relationalen Model in ein nicht-relationales übertragen werden müssen.

1.1 Transaktionale Sicherheit

Wenn Services untereinander Daten austauschen, so kann dies über synchrone Protokolle wie HTTP erfolgen. Diese Protokolle besitzen mehrere Nachteile:

  • Das Anlegen eines Datensatzes ist mit einer gewissen Verzögerung (Latenz) verbunden, da die Änderung erst an alle beteiligten Parteien verteilt werden muss.
  • Geht eine Änderung schief, weil beispielsweise ein Service nicht verfügbar ist, so ist nun der Datenbestand über alle Services hinweg inkonsistent.

Eine Lösung scheinen verteilte Transaktionen zu bieten. Verteilte Transaktionen sind aber komplex, dürfen nur kurz laufen und der Ausgang hängt im Fehlerfall von einer Heuristik ab.

Wer sich mit der Thematik eingehender beschäftigen möchte, dem seit der Artikel: Distributed Transactions: The Icebergs of Microservices von Graham Lea empfohlen.

2 Terminologie

In diesem Abschnitt werden wichtige Begriffe für das Verständnis von Eventsourcing und den Vergleich der Frameworks beschrieben.

2.1 Eventsourcing

Beim Eventsourcing erzeugen Services Ereignisse, sobald eine Zustandsänderung stattfindet. Andere Services können diese Events abonnieren und ihrerseits ihren Zustand ändern und weitere Events erzeugen. Um Inkonsistenzen zu vermeiden, wird beim Eventsourcing jede Zustandsänderung über einen Bus kommuniziert. Nach einer kurzen Zeitspanne befinden sich alle Services in einem konsistenten Zustand. Man spricht dann von Eventual Consistency. Durch ein erneutes Einlesen aller Ereignisse kann der Zustand jedes Service rekonstruiert oder neue Services synchronisiert werden.

2.2 Snapshotting

Durch das Einlesen der gesamten Ereignishistorie kann der Anwendungszustand rekonstruiert werden. Da dieser Prozess lange dauern kann, wird oft periodisch oder manuell ein sogenannter Snapshot erstellt.

Dabei wird ein Ereignis, welches den aggregierten Anwendungszustand enthält, auf den Bus gelegt. Alle bisherigen Ereignisse werden verworfen, und es müssen nur noch die Ereignisse nach dem letzten Snapshot aggregiert werden.

Das Snapshotting ist eine reine Performanceoptimierung und sollte nur dann verwendet werden, wenn tatsächlich ein Performanceproblem vorliegt.

2.3 Eventual Consistency

Bei Eventual Consistency lockert man die Konsistenzgarantien um eine hohe Verfügbarkeit zu erreichen. Änderungen werden an beteiligte Services asynchron übertragen, d.h. die Änderungen sind in diesen Services erst nach unbestimmter Zeit sichtbar.

Der große Vorteil dieses Konsistenzmodells ist, dass Services zum Zeitpunkt der Synchronisation nicht verfügbar sein müssen, und der Service, welcher für die Zustandsänderung zuständig ist, nicht warten muss, bis alle Services die Änderung bestätigt haben.

Ein Nachteil ist natürlich, dass ein Client möglicherweise veraltete Daten sieht. Diese Einschränkung stellt erstaunlicherweise wie die Erfahrung gezeigt hat für viele Anwendungen in der Praxis kein oder nur ein geringes Problem dar.

2.4 Command Query Responsibility Separation

Bis auf Spring Cloud Stream basieren alle hier vorgestellten Frameworks auf dem CQRS Muster.

CQRS ist ein Gegenentwurf zum klassischen Schichtenmodell. CQRS trennt die schreibenden Operationen (Commands) von den lesenden Zugriffen (Queries). Queries liefern Ergebnisse zurück und besitzen keinerlei Seiteneffekte. Commands aktualisieren die Domain Objekte (Aggregate) eines Services, liefern aber keine Ergebnisse zurück.

Diese Trennung erlaubt es, die Daten für lesende Zugriffe in optimierter Form vorzuhalten (Read Model).

Die eigentliche Zustandsänderung erfolgt über Events. CQRS wird oft mit Event Sourcing verwendet, setzt dies jedoch nicht zwingend voraus.

3 Spring Cloud Stream

Spring Cloud Stream von Pivotal ist der Neuling unter den Event Sourcing Frameworks. Das Framework ist nicht auf einen bestimmten Broker festgelegt. Durch die Abstraktion des Brokers kann neben Apache Kafka auch RabbitMQ eingesetzt werden.

3.1 Funktionsweise

Die Kommunikation mit dem Broker erfolgt über sogenannte Channels, wobei zwischen lesenden und schreibenden Channels unterschieden wird. Channels werden über die Annotation @EnableBindings an den Broker angebunden und besitzen keinerlei Kenntnis über die verwendete Middleware. Dies erlaubt es, den Broker zur Laufzeit auszutauschen, oder auch verschiedene Broker über ein gemeinsames Interface anzusprechen.

@EnableBinding({Source.class, Sink.class}) @SpringBootApplication public class Application {

Zum Schreiben von Events können Output Channels als Spring Bean injected werden. Zum Verarbeiten von Events können Methoden mit @StreamListener annotiert werden. Über das target Attribut wird konfiguriert, auf welchem Input Channel die Methode lauscht.

@StreamListener(INPUT) public void on(ArticleCreated postCreated) { articleRepo.put(postCreated.getId(), new Article(postCreated.getId(), postCreated.getName(), postCreated.getPrice())); }

Der StreamListener ist in der Lage, den Content-Type Header einer Nachricht zu interpretieren und automatisch den Payload in das gewünschte Format (meist Java-POJOs) zu konvertieren. Der Content-Type Header kann explizit beim Schreiben gesetzt oder über Properties konfiguriert werden.

output.send( withPayload(articleCreated) .setHeader(CONTENT_TYPE, "application/json") .build());

3.2 Main Features

Neben anderen Funktionen sind das die Main Features von Spring Cloud Stream:

  • Middleware-Unabhängigkeit durch Abstraktion
  • Publish-Subscribe Unterstützung unabhängig von der verwendeten Middleware
  • Consumer Group und Partitioning Support unabhängig von der verwendeten Middleware
  • Automatic Content-Type Handling
  • Unterstützung für Schema Erweiterungen
  • Basiert auf Spring Integration
  • Gute Test-Unterstützung

3.3 Vorteile

  • Middleware-Unabhängigkeit
  • Geeignet für die Umsetzung von Enterprise Integration Patterns dank Spring Integration
  • Nahtlose Integration mit Spring Cloud

3.4 Nachteile

  • Kein Snapshotting
  • Abstraktionen wie Aggregates oder Commands müssen selbst implementiert werden
  • Frühes Entwicklungsstadium, Features wie Fehlerbehandlung sind vorhanden aber kaum dokumentiert

3.5 Konfiguration

Dank Spring Boots Autoconfiguration wird nur ein Minimum an manueller Konfiguration benötigt. Sowohl Framework- als auch Middleware-spezifische Eigenschaften können über Properties definiert werden. Input und Output Channels werden deklarativ über Interfaces definiert.

Die Annotation @EnableBinding sorgt dafür, dass Spring Cloud Stream für jedes Interface und die dort definierten Channels eine Implementierung erzeugt und diese als Bean zur Verfügung stellt.

3.6 Serialisierungsformat

Das Serialisierungsformat wird entweder über Properties (global oder pro Output Channel) oder über den contentType Header gesetzt, wobei der contentType Header Vorrang vor dem Wert in der Property hat.

Wird das Format weder über Properties noch über den contentType gesetzt, so wird als Content-Type JSON angenommen.

Bei Middleware, die nativ keine Header unterstützt, werden ausgehende Nachrichten durch das Framework um fehlende Meta-Informationen ergänzt.

Als Formate werden Text, JSON, serialisierte Java Objekte und Avro unterstützt. Der eigentliche Payload wird stets als Byte Array übertragen wird.

Reichen die vordefinierten Formate nicht aus, können eigene Typen und Konverter definiert werden.

Methoden, die sich mittels @StreamListener Annotation an einem Input Channel registrieren, kümmern sich automatisch um die Deserialisierung, ohne dass der Content-Type angegeben werden muss.

3.7 CQRS

CQRS ist mit Spring Cloud Stream möglich, das Framework bietet im Gegensatz zur Konkurrenz keinerlei Unterstützung für die Implementierung des Designpatterns an.

Dies vereinfacht den Einstieg und macht das Framework auch für Szenarien attraktiv, in denen man Event Sourcing ohne CQRS nutzen möchte.

3.8 Dokumentation

Die Dokumentation ist ausführlich und führt den Entwickler anhand einfacher Codebeispiele an das Framework heran. Konzepte, Verwendung und Konfiguration werden Schritt für Schritt erläutert. Ein GitHub Repository bietet zahlreiche Beispiele für Themen wie Testen oder die Unterstützung von Kafka Streams.

4 Axon Framework

Axon von der Firma AxionIQ ist ein „CQRS Framework für skalierbare, hochperformante Java Anwendungen“ und mit 7 Jahren Entwicklungszeit das älteste der hier vorgestellten Frameworks. Es implementiert eine Architektur basierend auf dem CQRS Muster und stellt für dessen Umsetzung vorgefertigte Abstraktionen zur Verfügung.

4.1 Funktionsweise

Wie beim klassischen CQRS üblich teilt auch Axon die Anwendung in eine lesende und eine schreibende Komponente auf.

Über einen Commandbus werden Kommandos an einen Handler geschickt.

return commandGateway.send(createArticle) .thenApply(id -> …);

Dieser lädt das zugehörige Aggregat (ein Zusammenschluss von Business Objekten) aus dem sogenannten Repository und validiert den Befehl(Command). Nach erfolgreicher Validierung erzeugt der Handler ein Ereignis und sendet dieses an den Eventbus. Das Aggregate definiert einen Eventhandler, welcher das Ereignis entgegennimmt und die eigentliche Zustandsänderung durchführt.

@Aggregate public class ArticleAggregate { @AggregateIdentifier private String id; private String name; private BigDecimal price; @CommandHandler public ArticleAggregate(CreateArticle createArticle) { apply(new ArticleCreated(createArticle.getId(), createArticle.getName(), createArticle.getPrice())); } @EventSourcingHandler public void on(ArticleCreated articleCreated) { id = articleCreated.getId(); name = articleCreated.getName(); price = articleCreated.getPrice(); }

Weitere EventListener können am Eventbus registriert werden. Diese aktualisieren das für Lesezugriffe optimierte Read Model oder benachrichtigen externe Dienste.

@EventHandler public void on(ArticleCreated articleCreated) { Article article = new Article(articleCreated.getId(), articleCreated.getName(), articleCreated.getPrice()); articleRepository.save(article);

Für die Verwaltung des Read Models wird oft Spring Data verwendet, seit Axon 3.1 werden auch eigene Abstraktionen für das Query Handling bereitsgestellt z.B. Query Gateway und Query Bus.

4.2 Konfiguration

Ähnlich wie bei Spring Cloud Stream sorgt Spring Boot’s Autoconfiguration dafür, dass sinnvolle Standardeinstellungen basierend auf dem aktuellen Classpath angenommen werden.

Komponenten können durch die Implementierung von Bean Methoden angepasst werden. Die Konfiguration über Properties ist teilweise möglich, aber nicht im gleichen Umfang wie bei Spring Cloud Stream.

4.3 Serialisierungsformate

Als Serialisierungsformate werden u.a. XML, JSON und serialisierte Java Objekte unterstützt. Eigene Formate können über ein Serializer Interface implementiert werden.

4.4 CQRS

Axon implementiert alle im CQRS Umfeld gängigen Abstraktionen wie Aggregate, Command, Event, und Query. Dies vereinfacht die Umsetzung einer CQRS basierten Architektur.

Für den Einsatz von Event Sourcing ohne CQRS eignet sich das Framework nicht. Hier empfiehlt sich der Einsatz einer schlankeren Alternative wie zum Beispiel Spring Cloud Stream.

4.5 Dokumentation

Die Dokumentation beschreibt detailliert die Architektur des Frameworks, der einzelnen Komponenten sowie deren Konfiguration. Positiv fällt die Beschreibung der Anwendungsszenarien auf, in denen der Einsatz des Frameworks Sinn macht. Ein durchgängiges Tutorial existiert für die Version 2 des Frameworks.

Weiterhin stellt Axon mehrere abgeschlossene Beispielprojekte zur Verfügung, welche gut den Einsatz des Frameworks in der Praxis dokumentieren.

4.6 Main Features

  • Implementierungen der gebräuchlichsten CQRS, DDD und Event Sourcing Abstraktionen (Commands, Queries, Aggregates, Events)
  • Spring Boot Unterstützung durch einen Spring Boot Starter
  • Verteilte Businesstransaktionen durch Sagas
  • Snapshotting

4.7 Vorteile

  • Bestehende Abstraktionen vereinfachen die korrekte Implementierung des CQRS Patterns
  • Spring Boot Starter erlaubt es, schnell ein lauffähiges Projekt aufzusetzen
  • Aufgrund seiner langen Entwicklungszeit besitzt das Axon Framework eine gewisse Reife

4.8 Nachteile

  • Aktuell keine Unterstützung für Apache Kafka oder RabbitMQ
  • Das CQRS Musters stellt für Laien eine große Einstiegshürde dar

5 Eventuate Framework

Ähnlich wie das Axon Framework basiert die Architektur von Eventuate auf dem CQRS Muster und einem ereignisgetriebenen Programmiermodell.

Eine Besonderheit von Eventuate liegt in dem selbst implementierten Eventstore. Events werden hier nicht direkt auf den Eventbus gelegt, sondern zuerst in eine Datenbank geschrieben. Per CDC (Change Data Capture) werden die Änderungen aus dem Transaction Log der Datenbank gelesen und dann auf den Eventbus geschrieben. Vorteil dieser Architektur ist, dass bestehende Anwendungen leichter in eine ereignisgetriebene Architektur überführt werden können.

5.1 Funktionsweise

Services nehmen externe Aufrufe entgegen, mappen diese auf Befehle und senden diese an neue oder existierende Aggregate. Die empfangenen Commands werden vom Aggregat validiert und erzeugen ein oder mehrere Events, welche in den Eventstore abgelegt werden.

EventListener aktualisieren über empfangene Events Aggregate oder auch für den Lesezugriff optimierte Datenmodelle.

5.2 Konfiguration

Die Konfiguration erfolgt typischerweise über Spring. Da aktuell kein Spring Boot Starter für Eventuate existiert, müssen alle Komponenten als Spring Beans manuell erzeugt werden.

Der Konfigurationsaufwand ist etwas höher als bei den vorgestellten Konkurrenzprodukten.

5.3 Serialisierungsformate

Als einziges Serialisierungsformat wird aktuell JSON unterstützt.

5.4 Dokumentation

Die Dokumentation beschreibt ausführlich die Motivation für den Einsatz von Event Sourcing. Es wird auf Konzepte und Architektur des Frameworks eingegangen. Für die einzelnen CQRS Komponenten existieren Codebeispiele, ein durchgängiges Tutorial existiert nicht.

5.5 Main Features

  • Open Source (Eventuate™ Local) und SaaS (Eventuate™ SaaS) Versionen
  • Web Console zur Echtzeitdarstellung von Aggregates und Events
  • Implementierungen der gebräuchlichsten CQRS, DDD und Event Sourcing Abstraktionen (Commands, Queries, Aggregates, Events)

5.6 Vorteile

  • SaaS Version vereinfacht das Deployment
  • Integration mit dem Spring Framework
  • Zahlreiche Beispiele

5.7 Nachteile

  • Eigene Eventstore Implementierung
  • Kein Spring Boot Starter
  • Kein Test Support
  • Kein durchgängiges Tutorial

6 Übersicht

Spring Cloud Stream Axon Eventuate
Lizenz Apache 2.0 Apache 2.0 Apache 2.0 (Open Source Version)
GitHub Stars 260 963 314
Erstes Release 2016 2011 2016 (Open Source Version)
CQRS Unterstützung - x x
Sagas - x x
Hosting CloudFoundry, On-Premise On-Premise, Cloud-Hosting möglich aber nicht explizit unterstützt Amazon AWS, On-Premise
Snapshots - x x
Test Support x x -
Serialisierungsformate Text, JSON, Java-Objekte, Avro XML, JSON, Java-Objekte JSON
Unterstützte Messaging Middleware Apache Kafka, RabbitMQ JDBC, JPA, MongoDB Kafka + MySQL (Open Source Version), proprietärer Eventstore (kommerzielle Version)

7 Fazit

Event Sourcing stellt den Entwickler vor viele Probleme, welche Event Sourcing Frameworks auf unterschiedlichste Weise zu lösen versuchen.

Für einfaches Event-Sourcing stellt Spring Cloud Stream aktuell das Mittel der Wahl dar. Mit wenig Konfiguration ist die Verbindung zum Broker hergestellt und es können Events erzeugt bzw. konsumiert werden. Dank Abstraktion ist man nicht an eine spezielle Middleware gebunden, was ein mögliches Vendor Lock-In verhindert. Die Apache Avro Integration mach Spring Cloud Stream sehr attraktiv, wenn Events mit anderen nicht-JVM basierten Services austauschen möchte, da das Datenschema nicht dupliziert werden muss.

Möchte man Event Sourcing in Verbindung mit CQRS nutzen, so bietet sich das Axon Framework an. Es stellt für CQRS die passenden Abstraktionen zur Verfügung und erleichtert die Umsetzung des Musters erheblich. Reines Event Sourcing ist mit Axon zwar möglich, die auf CQRS basierte Architektur ist eher hinderlich.

Ist Time-To-Market ein wichtiger Faktor, so könnte Eventuate eine interessante Alternative zu Axon darstellen, da Eventuate bereits die nötige Infrastruktur als Software as a Service zur Verfügung stellt. Der Entwickler kann sich dann ganz auf die Entwicklung konzentrieren, ohne erst aufwändig die benötigte Infrastruktur aufzusetzen.