2 Punkte von GN⁺ 2023-12-18 | 1 Kommentare | Auf WhatsApp teilen
  • Das relationale CRUD-Modell zeigt die Speicherstruktur gut, überschreibt aber leicht den Geschäftsprozess, sodass es schwer ist nachzuvollziehen, was im System tatsächlich passiert ist
  • Event Sourcing hinterlässt nach jeder Operation unveränderliche Events als Event Stream und liest diese Liste später für Entscheidungen, um den aktuellen Zustand zu bestimmen
  • Die Modellierung beginnt damit, zuerst Events zu finden, und verbindet dann Befehle (command) als Handlungsabsicht der Nutzer mit den Geschäftsregeln, um den Prozess zu verstehen
  • Um Event-Kandidaten in bestehenden relationalen Daten zu finden, betrachtet man Statusspalten, Datumsspalten, Nullable-Eigenschaften und 1:N-Beziehungen; es ist jedoch riskant anzunehmen, dass sich allein aus Statuswerten eine vollständige Historie rekonstruieren lässt
  • Wenn nur noch Daten mit Endzuständen migriert werden, ist es praxisnäher, statt vergangene Events künstlich zu rekonstruieren, mit einem expliziten Import-Event wie Order Imported zu beginnen und dies in einer sicheren Umgebung wiederholt zu validieren

Vom CRUD-Datenmodell zum eventzentrierten Modell

  • Relationale Datenmodelle zeigen, welche Daten gespeichert werden, machen aber nur schwer verständlich, was im System passiert ist und wie Prozesse miteinander interagieren
  • Der bestehende CRUD-Ansatz kann wichtige Geschäftsinformationen verlieren, weil Daten überschrieben werden
  • Event Sourcing priorisiert Informationsqualität statt Speichergröße und speichert die nach jeder Operation entstandenen Fakten als Events

Grundmodell von Event Sourcing

  • Ein Event ist ein Fakt über etwas, das bereits passiert ist, und ist nach dem Speichern unveränderlich
  • Ein Event Stream ist eine geordnete Liste aller Vorgänge, die zu einem Datensatz passiert sind
  • Vergangene Events können nicht geändert werden, aber am Ende kann ein neues Event hinzugefügt werden, um frühere Fehler zu korrigieren
  • Bei Entscheidungen wird die Event-Liste gelesen und geprüft, um den aktuellen Zustand und die nächste Aktion zu bestimmen

Reihenfolge der Prozessmodellierung

  • Die Modellierung beginnt mit der Entdeckung von Events
  • Danach werden Befehle (command) identifiziert, um die beabsichtigte Handlung zu definieren
  • Zum Schluss werden die Geschäftsregeln geordnet
  • Events werden zur zentralen Achse, über die technische und fachliche Beteiligte den Prozess gemeinsam verstehen
  • Mit EventStorming von Alberto Brandolini lassen sich Events, Befehle und Regeln gemeinsam betrachten, um den Prozess zu erfassen

