Für ein größeres Kunden-Projekt beschäftige ich mich momentan sehr viel mit dem Konzept des Event Sourcing / der Event-driven Architecture. Dieses Konzept beschreibt einen Ansatz, wie man Änderungen an Daten in einem (meist verteilten1) Computersystem aufzeichnen und verarbeiten kann.

Ich schreibe diesen Artikel primär als Übung und Vertiefung, da man ein Thema erst wirklich dann versteht, wenn man es auch selbst erklären kann.

Referenzen

Direkt am Anfang möchte ich bereits auf die Quellen verlinken, welche eine große Hilfe für das Verständnis der Materie waren, und einen guten Einstieg in die Thematik bieten. Auf diesen basiert auch Großteils dieses Artikels.

Aufbau von Anwendungen

Ich arbeite primär im Bereich der Webanwendungen, in welchem Anwendungen und die darunterliegenden Systeme generell immer nach dem gleichen Muster aufgebaut sind:

  • Es gibt eine Datenbank (meist SQL-basiert, etwa MySQL oder PostgreSQL), mittlerweile aber auch gerne was aus dem NoSQL-Field.
  • Es gibt ein Anwendung, welche direkt auf die Datenbank zugreift und am Ende HTML-Seiten an Clients ausliefert.

Aufbau einfacher Webanwendungen

Dieser Aufbau ist einfach und hat sich über Jahrzehnte bei den meisten Webprojekten gut bewährt. Er ist einfach zu entwickeln, installieren, administrieren, und kann auch mit überschaubarem Aufwand skaliert werden.

Moderne Anwendung haben jedoch mittlerweile weitere Anforderungen, und bringen deshalb zusätzliche Komponenten mit in den Stack:

  • Einen Caching-Dienst wie memcached oder Redis, welche Kopie von Informationen aus der Datenbanken und berechnete Ergebnisse enthalten, um den Datenbank(-Cluster) zu entlasten.

  • Spezialisierte Suchdienste wie ElasticSearch für Volltextsuche für Seiten und Dokumente.

  • Zusätzliche Datenbanken für die Datenanalyse / Big Data, etwa Hadoop.

  • Message Queues zur asyncronen Datenverarbeitung wie RabbitMQ, Celery oder Eigenbau-Lösungen mit ähnlichen Zielen.

Jede dieser Lösungen verwendet meist Daten, welche aus der Haupt-Datenbank abgeleitet und dann in einem unterschiedlichen Format weiterverarbeitet werden. Dies führt auf Dauer zu einem etwas unübersichtlicherem Architekturdiagramm.

Aufbau komplexerer Webanwendungen

(Und hierbei sind noch nicht einmal Replikation und Cluster-Management-Dienste wie etcd berücksichtigt.)

Die Problematik

Sobald Daten an mehreren Stellen verteilt sind, und mehrere Dienste darauf parallel zugreifen können, entsteht die Gefahr von Inkonsistenzen.

Ein Beispiel aus der Praxis: Jemand arbeitet in einem CMS 2 und ändert einen Text an seiner Webseite. Diese Änderung muss nun sowohl in der Haupt-Datenbank aktualisiert werden (auch gerne Single Source of Truth genannt), zusätzlich sollten alle Referenzen im Cache gelöscht bzw. aktualisiert werden, und ElasticSearch braucht auch ein Update für den Index.

Klassisch würde die Zuständigkeit hierfür an die Anwendung fallen, welche bei einem Update der Seite alle drei Aufgaben implementiert (auch Dual-Write genannt). In einer perfekten Welt wäre das kein Problem - aber als Entwickler weiß man, dass nur ein ausgeschalteter Computer keine Fehler produzieren kann.

Ist beispielsweise während der Operation kurzzeitig ElasticSearch nicht erreichbar, wird die Indizierungs-Operation entweder übersprungen - oder die Anwendung stürzt einfach ab. Das Ergebnis ist das gleiche: Der Index passt nicht mehr zur Datenbank, und die daraus resultierenden Suchergebnisse sind falsch. Falls nicht regelmäßig der Index neu aufgebaut wird, bleibt dieser dauerhaft falsch.

Im Prinzip wurden für diese Fälle Transaktionen bzw. Zwei-Phasen-Commit entwickelt, aber ersteres funktioniert nur innerhalb einer (SQL-)Datenbank, und Letzteres ist vielen Entwickler schlicht nicht bekannt, und viele moderne (NoSQL-)Datenbanken implementieren es sowieso nicht.

Ein weiterer Nachteil ist es, dass die Anwendung von allen verbundenen Diensten Bescheid wissen und diese integrieren muss. Dies kann es in manchen Fällen sehr aufwändig machen, zusätzliche Dienste zu integrieren.

Und auch noch am Rande: Für den Fall dass Entwickler oder Administratoren direkt Änderungen an der Haupt-Datenbank vornehmen, geht die Konsistenz des Gesamtsystems sowieso komplett den Bach runter.

Die Folgen

Je nachdem um welchen Dienst es sich handelt, fallen diese Inkonsistenzen manchmal gar nicht direkt auf, oder reparieren sich von selbst: Ein Cache der von selbst abläuft stimmt irgendwann schon wieder, und der Suchindex wird ja bei der nächsten Änderung überschrieben.

