Event sourcing, CQRS, a do tego message brokers

0

Cześć, ostatnio natchnęło mnie na poznanie tych dwóch podejść architektonicznych i jak zauważyłem często idą one ze sobą w parze. No i natknęło mnie na wiele pytań, czyli:

  1. Rozumiem, że przy połączeniu tego mamy dwie bazy: Command (Event Store(?)) oraz Query (gdzie trzymamy jedynie obecny stan), tak? W takim razie jeśli Command nam produkuje event to powoduje jednocześnie zapis do bazy Query. No i tutaj na obecnym stanie w bazie Query leci UPDATE czy INSERT, który historyzuje poprzedni stan, a nowy ustawia jako aktualny? Czy może wydzielić to w ogóle na 3 story (Command, Query, Event?). W dodatku ten nowy stan tworzymy na podstawie eventów czy na podstawie obecnego stanu? Przykład: konto bankowe ma 4500 zł, przychodzi event DepositMoney i teraz bierzemy obecny stan i dodajemy do niego value tego eventu (np. 500 zł?)
  2. Rozumiem, że taki event sourcing pozwala nam na to, że potrafimy odtworzyć każdy stan z przeszłości wskazując na odpowiedni event. W takich domyślnych javowych aplikacjach enterprise używamy do tego jakichś gotowców pokroju Axona lub Spring Application Events? Czy raczej pisze się takie coś samemu? Czy możemy np. do tego użyć samemu Kafki i np. w jakiś sposób persystować nasze eventy Kafkowe (tutaj pytanie jak? Jakiś NoSQL? Redis?).
  3. Czy lepiej wydzielić do tego odpowiednie narzędzia czyli Springowe Eventy/Axon? Jeśli tak to tutaj też jak najlepiej przechowywać te eventy? Natomiast Kafki używać tylko do sytuacji pokroju register -> sendConfirmationEmail gdzie register wysyła message, który mówi żeby wysłać email przez sendConfirmationEmail dając mu jako message ten obiekt? Ogólnie do jakich zadań w biznesowych aplikacjach stosujemy message brokery pokroju Rabbita czy Kafki (ujednolicam tutaj, wiem że różnice między tymi dwoma narzędziami są i nie do końca stosujemy je do tych samych sytuacji).

Pytam, bo technologicznie jestem zacofany w mojej pracy i planuję zmiany, a chcę się zorientować w tym temacie, bo wydaje się dość ciekawy :'(.
Jeśli możecie to podrzućcie przy okazji jakieś ciekawe prelekcje/książki/artykuły w tej tematyce.

1

Command wyraża intencję wykonania jakiejś operacji, a przy wykonywaniu rozgłasza eventy, które mówią o tym, co zaszło. Command może być zależny od obecnego stanu, zewnętrznych systemów itp. i nie ma sensu go zapisywać, przykład:
Command: wymień 100 EUR -> PLN
Event: wymieniono 100 EUR na PLN po kursie 4,42

Eventy zapisujesz w event storze i tak naprawdę to wystarczy, jeśli nie stanowi to problemu wydajnościowego to queries mogą czytać prosto z event stora, a dodatkowe read modele możesz dodawać w ramach potrzeb. Wtedy one nasłuchują eventów i aktualizują się na ich podstawie.

W dodatku ten nowy stan tworzymy na podstawie eventów czy na podstawie obecnego stanu? Przykład: konto bankowe ma 4500 zł, przychodzi event DepositMoney i teraz bierzemy obecny stan i dodajemy do niego value tego eventu (np. 500 zł?)

Po co za każdym razem czytać poprzednie eventy skoro wystarczy zrobić jeden UPDATE?

Co do kolejek, javy itp. się nie wypowiem, bo nie jestem javowcem ani nie robiłem ES na produkcji. Zwrócę tylko uwagę, że event sourcing dotyczy tylko tego, jak wygląda główne źródło prawdy dla aplikacji i nie ma to nic wspólnego z kolejkami, przesyłaniem pomiędzy mikroserwisami itp.

0

Command wyraża intencję wykonania jakiejś operacji, a przy wykonywaniu rozgłasza eventy, które mówią o tym, co zaszło. Command może być zależny od obecnego stanu, zewnętrznych systemów itp. i nie ma sensu go zapisywać, przykład:
Command: wymień 100 EUR -> PLN
Event: wymieniono 100 EUR na PLN po kursie 4,42

Czyli przy komendzie rozgłaszamy eventy, które zapisujemy do event store, a command nie jest w zasadzie w ogóle persystentne?