Event-Kandidaten in bestehenden relationalen Daten finden

  • 1. Statusspalten untersuchen

    • Die Werte einer status-Spalte können Phasen des Datenlebenszyklus widerspiegeln
    • Wenn eine Bestellung Zustände wie initiated, shipped oder paid hat, können diese jeweils Kandidaten für die Events Order Initiated, Order Shipped und Order Paid sein
    • Statuswerte können jedoch eine abgeflachte Interpretation des Geschäftsprozesses sein; man darf sie nicht als vollständig annehmen
    • Event-Namen sollten nicht nach CRUD-Aktionen wie Order Created, Order Updated oder Order Deleted vergeben werden
    • State Obsession wird als zu vermeidender Ansatz genannt
  • 2. Datumsspalten prüfen

    • Datumsspalten können wichtige Zeitpunkte des Geschehens im Prozesslebenszyklus sichtbar machen
    • CreatedDate und ModifiedDate liefern wenig Information, aber ShipmentDate, DeliveryDate und OrderPlacementDate sind bessere Hinweise
    • Beispiele:
      • ShipmentDate kann ein Hinweis darauf sein, das Event Order Shipped einzuführen
      • OrderPlacementDate deutet an, dass Order Placed ein besserer Name als Order Initiated sein könnte
      • DeliveryDate zeigt, dass möglicherweise ein Event Order Delivered benötigt wird
    • Diese Hinweise sollten mit Domänenexpert:innen abgeglichen werden, damit sie zum tatsächlichen Geschäftsprozess passen
  • 3. Nullable-Eigenschaften von Spalten analysieren

    • Non-nullable-Spalten enthalten Daten, die immer vorhanden sein müssen
    • Nullable-Spalten können Werte enthalten, die später durch andere Operationen geliefert werden oder optional sind
    • Wenn in einem Ordering Process bestimmte Spalten Pflichtfelder sind, sollten diese Daten auch im ersten Event Order Initiated enthalten sein
    • Ein einzelner Event-Typ ist nicht zwingend immer der Startpunkt eines Streams; es kann mehrere Start-Events geben
  • 4. Tabellen mit vielen 1:N-Beziehungen finden

    • Um Stream-Grenzen zu finden, kann man zuerst Tabellen mit vielen 1:N-Beziehungen betrachten
    • Tabellen mit vielen „one“-Beziehungen sind Kandidaten für einen Stream-Typ
    • Außerdem sollte logisch beurteilt werden, ob Daten unabhängig voneinander existieren können
      • shipment kann ein separater Prozess neben order sein
      • order line kann nur schwer ohne order existieren
    • Bei der Diskussion über Grenzen lassen sich oft weitere Events entdecken und das Prozessverständnis vertiefen

Bei der Migration keine falschen Events erzeugen

  • Relationale Daten sind ein abgeflachter Endzustand; wenn man allein daraus versucht, die tatsächlichen detaillierten historischen Events zurückzurechnen, scheitert das oft oder bleibt ungenau
  • Statt kleinteilige historische Events zu erzwingen, ist es besser, explizit ein Event wie Order Imported bereitzustellen, das den vollständigen aktuellen Zustand und den Interpretationscode enthält
  • Ein Import-Event zeigt klar, auf welche Weise die Daten übernommen wurden, und kann für Fehlersuche und Diagnose wichtig sein

Mit einem Prototyp validieren

  • Eine Migration sollte in einer sicheren Umgebung als Prototyp ausprobiert werden, um zu prüfen, wie sich das Modell tatsächlich verhält
  • Die Ergebnisse sollten mit den Erwartungen verglichen und iterativ angepasst werden
  • Es braucht einen Ansatz, der nicht übereilt vorgeht, keine bestehenden Informationen verliert und das Modell auf Basis dieser Informationen später verbessert
  • Die allgemeine Strategie zur Migration relationaler Daten in dokumentbasierte Modelle wird auch in General strategy for migrating relational data to document-based behandelt