Eine Garantie dafür gibt es jedoch nicht. Je nachdem wie oft dies vorkommt, erschafft man sich eine ständige schleichende Korruption seiner sekundären Datenbanken. Dies wieder zu reparieren kann von zeitaufwändig (Neuabgleich aller Daten) bis nahezu unmöglich (manuelle Korrektur tausender Datensätze) sein.

Ein anderer Ansatz

Hier kommt die in der Einleitung erwähnte Event Sourcing bzw. die Event-driven Architecture als Alternative ins Spiel, welche diese Probleme von Grund auf lösen kann.

Die Grundidee ist erstaunlich einfach, erfordert jedoch ein wenig Umdenken bei der Datenhaltung. Am Einfachsten ist es dies direkt am Anfang neuer Projekte einzuführen, eine nachträgliche Migration ist jedoch abhängig vom Projekt durchaus Schritt für Schritt möglich.

Die Idee

Wir drehen bei diesem Ansatz den Datenfluss ein wenig um. Anstatt dass die Anwendung sich komplett um die Verteilung der Daten kümmert, werden zuerst einmal alle Daten in ein fortlaufendes unveränderliches Protokoll geschrieben (nein, keine Blockchain), in welches jede Änderung mit einer Nummer versehen wird.

Dieses Protokoll muss nicht unbedingt global sein, man kann auch beispielsweise ein Protokoll pro Objekt machen (etwa einer Seite im CMS oder einer Bestellung) - dies hängt immer von den jeweiligen Anforderungen ab.

Hierzu ist es jedoch notwendig, den Ansatz wie man seine Daten und insbesondere die Veränderungen dieser betrachet, grundlegend zu ändern. Anstatt dass wir Änderungen direkt in die jeweilige Datenbank schreiben, ist jede Aktion im System ein Ereignis (oder im Englischen event).

Ereignisse sind beispielsweise:

  • Änderung einer Seite
  • Anlegen eines Benutzers
  • Erstellung oder Status-Änderung einer Buchung

Alle verbundenen Dienste welche Informationen zu den Änderungen brauchen (etwa ElasticSearch) holen sich diese aus dem Protokoll ab und pflegen diese in ihren eigenen Datenspeicher ein. Anhand der fortlaufenden Nummer der Einträge kann effizient abgefragt werden, was noch nicht verarbeitet wurde.

Aufbau Event-basierter Anwendungen

Sollte jetzt beispielsweise der Suchindex wegen Wartungsarbeiten mal offline sein, merkt die Anwendung davon nichts (außer bei Suchanfragen), und sobald der Suchindex wieder da ist, kann es alle Änderungen die aufgelaufen sind einspielen.

Dies bedeutet auch, dass die primäre (SQL-)Datenbank auch nur noch ein “Konsument” des Event-Stream ist, und falls notwendig komplett aus diesem neu aufgebaut werden kann.

Wird der Endzustand eines Objekts benötigt, werden alle Events für dieses Objekt aus der Datenbank geladen und im Speicher angewandt. Dies klingt auf den ersten Blick weniger performant, da die Anzahl Events pro Objekt in der Regel gering ist (unter 100), reden wir hier eher von Millisekunden.

Ein weiterer großer Vorteil dieses Konzeptes ist es, dass man ein Auditlog für seine Anwendung praktisch gratis dazu bekommt. Klassisch muss Logging nochmal zusätzlich bei jeder Änderung im System mit implementiert werden, und die Praxis zeigt dass es hier durchaus Löcher geben kann - wenn nicht sauber darauf geachtet wird.

Bei Event Sourcing ist das Protokoll die Basis: Was nicht im Log ist, ist nie geschehen.

Datenspeicher

Zur Speicherung der Events gibt es eine Vielzahl von Möglichkeiten:

  • Eine der bekanntesten Datenbank für dieses Konzept ist Apache Kafka, welches jedoch einen hohen operationalen Aufwand erfordert. Als einfache Alternative für kleine bis mittelgroße Projekte bieten sich die Streams von Redis an.

  • Prinzipiell kann man dies auch mit einer SQL-Datenbank abbilden, insbesondere weil diese (insbesondere PostgreSQL) auch guten JSON-Support anbieten. Hierbei fehlen jedoch meist gewisse Echtzeit-Funktionalitäten zur Verbesserung der Latenz (Push vs Poll).

  • Im Vortrag von Matt Ho beim Gopherfest wurde eine reine Cloud-Lösung basierend auf AWS vorgestellt, welche vor allem eine enorme Skalierbarkeit bietet.

Fazit

In diesem Artikel konnte ich hoffentlich ein bisschen näher bringen, um was es sich bei Event Sourcing eigentlich handelt. In Vorbereitung habe ich bereits eine Artikelserie, welche eine konkrete Implementierung über den ganzen Stack (von Client bis zum Server) beschreibt.


  1. Mit einem verteilten System wird meist von Anwendungen gesprochen, die aus meisten Teilen (eigenständig laufende Instanzen einer Programms) bestehen, die häufig auf getrennten Computern ausgeführt werden. Aufgrund der allgemein höheren Komplexität und Wahrscheinlich für Ausfälle erfordert dies meist andere Konzepte für die Architektur und Implementierung. ↩︎

  2. Content-Management-System ↩︎