Eventy zapisujesz w event storze i tak naprawdę to wystarczy, jeśli nie stanowi to problemu wydajnościowego to queries mogą czytać prosto z event stora, a dodatkowe read modele możesz dodawać w ramach potrzeb. Wtedy one nasłuchują eventów i aktualizują się na ich podstawie.

No właśnie, czy takie Query prosto z event store nie jest dość powolne? No, bo tutaj w tym przypadku jeśli nie trzymamy nigdzie stricte stanu naszej encji to musimy zaciągnąć wszystkie eventy i na ich podstawie uzyskiwać obecny stan? Czy może tworzy się do tego wtedy jakieś Cache, które pozwalają nam np. ogarnąć stan naszej encji przy danym evencie w taki sposób żebyśmy nie musieli zaciągać całej historii, a jedynie od danego eventu?

Po co za każdym razem czytać poprzednie eventy skoro wystarczy zrobić jeden UPDATE?

Czyli jeśli mamy taki read model pod Query to tam mutujemy stan przez UPDATE na naszej stronie READ, tak? No tylko teraz to nie robi nam problemu dwóch źródeł prawdy? Co jeśli pójdzie event do event store, a insert do naszego read modelu się nie powiedzie?

Co do kolejek, javy itp. się nie wypowiem, bo nie jestem javowcem ani nie robiłem ES na produkcji. Zwrócę tylko uwagę, że event sourcing dotyczy tylko tego, jak wygląda główne źródło prawdy dla aplikacji i nie ma to nic wspólnego z kolejkami, przesyłaniem pomiędzy mikroserwisami itp.

Tak, wiem, wiem że kolejkowanie/komunikacja między mikroserwisami to jest inna bajka niż ES, jednak wolałem od razu tutaj też o to zapytać :D

1
lavoholic napisał(a):

Czyli przy komendzie rozgłaszamy eventy, które zapisujemy do event store, a command nie jest w zasadzie w ogóle persystentne?

W zasadzie tak.

No właśnie, czy takie Query prosto z event store nie jest dość powolne? No, bo tutaj w tym przypadku jeśli nie trzymamy nigdzie stricte stanu naszej encji to musimy zaciągnąć wszystkie eventy i na ich podstawie uzyskiwać obecny stan? Czy może tworzy się do tego wtedy jakieś Cache, które pozwalają nam np. ogarnąć stan naszej encji przy danym evencie w taki sposób żebyśmy nie musieli zaciągać całej historii, a jedynie od danego eventu?

Jeśli query dotyczy jednej encji, która ma kilkanaście czy nawet kilkadziesiąt eventów to ich odtworzenie może mieć pomijalny narzut. Zwykle jeśli aplikacja jest oparta standardowo na bazie SQL to też często na jedną encję biznesową leci wiele zapytań z joinami które swój koszt mają.

Czyli jeśli mamy taki read model pod Query to tam mutujemy stan przez UPDATE na naszej stronie READ, tak? No tylko teraz to nie robi nam problemu dwóch źródeł prawdy? Co jeśli pójdzie event do event store, a insert do naszego read modelu się nie powiedzie?

Nie znam się, no ale można próbować ponownie, skasować read model i przebudować od nowa itp.

0

Rozumiem, a w zasadzie co powinien taki Event zawierać? Id eventu, Id agregatu, timestamp? No, ale w zasadzie powinien też trzymać dane, co się dokładnie zmieniło no i o ile.
No, bo np. mamy coś takiego:

class User {
 UUID id;
 String login;
 String password;
 String email;

 User changePassword(PasswordChanged passwordChanged) {
     this.password = passwordChanged.getPassword();
     eventPublisher.publish(passwordChanged);
     return this;
 }

User changeEmail(EmailChanged emailChanged) {
     this.email = emailChanged.getEmail();
     eventPublisher.publish(emailChanged);
     return this;
 }
}

No i teraz, jak skladamy coś takiego do EventStore to jak jednoznacznie pokazywać gdy pobieramy to z tego eventstora, że dla danego User'a zmieniło się w tym evencie właśnie to pole i to z taką wartością? W dodatku co sprawdza się najlepiej jako EventStore? NoSQL, SQL, Key Value, czy może jakaś Kafka?

9

Ja zacznę od początku:

W takim razie jeśli Command nam produkuje event to powoduje jednocześnie zapis do bazy Query. No i tutaj na obecnym stanie w bazie Query leci UPDATE czy INSERT, który historyzuje poprzedni stan, a nowy ustawia jako aktualny? Czy może wydzielić to w ogóle na 3 story (Command, Query, Event?). W dodatku ten nowy stan tworzymy na podstawie eventów czy na podstawie obecnego stanu? Przykład: konto bankowe ma 4500 zł, przychodzi event DepositMoney i teraz bierzemy obecny stan i dodajemy do niego value tego eventu (np. 500 zł?)