1 Kommentare

 
GN⁺ 2023-12-18
Meinungen auf Hacker News
  • 2c: Wenn ihr PostgreSQL auch in anderen Teilen der App braucht, ist es sinnvoll, die Event-Daten ebenfalls in PostgreSQL + FOSS-Reporting-Tools (Apache Superset, Metabase usw.) zu speichern und damit grob bis 2 TB durchzuhalten.
    Danach könnt ihr entscheiden, ob die gesamten 2 TB online bleiben müssen oder ob Zusammenfassungen auf Tages-/Stundenbasis reichen. Wenn Letzteres der Fall ist, reicht es völlig, weiter bei PostgreSQL zu bleiben[1].
    Ein Kunde verarbeitet mehr als 10 TB, 1.500 Events pro Sekunde und 600 Byte pro Datensatz (80 GB pro Tag vor der Indizierung), hält nur Detaildaten für 2 Tage online, fasst den Rest zusammen und verschiebt die Details nach S3, wo sie weiterhin per Athena SQL abgefragt werden[2].
    Die Gesamtkosten einschließlich Reporting-Portal für Kunden liegen unter 2.000 Dollar, und auf AWS RDS mit Multi-AZ-Auto-Failover (db.m7g.2xlarge) werden sowohl Inserts als auch Reporting-Queries verarbeitet; die Last liegt unter 2 %. Weil das Business-Team Diagramme und Graphen selbst erstellt, verbringt ein Engineer weniger als 5 Stunden pro Monat mit Wartung.
    Bei proprietären Tools sind ein paar Diagramme zwar „out of the box“ dabei, aber mit pgsql liegen die Daten an einem Ort, es gibt nur ein System zu lernen, nur ein System online zu halten/zu replizieren/zu sichern/wiederherzustellen, nur ein System abzusichern/zu skalieren, nur einen Vendor zu verwalten, und es gibt Millionen Engineers, die dieses System kennen.
    In Systemen wie Preset oder Metabase dauert es eine Stunde, 12 Diagramme zu erstellen, und auch nichttechnische Mitarbeitende können das tun.
    Zur Einordnung: Ich bin zwar voreingenommen, habe aber über mehr als 20 Jahre Datenbanken und Reporting-Systeme kommen und gehen sehen, und das gute alte PostgreSQL wird jedes Jahr besser.
    https://instances.vantage.sh/aws/rds/db.m7g.2xlarge?region=u...
    [1] Wenn es wirklich nötig ist, gibt es auch PostgreSQL-kompatible Systeme für zusätzliche Skalierung. Aurora kann etwa 3–5× skalieren, TimescaleDB etwa 10×, CitusDB etwa 10×+. Das geht jeweils mit dem Preis einher, etwas weniger standardkonform zu werden; daher würde ich es nicht empfehlen, bevor es tatsächlich nötig ist.
    [2] Das Kunden-Reporting-Dashboard braucht Antwortzeiten unter 1 Sekunde; PostgreSQL liefert das durch Abfragen indizierter Zusammenfassungstabellen. Athena antwortet durch parallele Scans in etwa 1–2 Sekunden.

    • Wenn man in diese Richtung geht, kann in Fällen, in denen „Zeitreisen“, „Wiederherstellung überschriebenen Zustands“ oder „Neuinterpretation vergangener Events“ benötigt werden, auch ein Audit-Log allein ausreichen.
      Man behält Snapshots der Daten vor dem Speichern, hat Skripte zum Identifizieren und Sammeln einer bestimmten Event-Reihenfolge und lässt dann einen Menschen prüfen, um die Auswirkungen neuer Logik massenhaft rückwirkend anzuwenden.
      Tools wie https://django-simple-history.readthedocs.io/en/latest/ sind eine halbwegs zuverlässige und einfache Lösung zum Erstellen von Audit-Tabellen; wenn man sogar direkten Datenbankzugriff auditieren muss, kann man zusätzlich Postgres-Trigger verwenden.
      In der Theorie mag ich Event Sourcing, aber in der Praxis bringt es zu viel Boilerplate mit sich, um neue CRUD-Flows hinzuzufügen oder Eingriffe und Hotfixes, wie sie Early- bis Mid-Stage-Startups in unerwarteten Situationen häufig brauchen, schnell und zuverlässig auszurollen.
      Wenn man nicht gerade etwas wie Payment-Processing-Rails implementiert, ist Event Sourcing möglicherweise nicht die richtige Wahl.
      Auch unter https://news.ycombinator.com/item?id=17817375 (2018) gibt es eine gute Diskussion über die Nachteile von Event Sourcing.
    • Dieser Kommentar ist wertvoller als der Artikel selbst.
      Das einzige Problem bei PostgreSQL ist, dass es auf der Insert-Seite interessante Skalierungsprobleme gibt. Üblicherweise empfiehlt man, eine Queue zwischen Event-Quelle und DB zu setzen.
    • Ich frage mich, ob man dafür im Großen und Ganzen eine Tabelle mit einer Definition wie {id:uuid,created_at:timestamptz,data:jsonb} hat.
      Besonders wenn die Event-Strukturen vielfältig sind und sich Event-Definitionen weiterentwickeln, ist es schwierig, die Funktionen von JSONB-Indizes richtig auszunutzen.
      Vermutlich muss ich mich in diese Doku noch besser einarbeiten: https://www.postgresql.org/docs/current/datatype-json.html#J...
    • Ich frage mich, wo man einen ausführlichen Artikel darüber finden kann, wie man ein solches System aufsetzt.
  • Früher hat unser Team einmal Event Sourcing ernsthaft in Betracht gezogen, aber für mich wirkte es wie eine Lösung auf der Suche nach einem Problem.
    Es hätte bei uns vielleicht funktionieren können, aber die Vorteile waren nicht sofort klar, und die Risiken und Lernschleifen, die wir uns mit der Einführung eines neuen Ansatzes eingehandelt hätten, schienen weder für das Projekt noch für das Unternehmen das Beste zu sein. Also haben wir es letztlich verworfen.
    Vielleicht war das eine Entscheidung, bei der uns ein Werkzeug und eine Lernchance entgangen sind, aber ich bereue nicht, nicht in dieses Rabbit Hole gegangen zu sein, zumal uns kein Fuchs im Nacken saß.

    • Ein gut funktionierendes, langweiliges und traditionelles System ist eine Bedrohung für ein aufgeblähtes Engineering-Team, das nichts zu tun hat und seinen Lebenslauf aufpolieren will.
      Genau das ist das „Problem“, das diese Lösung löst.
    • Zum Beispiel ergibt eine temporale Datenbank für Finanzdaten durchaus Sinn.
      In den meisten Fällen reicht es aber, eine normale Datenbank zu verwenden und frühere Änderungsverläufe in Hilfstabellen zu speichern. Dann verhält sich die Hauptdatenbank wie eine Art materialisierte View.
    • Fast alle Daten, die man in SQL speichert, würden eigentlich besser in eine Dokumentendatenbank passen, aber niemand ist damit vertraut, also macht man einfach weiter wie bisher.
      Ich habe keine großen Beschwerden und würde auch nicht sagen, dass es die falsche Wahl war, aber bei der Art, wie Änderungen am Datenmodell gehandhabt werden, entstehen Probleme.
      Es wirkt so, als hätten die meisten Arten der Datenspeicherung nicht mit der heutigen Art, Software zu bauen, Schritt gehalten; Dinge wie Events und Queues sind das Ergebnis davon, benötigte Funktionen auf bestehende Systeme aufzusetzen.
      Viele Datenbeziehungen finden heute zwischen mehreren Services statt, also außerhalb der Datenbank. So sieht die moderne IT-Landschaft in vielen Organisationen nun einmal aus.
      Es gibt interne Stammdaten, die mehrere Business-Teams unterstützen, und sie interagieren mit über 300 IT-Systemen und Anwendungen, um Abläufe zu vereinfachen.
      Mit Microservices ist es einfacher, Business-Logik und Datenmodelle sauber zu halten, aber dafür muss man Events, Queues, Datenzustände und abhängige Stores verwalten, und das ist inzwischen viel zu komplex.
      Ich mag SQL, aber ehrlich gesagt würden die Systeme, die wir heute bauen, vermutlich fast vollständig in SQLite passen.
  • Was in solchen Diskussionen fehlt, ist die Frage, wann eine eventgetriebene Architektur angemessen ist.
    Kurz gesagt: Wenn ein Kunde etwas getan hat und eine Antwort erwartet, ist das nicht eventgetrieben, sondern einfach Request/Response.
    Eventgetrieben ist es, wenn etwas außerhalb des normalen Bandes passiert. Zum Beispiel, wenn man Code zu GitHub pusht und dadurch ein Build ausgelöst wird.
    In diesem Beispiel ist das Neuladen der Seite, um den aktualisierten Code zu sehen, Request/Response, während der in die Queue gestellte CI-Build eventgetrieben ist.
    Hoffe, das hilft.

    • So einfach ist es nicht. Ob etwas Request-Response ist, ist kein Kriterium dafür, ob man Event Sourcing oder eine eventgetriebene Architektur wählt.
      Auch mit Event Sourcing oder eventgetriebenen Ansätzen kann man Request-Response-, Inline-, blockierende und zyklische Abläufe bauen.
      Umgekehrt kann man auch ohne Event Sourcing oder eventgetriebene Architektur mit Workern, Queues, Actors, Multithreading und ähnlichen Ansätzen gute Asynchronität umsetzen.
  • Domain Events zu modellieren ist nützlich, um mit Fachexperten das zu lösende Problem zu beschreiben, und es kann sinnvoll sein, sie bei der Planung der Lösung in der Dokumentation festzuhalten.
    Wenn man tatsächlich ein System implementieren will, das einen Audit Trail für eine langlebige Zustandsmaschine bereitstellt, ist es wahrscheinlich besser, etwas wie Temporal.io oder Durable Functions zu verwenden.
    Solche Tools nutzen intern Event Sourcing für die Persistenz und bieten ein Programmiermodell, das Code zur Orchestrierung von Funktionen (Workflows) und Code zur Interaktion mit der realen Welt (Activities) mit unterschiedlichen Einschränkungen versieht und einen dazu zwingt, über Deduplizierung und Idempotenz nachzudenken.

    • Durable Functions haben das Problem, dass es ihnen an Observability mangelt.
      Ich würde gern Vorschläge hören, wie man dieses Problem überwindet.
  • Das Konzept ist interessant, aber der Artikel erklärt nicht gut, wie es funktioniert
    Mich würde interessieren, wie man den aktuellen Zustand aus einem Event-Stream effizient rekonstruiert und wie man einen Event-Stream in einer Datenbank modelliert

    • Es gibt mehrere Vorträge des Autors
      https://www.youtube.com/watch?v=gG6DGmYKk4I
      https://www.youtube.com/watch?v=jnDchr5eabI
      https://www.youtube.com/watch?v=ArcypYS5XBQ
      https://www.youtube.com/watch?v=uODSwR2CIV4
      Außerdem pflegt er Beispiele auf GitHub
      https://github.com/oskardudycz/EventSourcing.NetCore
      https://github.com/oskardudycz/EventSourcing.NodeJS
      https://github.com/oskardudycz/EventSourcing.JVM
    • Es gibt zwei Wege
      Erstens: Man verwendet eine Datenbank, die für diesen Zweck entworfen wurde. Beispiele sind Google BigQuery, Amazon Redshift, ClickHouse usw.
      Alle aktuellen Daten sind im Grunde eine Art Aggregation. Anders gesagt: wie eine group-by-Abfrage auf einer Event-Datenbank
      Wenn Events vorhanden sind, kann man den aktuellen oder einen früheren Zustand technisch per Aggregationsabfrage wiederherstellen; das ergibt also Sinn
      Zweitens: Man benennt den relationalen Speicher um und nennt ihn eine Cache-Schicht neben dem Event-System
      Funktional ist es dasselbe, löst aber bei Leuten, die sich darauf versteifen, alles eventgetrieben machen zu müssen, keine Warnlampen aus
      Die im Artikel beschriebene Architektur gibt es tatsächlich. Sie ist allerdings extrem komplex, und Dienste, die sie nutzen, machen normalerweise sehr gezielte Dinge. Man denke an Google Analytics, Datadog oder Splunk
    • Es gibt nicht den einen „aktuellen Zustand“. Diese Denkweise entsteht daraus, alles in einer einzigen DB zu zentralisieren
      Man kann je nach unterschiedlichen Anforderungen unterschiedliche Zustände in unterschiedlichen Systemen erzeugen
      Wenn man ein Einkaufssystem baut und Käufe sowie Kunden hat, kann ein Dienst die Events lesen und relationale Tabellen für Finanzzwecke erstellen
      Ein anderer Dienst kann die Events lesen und einen Key-Value-Store für Kundendaten aufbauen, und ein dritter Dienst kann einen OpenSearch-Dienst für die Produktsuche betreiben
      Ein Event-Stream ist eine Liste. Wenn man etwas Zweckmäßiges wie Kafka verwendet, werden daraus mehrere Listen, also Topics, Partitionen usw.
    • Sinnvoller ist es für bestimmte Streams, in denen sich viel ändert und deren Daten interessant genug sind, um zu sehen, was unterwegs passiert ist
      Aber auch das lässt sich innerhalb eines relationalen Modells lösen
  • Das ist der Unterschied zwischen Top-down vs. Bottom-up, oder zwischen maßgeschneidert und allgemein einsetzbar
    Top-down bedeutet, von der Business-Domain auszugehen und die Implementierung auf verfügbare Technologien, Tools und Anbieter abzubilden
    Bottom-up bedeutet, von verfügbaren Technologien, Tools und Anbietern auszugehen und daraus eine funktionierende Lösung zusammenzusetzen
    Zum Maßgeschneiderten gehören DDD, CQRS/ES, Sagas, TBUI (task-basierte/-getriebene UI), GraphQL, algebraische Datentypen usw.
    Zum Allgemeinen gehören RDBMS, CRUD, REST, ACID-Transaktionen, CDC, generische Admin-UIs, No-Code/Low-Code, eingeschränkte/allgemeine Typen usw.

  • Ich werde einfach weiter die gute alte relationale Datenhaltung verwenden

    • Gut so; mach das, bis es nicht mehr geht. Es geht darum, keinen Hammer für Schrauben zu benutzen
  • Ich stimme eventbasierten Architekturen zu, aber dieser Artikel scheint Mühe zu haben, den Punkt zu vermitteln
    Ich würde mich auf den Unterschied zwischen Datenbeziehungen und geschäftlichen Handlungen konzentrieren
    Sobald man beginnt, in Handlungen und Geschäftsaktivitäten zu denken, wird die Bewegung weg von operativen relationalen Datenspeichern viel deutlicher

    • Auf abstrakter Ebene lassen sich auch Events als Relationen modellieren
  • Event Sourcing hat viele gute Eigenschaften und ist deshalb interessant
    Aber braucht man nicht trotzdem Beziehungen? Wenn ja, wie werden diese Beziehungen implementiert?
    Wenn die Antwort lautet: „Sie stecken alle implizit im Code der Anwendungsschicht“, fällt es mir schwer, das zu akzeptieren
    Man braucht trotzdem die Möglichkeit, Beziehungen abzufragen, relationale Views aktuell zu halten oder etwas Ähnliches
    Es ist in Ordnung, wenn Beziehungen nicht der Kern des Persistenzmodells sind, aber irgendwo in der Datenschicht müssen sie implementiert sein; dazu sehe ich hier keine Erwähnung
    Firestore hat dasselbe Problem. Alle gehen irgendwie mit Beziehungen um, aber am Ende wird daraus nicht skalierbarer Spaghetti-Anwendungscode

    • In einem Event-Sourcing-System projiziert man den Event-Stream in Read Models, und davon kann es mehrere geben: relationale, Zeitreihenmodelle usw.
      Wenn man mit funktionaler Programmierung vertraut ist, ist es im Kern dasselbe wie eine fold-Operation, die einen Event-Stream zu einem Zustand zusammenfaltet
      Aus meiner früheren Arbeit mit Event-Sourcing-Systemen: Es hat den Vorteil einer explizit gespeicherten Event-Historie, erhöht aber auch die Komplexität erheblich
      Es entstehen Fragen wie: Wie erzeugt man Read Models konkret, wie verwaltet man Modellversionen, legt man Snapshots von Read Models an usw.
      Meiner Erfahrung nach war die zusätzliche Komplexität in den meisten Kontexten, in denen dieses Pattern eingesetzt wurde, den Aufwand nicht wert
  • Was man braucht, ist eine Command Queue. Command Events sind keine Domain Events