Jak już wspomniał @mad_penguin, commands to jedynie zasygnalizowanie intencji. Między przetworzeniem command i opublikowaniem eventu należy dokonać walidacji, logikę biznesową itp. Wykonanie command może, a czasem nawet musi się nie powieść. Np. jeśli command to polecenia wybrania z konta sumy pieniędzy której na tym koncie nie ma. Event to z kolei informacja o tym co się stało w systemie, i sam w sobie nie może zostać odrzucony. Przy odtwarzaniu stanu agregatu (jeśli mowa o event sourcing to warto zapoznać się z ideą agregatów) event musi zostać zaakceptowany. Jeśli event wprowadza w błąd stan systemu z punktu widzenia zasad biznesowych, to należy opublikować kolejny event naprawiający ten stan (tzw. compensating event). Co za tym idzie, wszelkie "encje" (a tak naprawdę agregaty, a więc zbiór encji i value objects) powinny być odtwarzane za każdym razem (budowane) za pomocą eventów, ponieważ tylko to załadowanie eventów zapewnia że jest to aktualny stan obiektu. Queries niosą ze sobą eventual consistency, a więc nie ma gwarancji że są aktualne.

Punkty 2 i 3

Tutaj również za bardzo się nie wypowiem co do implementacji technicznej. Ogólnie najważniejsze jest aby każdy event był zawsze zapisywany (transakcyjność).To czy i jak później się go wyśle do kolejek ma drugorzędne znaczenie. Najważniejsze aby każdy event znalazł się w event store- nie ważne czy jest to jakaś dedykowana technologia (np. https://eventstore.com/) czy też własna implementacja. Można np. zastosować tzw. outbox pattern, a więc event zostaje zapisany do event store a następnie jakiś proces w tle emituje ten event do kolejki.

Co do tego kiedy stosuje się brokery- wtedy kiedy eventy (i czasem commands) muszą docierać do modułów poza procesem. Np. kiedy event opublikowany przez agregat A w serwisie A musi zainicjować jakiś proces biznesowy w agregacie B w serwisie B, zakładając że oba serwisy są niezależnymi od siebie procesami i komunikują się sieciowo.

Czyli przy komendzie rozgłaszamy eventy, które zapisujemy do event store, a command nie jest w zasadzie w ogóle persystentne?

Tak

No właśnie, czy takie Query prosto z event store nie jest dość powolne? No, bo tutaj w tym przypadku jeśli nie trzymamy nigdzie stricte stanu naszej encji to musimy zaciągnąć wszystkie eventy i na ich podstawie uzyskiwać obecny stan? Czy może tworzy się do tego wtedy jakieś Cache, które pozwalają nam np. ogarnąć stan naszej encji przy danym evencie w taki sposób żebyśmy nie musieli zaciągać całej historii, a jedynie od danego eventu?

Query to read-model, a więc używane jest np. do zwrócenia danych użytych do wyświetlania po stronie klienta. Query nie powinno być używane po stronie zapisów (commands) ponieważ jak już wspomniałem jest eventually consistent, a więc wykonywanie operacji biznesowych było by z góry narażone na poważne błędy.

Do budowania queries (read models) używa się projekcji, a więc procesów (lub nie procesów, zależnie od technologii) nasłuchujących danych eventów i na podstawie tych eventów budujących read model.Wyobraź sobie że chcesz zbudowań stan konta- do tego będziesz nasłuchiwał każdego eventu związane z kontem. Przy pierwszym evencie (np. AccountOpened/AccountCreated) utworzysz nowy rekord konta w bazie (zakładając że zapisujesz read modele to bazy właśnie) a następnie aktualizował ten rekord przy każdym kolejnym evencie dla tego konkretnego konta.

Jeśli chodzi o ładowanie stanu agregatów z eventów, to oczywiście może być tak że masz tysiące eventów i ładownie ich przy każdej obsłudze command może przynieść problemy wydajnościowe. By temu zapobiec stosuje się snapshots, ale nie należy tego mylić z tym czym są queries. Ponadto przy mniejszej ilości eventów składających się na agregat ładowanie ich za każdym razem nie jest tak naprawdę problemem przy dzisiejszych technologiach. W związku z tym snapshots należy raczej stosować wybiórczo, tam gdzie naprawdę mogą przynieść korzyści.

No i teraz, jak skladamy coś takiego do EventStore to jak jednoznacznie pokazywać gdy pobieramy to z tego eventstora, że dla danego User'a zmieniło się w tym evencie właśnie to pole i to z taką wartością? W dodatku co sprawdza się najlepiej jako EventStore? NoSQL, SQL, Key Value, czy może jakaś Kafka?

Twój model agregatu jest błędny, bo metody które (jak mniemam) mają obsłużyć commands przyjmują eventy. One powinny zamiast tego przyjmować albo parametry albo właśnie obiekt command, np:

User changeEmail(ChangeEmail changeEmail) { //To jest command
     if (...) { // Sprawdzenie jakiś zasad biznesowych
        // Ewentualny błąd jeśli jakieś wymogi nie są spełnione
     }

     this.email = emailChanged.getEmail();
     eventPublisher.publish(new EmailChanged(...));// Opublikowanie eventu- na tym etapie agregat sygnalizuje że wszystko się zgadza i email został zmieniony
     return this;
 }

Poza tym event powinien oczywiście posiadać powiązane IDs jak i wartości, np. w przypadku powyżej nowy email.

Tutaj kilka linków do artykułów, mam nadzieję że pomocnych:
Implementing an Event Sourced Aggregate
DDD – The aggregate
Event Sourcing (ogólnie)
Event Sourcing: Projections

Tutaj jeszcze link do mojej wypowiedzi na forum na temat czym tak naprawdę są agregaty. Temat dotyczył DDD ale sama idea agregatów dotyczy zarówno DDD jak i event sourcingu.

Obecnie pracuję nad własną implementacją event store w oparciu o bazę NoSQL. Może za jakiś czas wrzucę to na GitHub (obecnie mam w prywatnym repo) co by zaprezentować przykładową implementację jak i używanie event sourcingu z agregatami.

0

Fajnie wyjaśnione ale mam jedno pytanie. Mając coś takiego:

User changeEmail(ChangeEmail changeEmail) { //To jest command
     if (...) { // Sprawdzenie jakiś zasad biznesowych
        // Ewentualny błąd jeśli jakieś wymogi nie są spełnione
     }

     this.email = emailChanged.getEmail();
     eventPublisher.publish(new EmailChanged(...));// Opublikowanie eventu- na tym etapie agregat sygnalizuje że wszystko się zgadza i email został zmieniony
     return this;
 }

Rozumiem, że publikujemy event tylko jak wszelka walidacja itp. zostanie zakończona sukcesem? Co odnośnie faili? Wtedy nie ma sensu zapisywania eventu w event storze bo nie zmienił się stan obiektu, tak?

1

@Skoq: Dokładnie, event zapisujemy tylko wtedy gdy wszystko przejdzie pomyślnie. To jakby skutek pomyślnie użytego Command.

@Aventus @mad_penguin wiele mi to nakreśliło, zaraz zabieram się za te linki które podesłałeś i spróbuję sobie jakiś nawet głupi "sklep" w takim podejściu zakodzić.

0

Taki event sourcing czy cqrs to wycinek znanego i lubianego mvcc tyle, że bez cc :-) Ogólnie w trakcie nauki czy też wyboru jaki się dokonujesz warto też spojrzeć na to co właśnie tracisz.

Docelowo eventy jakie wpadają są zapisywane, ich się nie wycofuje, one są źródłem prawdy, co nie? To podejście oddaje programistom większą kontrolę nad tym co robią, ale w praktyce jest to bardzo uciążliwe wymaganie do spełnienia z poziomu samej aplikacji, która wykonuje też biznesową logikę - to tak jakby zadania w obrębie SQL sprowadzać do jednego spójnego zapytania / insertu.

W społeczności clojure znanym przykłademem event sourcingu / cqrs jest baza datomic - ale to jest to baza obsługująca transakcje zgodne z ACID, robiona przez łebskich ludzi, a nie przypadkowych średniaków. Natomiast bez transakcji taki event sourcing czy cqrs gwarantuje rozjazd w spójności danych. Wtedy mam czyste wątpliwości na ile źródło prawdy jest prawdą :-)

0

Mam jeszcze też pytanie co do czystego CQRS bez ES, wtedy to wyglądałoby tak:

User changeEmail(ChangeEmail changeEmail) { //To jest command
     if (...) { // Sprawdzenie jakiś zasad biznesowych
        // Ewentualny błąd jeśli jakieś wymogi nie są spełnione
     }

     this.email = emailChanged.getEmail();
     //bez wypuszczania eventu
     return this;
 }

Następnie takiego User'a rzucamy do jakiegoś CommandHandlera, który zapisze nam go do repo?

1 użytkowników online, w tym zalogowanych: 0, gości: 1, botów: 0