Transkrypcja
Wprowadzenie i temat odcinka
Cześć Michał.
Cześć Wojtek. Witamy w kolejnym odcinku, który tym razem nosi nazwę… „Ach, gdybyż tak cofnąć tamte zdarzenia!” Cóż za poetycka nazwa!
Cóż za poetycka nazwa! Czyli kiedyś to było. Kiedyś to było. Kiedyś to były czasy. Kiedyś to były czasy, ale już tego nie ma, czasu cofnąć. Gdyby tak udało nam się te czasy cofnąć.
Wprowadzenie do Event Sourcingu
I słyszałem, że jesteś… plotka mówi, że jesteś wielkim fanem cofania czasu, czyli Event Sourcingu.
Czy to jest akurat cofanie czasu? Tu i można by tutaj różne podpiąć wyjaśnienia pod to i różne pewnie definicje. Ale gdybyś tak może nie cofać tamtego zdarzenia, tylko po prostu je skompensować, to może tak by się dało. Wtedy nie trzeba żadnego koła tam toru przepływu, czy jak to tam się tam nazywało. Ale DeLorean by się przydał. Nie pogardziłby człowiek.
No dobra, no to jeśli nie musimy tego cofać, to co właściwie możemy zrobić? Iwan, co to właściwie jest? Co my tam nie możemy zrobić?
Zacznijmy od tego, co może robią, co możemy, a co nie możemy. To będzie w drugiej części.
Dobra, czyli zaczynamy od takiego standardowego: ale o co chodzi? Co to właściwie jest? Co to jest zdarzenie? Co to jest Event Source i z czym to się je?
Definicja zdarzenia i jego cechy
Zdarzenie. To by było coś po prostu takiego, co się zdarzyło. I ono ma taką fajną cechę, że jest w przeszłości, czyli jak się zdarzyło, to się dokonało i koniec już tego. Ma to sens i nie można tego już cofnąć. Dlatego właśnie tam ciężko jest z tym cofaniem i tam, wiadomo, Martin Apple robił co mógł, a i tak mógł tylko skompensować.
No tak, to jest chyba dobra ilustracja, w ogóle cała ta trylogia, co tam można zrobić z tymi zdarzeniami w przyszłości. I tak samo my możemy z naszymi zdarzeniami robić może podobnie, bo może aż tak wybitnego sprzętu nie musimy do tego kombinować. Ale fajnie by było, jakbyśmy np. te zdarzenia, które gdzieś tam się zdarzyły, zapisać sobie np. w bazie. Można je zapisywać gdziekolwiek, ale w bazie byłoby fajnie.
Więc takie zdarzenie to zazwyczaj rozumiemy jako szerszy termin, jako zdarzenie biznesowe. Bo to, że tam wczoraj padał deszcz, to jest jakieś zdarzenie, które niekoniecznie chcielibyśmy gdzieś uwiecznić. No chyba, że robimy system kontrolujący opady deszczu i wtedy wiadomo, wtedy jest to nasza część domeny. I tutaj, jeżeli chcielibyśmy jakiekolwiek zdarzenia biznesowe z naszej domeny, nad którą pracujemy, sobie wyryć w kamieniu, to Event Source wydaje się być ciekawą propozycją do tego. Bo jeżeli coś się zdarzyło w naszej domenie, to po prostu fajnie by było mieć informacje o tym, co to było, kiedy to było. I właściwie tyle, bo cała reszta to… zresztą chyba w toku rozmowy dojdziemy do tego. Cała reszta, co można jeszcze z tym zrobić, to się pojawi, pokaże się, wyjaśni się może za moment w przyszłości.
Ale to też nie tylko się wyjaśni, co więcej, pokaże nam potencjał, jaki otwiera w ogóle to, że zbieramy zdarzenia. Bo jeżeli dojrzeliśmy do tego, żeby nie zapisywać stanu na moment wystąpienia tego stanu u nas w jakimś tam naszym storage’u, tylko zapisywać zdarzenia biznesowe właśnie w takiej formie, że coś się zdarzyło. I nieważne co to się zdarzyło, tak naprawdę na moment zapisywania. Ten storage może być na tyle generyczny, że otrzymuje każdy rodzaj zdarzenia. Musimy tylko w schemacie zdarzenia zawrzeć informacje o jego typie i czasie. No i wiadomo, jakieś tam metadane, takie jakie nam są akurat potrzebne. I dobrze by było zawrzeć, ale na pewno musi być czas wystąpienia tego zdarzenia, no i typ tego zdarzenia oraz jego treść. No i mniej więcej po to by w tym chodziło, mniej więcej to by była już taka… to by był taki szybki wstęp do tego, czym to zdarzenie może być.
Po co nam Event Sourcing? Modelowanie zdarzeń.
No i teraz, na co to komu?
Na co to komu? No właśnie. No bo OK, zapisujemy sobie te zdarzenia, mamy tam całą masę. No i co dalej? Podejrzewam też, że te zdarzenia wypadałoby jakoś zamodelować sensowniej niż… niż… z większym sensem, niż zwykle ładujemy rzeczy do bazy, tak troszkę, troszkę na wariata. No bo jednak te zdarzenia powinny chyba reprezentować troszkę biznesowo bardziej podejście niż taki techniczny tylko i wyłącznie zapis. Tu też pewnie wchodzi kwestia modelowania i odpowiedniego jakby payloadu do tego zdarzenia. To jest jedna, jedna rzecz. No i i OK, mówisz o jakiś fajnych cechach, które z tego podejścia wynikają, więc jakby co, to co to nam to daje?
Może zacznijmy od tego modelowania.
Modelowanie zdarzeń i ewolucja schematów
Tak, można by zacząć od modelowania. I tutaj, tak jak w poprzednim odcinku mieliśmy nasze postacie, które tam miały jakieś tam swoje charaktery, od najbardziej tych światłych do takich najbardziej mrocznych, to może tutaj też moglibyśmy sobie takie postacie zamodelować. I taka postawa, którą wszyscy znamy, lubimy i pewnie niejeden raz ją wybieraliśmy, mogłaby nosić miano czcigodnego rzeźbiarza schematu. Tak w ogóle na razie w oderwaniu od czegokolwiek związanego ze zdarzeniem, bo był sobie już dawien dawna ten nasz rzeźbiarz, dostawał zlecenia: „Proszę mi tu wyrzeźbić taki schemat. On ma tutaj być optymalny. Mam zapisać sobie to i tamto. Mam mieć tu jakieś, odpukać, relacje albo cokolwiek inne. Normalizacja, czyli inne takie tam…” Nawet nie będę tutaj się wyrażał, bo nie wypada.
Więc nasz rzeźbiarz brał swoje dłuto, siadał do kawałka marmuru i tyle weń krzesał, aż schemat powstał. I ten schemat później dostarczał do jakiś tam kolejnych domyślnych wypełniaczy tego schematu. Oni tam sobie dzielnie wpisywali rzeczy, no i w pewnym momencie okazywało się, że to rozwiązanie idealne, bo mamy cudowny schemat i możemy każdy snapshot swoich danych wpisać. Tylko w pewnym momencie przychodził i mówił: „A gdybyśmy tak tutaj dodali takiego loga np.?” Wszyscy rozdziawiali twarze i pytali: „A cóż to takiego jest ten audyt log, bo ja bym chciał, żeby to mi się zalogowało, że w tym momencie się zdarzyło to, a chwilę potem zdarzyło się tamto i jeszcze takie. I tak w ogóle to mam 25 typów takich różnych dziwactw do zalogowania”. No i co? No to czcigodni myśliciele zasiadali, pewnie w kręgu jeszcze wtedy, i wymyślali, że może byśmy tutaj do naszego schematu dorobili kawałek takiego czegoś, gdzie będziemy wrzucać ciurkiem to wszystko, co się zdarza jak leci, nadamy temu kolejne linijki, jakieś tam timestampy i jakoś to będzie. No i takie coś trafiało do naszego rzeźbiarza schematu. Powstawała taka tabelka cudująca.
No i wszystko byłoby pięknie, jakby taka tabelka była jedna w systemie, ale product ownerzy są zachłanni. Dasz audyt loga, następnego dnia pojawiały się pewnie takie requesty co moment.
No tak, no i się okazało, że w którymś momencie to takie staromodne rzeźbienie schematu już nie wystarczało. Trzeba było coś z tym zrobić i wpadnięto na całkiem inne pomysły, że może by jednak w ogóle odejść od tego sztywnego schematu. A co by było, jakby nie było schematu? Toż to zgroza! Ale albo gdyby go robić na bieżąco, na bieżąco odtwarzać i w ogóle takie cuda? To tak się chyba nie da.
Chyba się nie da. Bo co? Gdzie my to zapiszemy? Nie będziemy mieli tabelki na to wszystko nowe. Ale się okazuje, że jakby tak upchać te wszystkie zdarzenia biznesowe do jednego wora, to co by to dało? Co by to było? Mielibyśmy wtedy potencjalnie jedną tabelkę w takim schemacie. No to tutaj proszę wybaczyć, że przy tej powieści jeszcze jestem, ale jak już wychodzimy z tego klasycznego schematu, to może… może jeszcze mamy tą jakąś starą bazę, która ma jakieś tabelki. I tutaj dopiero raczkujemy w temacie zdarzeń, nowości i jeszcze nie mamy tutaj żadnego piewcy tejże zdarzeniowości odkrytego, wykształconego, więc dopiero raczkujemy. My tworzymy sobie taki wycinek schematu, który by nam mógł zamodelować te zdarzenia w ogólnej postaci.
Się okazuje, że da się to zrobić nawet na takiej zwykłej tabelce w zwykłej bazie danych gdzieś tam z zamierzchłej przeszłości. Wystarczy, że wsadzimy do tej tabelki pole na wszystko, czyli na payload, damy do tego jakieś metadane, np. timestamp, kiedy się to nasze zdarzenie wydarzyło, kiedy zaistniało w ogóle, tudzież tak bardziej już technicznie mówiąc, to właściwie kiedy zostało zapisane. Albo też można to podciągnąć pod moment wytworzenia tego zdarzenia, czyli tego obiektu, który zapisujemy. I jeszcze pewnie jakiś tam ID porządkowy, jakiś może GUID, UID, cokolwiek byśmy tam chcieli dać. Numer sekwencji, być może tudzież jakiejś wersji, ale to wszystko to może wyjść z późniejszych naszych prób. Grunt, że mamy jakiś tam zestaw metadanych i zestaw danych właściwych, zwany payloadem, i możemy to sobie zapisać w tejże właśnie tabelce. W teorii.
Ale taka jedna tabelka wystarczy nam do takiego prostego POC-a, żeby już ta nowość mogła zaistnieć. I nagle się okazuje, że w tej sytuacji możemy więcej w tym naszym event storze, bo to już do tego zmierza i nazwiemy go szumnie. Zresztą to na razie tylko znamiona, jeśli tak, to oczywiście w żadnym wypadku nie jest to jeszcze event store w pełni, a jest to już taki fundament pod event store. Więc możemy tam sobie w niego wrzucać różne eventy. To jest właśnie to słowo, które musiało tutaj doczekać do tego trzeciego odcinka, ale już dokonało się. Więc te eventy możemy tam wrzucać i możemy je nawet odczytywać, jak nam się zapisały. To jest spoko. I jeżeli nawet korzystamy z tej starej relacyjnej bazy danych – i relacyjnej, transakcyjnej, bardziej potem relacyjnej, to to co odkładamy na bok – ale te relacyjne bazy danych są jeszcze transakcyjne najczęściej. Więc jeżeli już mamy to transakcyjnie zapisane, to możemy mieć pewność, że to tworzy jakąś sekwencję w czasie tych wydarzeń, dzięki czemu możemy zamodelować kawałek życia naszej domeny właśnie w ten sposób. Czyli od takiego prostego, wyrzeźbionego schematu, gotowego na przyjęcie każdego najwybitniejszego z przebadanych, doszliśmy do takiego ogólnego worka na cokolwiek. No i teraz ja się pytam: na co to komu?
Korzyści i zastosowania Event Sourcingu
Na co to komu? Audytowalność na pewno już widać w tym rozwiązaniu, bo ten nasz worek z danymi oczywiście możemy go tam sobie poczytać. Zakładam, nie wszystko w jednym worku, tylko np. różne akcje w różne worki, w różne tabele, jeśli mamy taką potrzebę, jakieś partycje byśmy tak na przykład np. żeby fajnie podzielić. Dowolność już na pewno jest i audytowalność każdego eventu, który wystąpi u nas w systemie, to już widać, że to jest zapewnione. Nie musimy robić jakiś ekstra ficzerów, jakiś ekstra rozwiązań i podpinać pod nasze standardowe kontrolery czy inne serwisy jakiś dodatkowych wywołań, które by tą audytowalność tam robiły, tak jak to zwykle sobie pewnie gdzieś tam robiliśmy. To na pewno jest fajna, fajna cecha tego. Więc jeśli rzeczywiście mamy system, gdzie chcemy mieć wgląd w historię jakiegoś naszego procesu, jakiegoś naszego obiektu, który zachodzi, każdy krok ma być widoczny, no to jest fajne rozwiązanie i da nam duży wgląd w obraz sytuacji, który nam się buduje. Więc to jest, to jest na pewno na plus. Czy coś jeszcze? Bo mam jeszcze jakiś plus. I co możemy sobie z takimi eventami zrobić, bo mamy ich setki, tysiące, i co dalej?
I co dalej? Ktoś chciałby zapytać: „No dobrze, ale jaki jest stan bieżący? Co my właściwie, co… jaki jest stan systemu?” No i tu się zaczynają pierwsze schody. Bo mamy te eventy w bazie i możemy je sobie wyciągnąć, ale dostajemy sekwencję eventów, jeżeli zapytamy o jakiś odcinek w czasie. Nie za bardzo mamy zapytać, po czym, o ile oczywiście nie sięgamy do jakiś wymyślnych, wykwintnych ficzerów związanych z zapytaniami. Załóżmy, że mamy PostgreSQL. PostgreSQL pozwala po JSON-ie. A i jeszcze załóżmy, że trzymamy to w JSON-ie. Przy tych założeniach możemy tutaj JSON-em odpytywać, ale wiadomo, że założenia są zawsze potrzebne, więc załóżmy, że są.
Załóżmy, że mamy jakiś dowolny engine trzymający nam te dane i ten engine pozwala odpytać o coś ze środka tych danych, to w teorii moglibyśmy sobie po prostu przepytać o zdarzenia o określonym typie. Co do metadanych, to nawet nie trzeba jakichś wymyślnych zapytań tutaj robić, bo założyliśmy, że metadane trzymamy osobno właśnie po to, żeby można było to odfiltrować, żeby można było jakiś tam typ wyłonić. I jeżeli trzeba, to jeszcze hierarchicznie możemy to zrobić, czyli możemy jakieś podtypy tam porobić i względem tego odpytywać, ale to już jest troszkę na inne zastosowanie. Ale gdybyśmy chcieli wyszukiwać po zawartości tych danych, też potrzebujemy bardziej wykwintnego mechanizmu zapytań.
Połączenie z CQRS i odtwarzanie stanu
No i tutaj właśnie się pojawia idea połączenia tegoż wzorca, bo nie ma co ukryć, Event Sourcing jest wzorcem projektowym właśnie z gatunku tych bardziej wyrafinowanych, i on się lubi łączyć z takim wzorcem, który się nazywa CQRS na przykład.
Troszkę nie wspomnieliśmy jeszcze, co to jest ten CQRS. Tak rozpatrując skrót, to jest Command Query Responsibility Segregation i może to jest nawet temat na osobny odcinek teraz, bo możemy sobie oczywiście robić CQRS bez Event Sourcingu, bez ES-a.
Tak, tylko że Event Sourcing dość naturalnie podąża w stronę CQRS-a i tak to od nas ten rozdział modeli do zapisu i do odczytu dość naturalnie tutaj występuje, więc to jest taka naturalna kolej rzeczy. Ale oczywiście oba typy podejścia nie są ściśle ze sobą związane.
Tak, oczywiście. Więc żeby teraz np. troszkę kontekst przybliżyć, CQRS jest to taki wzorzec, w którym zakłada się rozdzielenie stosów, tak to się najczęściej nazywa, na stos zapisujący i stos odczytujący. Czyli mamy dwie możliwe do wykonania ścieżki w naszej logice: ścieżkę modyfikacji danych, która przechodzi właśnie przez ten człon związany z literką C, czyli Command, i ścieżkę odczytu danych, która przechodzi przez to coś co ma Q, czyli Query. No i Command Query Responsibility Segregation – właśnie rozdzielenie tych dwóch odpowiedzialności, czyli osobna część systemu zapisuje nam tylko zmiany, a ta druga tylko nam odczytuje. One są rozdzielone.
I teraz Event Sourcing. Jeżeli chcemy wcisnąć w to Event Sourcing, to te nasze zdarzenia będą musiały być jakoś zapisywane. Taka jest idea, że one się po prostu zapisują, więc one przelatują nam przez tą ścieżkę z komendami. Czyli każda akcja zainicjowana przez użytkownika bądź przez jakiegokolwiek aktora – tutaj możemy w ogólności aktora rozpatrywać – przechodzi przez ścieżkę procesowania komend, czyli musimy wytworzyć jakąś tam komendę. Korzystając oczywiście z tych dwóch wzorców razem, musimy stworzyć komendę, która pociągnie za sobą wykonanie tejże akcji biznesowej i wtedy, po wykonaniu akcji biznesowej, w zależności od tego, jaki jest rezultat tej akcji, tego typu zdarzenie będziemy emitować. I te zdarzenia wyemitowane, po i na końcu handlera tej akcji biznesowej, jakiegoś tam handlera komendy, coś będzie łapało i zapisywało w tym naszym event storze. No i event store będzie cały czas przyrastał. To jest taki twór append-only, więc zapisy tam są bardzo prostą operacją. Właściwie można powiedzieć, że on jest z natury rzeczy zoptymalizowany pod zapis, bo tam nie trzeba grzebać w przeszłości, tam wręcz jest zabronione grzebanie w przeszłości. Musimy więc sobie tylko dopisywać do końca naszego strumienia eventów kolejne eventy biznesowe. Stąd też właśnie jest Event „Stream” w tej nazwie… właściwie „Source” to coś bardziej od źródła wartości, ale ten stream, który trafia do event store’a, to wszystko ma „event” w nazwie, musi mieć. On właśnie tworzy, i on właśnie tworzy to źródło eventów.
Natomiast do odczytu możemy oczywiście, możemy sobie taki event store też, z niego odczytać eventy i dostaniemy strumień eventów, dlatego wspomniałem tą nazwę. I teraz ten strumień eventów, które się zdarzyły w jakimś tam czasie, powiedzmy od początku świata do teraz, możemy sobie przetworzyć. To jest jedna z głównych cech, taka podstawowa, wręcz rekordowa, powiedziałbym rdzenna.
Czyli to przetworzenie tych eventów, to tak naprawdę ono generuje nam obecny stan systemu.
Tak. Czyli to jest nasza druga część tej układanki. Czyli mamy warstwę zapisu, czyli tam w event storze zapisujemy te rzeczy, natomiast odtwarzając po kolei te eventy, nakładając je na obecny stan, tworzymy bieżący stan systemu. To jest ta część, którą później możemy query’ować i to jest nasz obecny stan systemu. Tak oczywiście w uproszczeniu, bo jeszcze przydałaby się projekcja jakaś, która read model wyprodukuje, sam read model. Oczywiście. Może powiem tak, jeszcze parę innych pewnie dyrdymałów, ale w takim ogólnym ujęciu to jest właśnie to.
Czyli mamy zapisany ciągły strumień eventów i pobieramy sobie jakąś tam jego część, jakiś wycinek. W szczególności pobieramy od początku w ogóle zapisywania tych eventów, czyli od tego naszego umownego początku świata, do teraz. Odtwarzamy wszystkie eventy. Do czego jest nam potrzebna też logika biznesowa? Część logiki biznesowej już mamy. To jest ta część, która wykonała tą pracę biznesową właśnie. To są te składowe procesów biznesowych, właśnie nasze event handlery… właśnie, przepraszam, command handlery, które tworzą nam, w odpowiedzi na komendy, określone zdarzenia biznesowe, czyli one de facto dokonują zmiany stanu naszego systemu. Ponieważ każde dopisanie eventu do naszego event store’a zmienia nam stan systemu, dlatego że możemy teraz odtworzyć od dowolnego momentu ten stan systemu i sprawdzić, jaki on w danym momencie jest. Notabene tak właśnie działają wszelkiej maści handlery zdarzeń biznesowych, które muszą wiedzieć, jaki jest stan bieżący. Czyli tak czy siak musimy odtworzyć ten stan. Nie mamy tutaj jednak konieczności korzystania z tego wykutego w kamieniu schematu, bo ten schemat tak naprawdę trzymamy tylko i wyłącznie w logice, na potrzeby odtwarzania i na potrzeby przetwarzania kolejnych komend. Oczywiście, bo musimy wiedzieć, w którym momencie jesteśmy, więc my musimy mieć ten stan cały czas stworzony.
Teraz można by zapytać, a w ogóle jak to można? Jak to tak od początku, od początku? Bo jak siadamy np. jesteśmy czystym i muszę tu wprowadzić termin „agregatem”, który trzyma handler, i nagle dostaje tenże agregat nasz biznesowy komendy, to on musi znać ten stan. Na początku nie zna stanu, czyli ma stan zero. Więc my z takiego stanu zerowego, odtwarzając wszystkie eventy, jakie się wydarzyły od początku świata do teraz, odtwarzamy stan chwili na teraz, kiedy nie ma jeszcze tych eventów. Czyli przychodzi pierwszy… przychodzi pierwsza komenda, która wyprodukuje nam pierwszy event. Oczywiście mamy jakiś tam domyślny. I na tym stanie domyślnym, aplikując kolejne eventy z naszego strumienia zdarzeń, po prostu zbudujemy sobie ten stan. Czyli potrzebujemy jeszcze takiego sprytnego odtwarzacza naszych eventów i możemy sobie ten stan budować tak praktycznie dowolnie za każdym razem.
Wydajność, snapshoty i archiwizacja
I teraz się rodzi pytanie podchwytliwe oczywiście, bo tu nie ma prostych pytań, są same podchwytliwe. Czy to fajnie tak odtwarzać za każdym razem po kilkaset eventów? Albo kilka tysięcy?
Kilka tysięcy. Odpowiedź pewnie jest: pewnie nie fajnie, zwłaszcza jeśli logika, która ma to stworzyć, jest dość gruba. Więc co możemy? Czy są sposoby, żeby przyspieszyć proces?
Lista właśnie. Cieszę się, że pytasz.
Cieszę się, że pytasz naturalnie, ale odpowiedź jest… Odpowiedź jest być może zaszyta nawet w tym pytaniu. Bo jeżeli te handlery są grube, to może tak najpierw spróbowalibyśmy je odchudzić. Czyli może byśmy… I tu znowu się pojawi, co?
Już muszę się… nie daj odchudzić. Są grube, bo nasza logika jest gruba, bo nasz system jest potężny.
O kurka, tak. No to jeżeli mamy tak potężne rzeczy w naszym systemie, to wymyślono coś takiego, co jest takim trochę zapożyczeniem z poprzedniej ery, czyli snapshot tego stanu. Czyli powiedzmy, że mamy tam milion tych naszych grubych eventów już w naszym strumieniu. Żeby nie musieć za każdym razem, przy każdej komendzie, odtwarzać tego miliona eventów, po prostu sięgamy po ostatni snapshot stanu, który może być też trzymany w formie eventu, albo może być w jakiejkolwiek innej formie trzymany. Grunt, że jeżeli widnieje u nas w domenie informacja o tym… w danych domenowych widnieje informacja o tym, że mamy snapshot zrobiony, to sięgamy po prostu do ostatniego ważnego snapshota i odczytujemy ten snapshot. Już mamy ten stan początkowy i po prostu odgrywamy od tego snapshota te eventy, które się wydarzyły do momentu teraz. Gdyby się tak zdarzyło, że jednak nie mamy tego snapshota, musimy wszystkie eventy przejechać od początku. I tyle.
I to jest taka też wskazówka na to, jak sobie radzić z jakimiś tam błędnymi snapshotami, np. znaleźliśmy buga w systemie, okazuje się, że nasze dane są błędne, więc aplikujemy fixa, unieważniamy wszystkie snapshoty i do widzenia. I wtedy pierwszy dostęp albo unieważnia snapshot i za nim mówimy „do widzenia”, przebudowujemy, my tworzymy nowy snapshot w tym momencie, w którym unieważniliśmy stary. Może to nawet zrobić podmieniając, czyli podwójne buforowanie snapshotu: najpierw zrobić nowy snapshot, a później unieważnić stary i przełączyć się na ten nowy, i koniec. Tak, to już w takie szczegółowe techniki wchodzimy.
Warto też może powiedzieć, że jeśli kogoś przeraża tutaj wizja odtwarzania milionów eventów potencjalnie, to właśnie to jest to. Też jestem w stanie pomóc na zasadzie, że my tak naprawdę nie musimy tych naszych wszystkich eventów od początku życia systemu trzymać w tym jednym początkowym miejscu. No bo może być tak, że nasz system działa już 10 lat, no a na produkcji zebrał tych eventów ileś tam milionów. Mamy ileś tam tych snapshotów i nie ma też większych przeciwwskazań, żeby te starsze eventy, jeśli uznamy, że już nie są potrzebne, przenieść na boczek, na jakiś storage może tańszy, na jakąś pamięć masową taśmową, cokolwiek, czy chociażby do jakiejś partycji, która jest aktualnie… która jest przeznaczona po prostu… po prostu fizycznie miejsce, gdzie już nie będą nam potrzebne, bo akurat nie robimy restore’u od samego początku systemu, czyli sprzed 10 lat. No bo szansa jest rzeczywiście, że już tamtych eventów nie będziemy potrzebować do bieżącej pracy, ale one wciąż mogą być potrzebne właśnie np. do wymogów audytowalności, do jakiegoś zerknięcia, jeśli przyjdzie jakaś kontrola czy coś. Więc wtedy mówimy, że dobrze, my mamy te eventy tutaj na taśmie w szafie, więc możemy je za 3 dni tutaj podrzucić, ale chwilowo ich nie ma, ale wiemy, że one tam są. I albo sobie możemy… sobie Glacier możemy sobie tam wrzucić. To jest bardzo tani storage i wiemy, że tam będą i z dokładnością dziewięciu dziewiątek będą na pewno odtestowane, więc…
Więc to jest ten fajny, fajny plus, że tak naprawdę nie musimy ich ciągnąć przez 10 lat życia naszego systemu, gdzie pewnie jeśli mamy tabelę w relacyjnej bazie danych i mamy tam miliony wierszy, to raczej nie czyścimy starych wierszy, jakiś nieużywanych, nawet czasami nie wiadomo, które są dokładnie używane. No chyba, że mamy jakieś daty czy odczyt tego typu rzeczy, a tam jest… znaczy ciężej zrobić, znaczy ciężej zarchiwizować. No chyba, że wykorzystamy jakieś mechanizmy wbudowane w bazę danych.
Tak, czasem bazy dają jakieś partycjonowanie i wtedy możemy skorzystać. A tutaj faktycznie, taki audyt gdyby przyszedł z zewnątrz, to co najmniej 3 dni, bo jak je mamy na taśmie, to musimy jeszcze do dziadków po magnetofon pojechać, odtworzyć. To są już skomplikowane rzeczy, ale da się to zrobić. I z tym magnetofonem jak wrócimy, to możemy sobie je wszystkie odtworzyć, przesłuchać i zbudować ten stan taki, jaki był.
Wyzwania i ewolucja logiki
I teraz tylko, że to taka ciekawostka, bo przez te 10 lat mogła nam logika wyewoluować i te stare eventy mogą być już takie średnio takie tego.
No właśnie, to jest chyba też problem, który poruszymy w sekcji wyzwań, która jest trzy razy dłuższa niż sekcja benefitów, więc tutaj to może być… może być duży temat do dyskusji. Ale tak, no musimy pamiętać, że eventy nasze się zmieniają. Stare oczywiście są starymi, ale będą odtwarzane na nowym systemie, no bo kodu przecież nie będziemy starego trzymać. On się cały czas zmienia, cały czas logika nam ewoluuje, a te eventy muszą być kompatybilne z tą naszą ewoluującą logiką, więc to jest jakieś wyzwanie.
Tak, czyli istnieje jakiś taki zbiór aktualnie używanych handlerów biznesowych. I to są zarówno handlery od strony komend, jaki eventów, i one muszą zapewniać nam możliwość przetwarzania tych aktualnych danych, takich jak one przychodzą. Czyli jeżeli klient jakiś strzela komendą do systemu, to my musimy mieć pewność, że ta komenda zostanie właściwie obsłużona i wyprodukuje nam jakiś tam jeden event, albo może kilka naraz, bo też nie ma ograniczeń tutaj na ilość zdarzeń biznesowych, które sobie tutaj emitujemy. Natomiast jeżeli chodzi o te stare eventy, które już np. nie są obsługiwane, bo ich handlery poginęły, to zapewne też istnieją jakieś metody na odtworzenie tej logiki nawet teraz, jeżeli ona byłaby potrzebna.
Tutaj z korzyści jeszcze mamy tę audytowalność właśnie, ale jeszcze jest taka korzyść, że możemy w ogóle sięgnąć wstecz, do początków świata, z dokładnością właśnie do tych uwag, które przed chwilą tutaj naszkicowaliśmy, i na przykład wygenerować jakiś raport z danych, który… taki raport, który sięga historią do początków świata, pod warunkiem, że oczywiście w tych początkach świata mieliśmy zbierane te zdarzenia biznesowe, które nas interesują. Bo oczywiście schematu nie mamy sztywno wykutego, ale tak naprawdę schemat domenowy cały czas nam się zmienia, więc korzystamy tutaj z możliwości niewyrycia go w kamieniu, ale też niesie to taki skutek, że możemy niekoniecznie mieć w przyszłości te dane, których potrzebowalibyśmy. Czyli tak długo, jak możemy sięgnąć do danych, których potrzebujemy, możemy sobie takie raporty historyczne nawet dzisiaj generować. I nawet możemy sobie na potrzeby tych raportów utrzymywać taki kod specjalnie do nich przeznaczony, który będzie nam to odtwarzał, jak będzie potrzeba, gdy będziemy np. zmieniać jakieś tam warunki do tych raportów i coraz to nowe aspekty z tych danych wydobywać i jakoś je tam zestawiać. Jest taka fajna rzecz, która nie występuje w tych klasycznych, sztywnych schematach danych, gdzie mamy zapisany stan na chwilę obecną, jeżeli nie zrobimy typowego audyt loga właśnie z dodawaniem takiej tabeli audytowej, historycznej, czasem zwanej historyczną, to nie mamy tego niestety dostępnego.
Tak, tak. Tutaj ewidentnie widzimy, że zastosowania tego muszą być też w przypadkach, które rzeczywiście mogą wymagać tego typu cech systemu, czyli właśnie ta nasza audytowalność. Jeśli przewidujemy, że rzeczywiście będziemy musieli mieć pewne… pewne rzeczy bardziej już związane z analityką, czyli gdzieś tam będziemy te dane przerabiać, może je bardziej analizować, np. jakieś akcje użytkownika w kontekście, czy do swojego profilu zmian możemy sobie sprawdzać, ile razy coś tam mu się w profilu zmieniało. Może chcemy na tej podstawie wyciągnąć jakieś wnioski konkretne, więc rzeczywiście wtedy możemy sobie taki ślad tych eventów prześledzić i coś tam powiedzieć, coś tam zoptymalizować. To jest tak troszkę na pograniczu takiej już analityki i rzeczywiście faktycznej zmiany domenowej danego obiektu. To rzeczywiście, jeśli wiemy, że coś takiego jest, to to na pewno… na pewno nam się fajnie przyda.
Natomiast jeśli mamy dość prosty ten model i generalnie nie interesują nas za bardzo pośrednie zmiany, no to tutaj już jest ciężej uzasadnić takie podejście. Bo tu najprostszym przykładem, który się zwykle podaje w przypadku Event Sourcingu, jest oczywiście prowadzenie księgowości, czyli dopisywanie kolejnych kwot. Ja szczerze mówiąc, nie zaufałbym raczej bankowi, który prowadząc moje konto robiłby zwykły update salda: „Numer konta równa się i tyle”. I jest to jakaś metoda i nie prowadzi żadnych więcej historycznych tracków, co tam się właściwie działo, i tylko pójdzie człowiek, zobaczy, że chyba jest nie ta kwota. Tutaj klasycznie się robi listę transakcji, które się później sumuje, patrząc na to, jaki jest wynik na końcu. Ale są też inne domeny i inne konteksty biznesowe, które pewnie by też mogły na tym skorzystać, nie tylko na tym ostatnim updacie, który nastąpił, i w sumie później nie wiemy nawet, czemu to się stało. Nie wiemy, nie potrafimy prześledzić dokładnie, nie znamy historii. Nie znamy historii. Możemy oczywiście przejrzeć nasze logi, bo pewnie mamy tam dużo logów, debug logów, warningów i innych zdarzeń, czasem się zdarzy, i tego typu ciekawych konstrukcji, które nam to umożliwiają. Ale oczywiście to jest tylko chwilowa proteza na takie rzeczy, więc do umożliwienia fajnego śledzenia procesu krok po kroku i wyciągnięcia wniosków konkretnych. Więc jeśli mamy takie wymaganie, to na pewno nam się fajnie sprawdzi.
Event Sourcing w technologii i jego implikacje
Tak jest. I tutaj pojawiło się fajne słowo klucz: transakcja. Akurat w kontekście biznesowym, ale jakbyśmy przeszli płynnie z tego kontekstu na kontekst technologiczny, to słyszeliśmy o transakcjach plus kontekst wyzwań, bo już powoli chyba wkraczamy w wyzwania, które mierzymy. Cały czas błądzimy po różnych meandrach tutaj kontekstów.
I teraz jeszcze bym chciał nawiązać do tego kontekstu technologicznego, bo transakcja, jak wiemy, występuje w bazach transakcyjnych. No i się okazuje, że jakby tak złapać za śrubokręt, odkręcić obudowę do takiej bazy transakcyjnej, to pod środkiem, pod spodem będzie, w środku… co będzie? Event Sourcing. Bo tak mniej więcej jak jakiś log append-only będzie zapisywany, popularnie zwany commit logiem. To częściej podobne zjawisko znajdziemy też, jak byśmy obudowę od Gita odkręcili, tylko tam się będzie ciężej w okablowaniu rozeznać.
Tak, tak, tam też przynajmniej logicznie to polega na eventach, snapshottingu i tak dalej.
Czyli generalnie każdy silnik bazodanowy wykorzystuje ten wzorzec. Każdy, czyli mógłby, gdyby chciał, bo kto zabroni komu napisać na piechotę bazę danych i powiedzieć, że tak ma być? Ale takie szanujące się bazy danych jak np. Oracle, to już od zarania dziejów oni się w ogóle chwalili tym, że wykorzystują transaction log, który jest de facto przykładem Event Sourcingu. MySQL na pewno, MSSQL, wszystkie, wszystkie bazy danych transakcyjne. MongoDB też zakładam, że ma pod maską Event Sourcing. I systemy kontroli wersji, to są takie najbardziej popularne przykłady, już poczynając od SVN, ale i te wszystkie okoliczne, a Git to już w zupełności.
Tak, to, to już w ogóle jest Git. To już jest w ogóle chyba zaawansowany przykład Event Sourcingu.
No dobra, no to mamy, mamy ten Event Sourcing jednak obecny w naszym świecie od dość dawna. Tutaj nasi ziomkowie, gdzieś tam, powiedzmy z początków Domain-Driven Designu, z początków takich wzorców typu CQRS albo coś podobnego, czy jakichś tam event driven, coś tam, event processing, cokolwiek byśmy, jak byśmy tego nie nazwali, to pewnie zmierzali w tą stronę, aż w końcu powstał tenże nasz Event Sourcing w połączeniu z CQRS-em, by się ładnie spinający. Był on pewnie odpowiedzią na to, że mamy system, który wymaga jakiejś tam audytowalności, pewnie wysokiej dostępności. Więc tutaj się jednocześnie i w wymaganiach, i w skutkach pojawił brak transakcyjności. No bo się pewnie w którymś momencie ludzie zorientowali, że jeżeli mamy zapisy transakcyjne i mamy skomplikowane procesy biznesowe, to transakcja, która ma trwać od początku do końca, strasznie ogranicza wydajność, bo to wszystko musi być zablokowane i wtedy nikt nie może mieć dostępu do tego samego fragmentu danych, tego samego wycinka domeny. Więc zrezygnowano z transakcji i pojawiły się trudności wynikające z tego. Jednocześnie pojawił się pewien wzrost wydajności.
Nowe możliwości i wyzwania związane z brakiem transakcyjności
Czyli nowe możliwości i nowe wyzwania. I teraz, jak bez tej transakcyjności, takiej znanej i lubianej gdzieś tam z dawnych czasów, moglibyśmy sobie poradzić, kiedy na przykład coś nam się zepsuje? Jak mamy transakcję, no to leci wyjątek i baza nam powie, że się nie da zapisać. „Wracaj do domu”. No właśnie, brak perfekcyjności, i to z tym się też oczywiście wiąże. Przejście ze strong consistency w eventual consistency. To teraz takie pytanie: czy w naszym systemie faktycznie eventual consistency już teraz nie występuje? No bo jeśli mamy załóżmy system mikroserwisowy… Jeśli to jest system mikroserwisowy, więc pewnie jest oczywiście backpressure, więc pewnie jakaś kolejka, jakaś notyfikacja z naszego tutaj magazynu leci do naszej faktury czy gdziekolwiek indziej. No i w tym momencie, jak to już leci eventem, no to jest duża szansa, że to nie będzie transakcyjne, więc to będzie eventual consistent, więc ten wynik i tak będzie spójny, ale dopiero za jakiś moment, za jakiś moment. Więc czy rzeczywiście ten eventual consistency nas tak bardzo zaboli? Może już teraz mamy ten eventual consistency tak naprawdę, i fakt tego, że u nas read model odbuduje się dopiero po jakimś czasie, to może nie mieć większego znaczenia. Może już teraz nasz interfejs użytkownika optymistycznie wykonał jakąś akcję, a później tylko poluje po to, by uzyskać wynik naszej pracy. Może mamy jakiś web socket, może mamy jakieś server-sent events, więc generalnie często tak będzie, że właśnie coś takiego mamy. Tak więc, jeśli coś takiego mamy, to tak naprawdę eventual consistency tutaj zupełnie nie przeszkadza, bo to jest bardzo podobne podejście, jeśli chodzi o rozwiązanie konsystencji. Czy musimy po prostu poczekać na ten wynik i podpytać ten nasz model po jakimś czasie, ewentualnie jakimś rytuałem tutaj, tutaj raczej nas to nie zaboli. Natomiast jeśli chcemy to zrobić w modelu strong consistency, to już trzeba będzie się troszkę pobawić, prawdopodobnie jakimś rodzajem rozproszonych transakcji na Event Store i nasze tworzenie modelu części, więc Event Store ma to już w sobie wbudowane. Tak jak EventStoreDB na przykład. Zdaje się, że Marty, czyli ten Event Store do DDD, też to ma od razu w pakiecie. Natomiast jeśli używamy naszego tutaj domowego rozwiązania, bądź co bądź, Kafki, o czym jeszcze porozmawiamy, to tutaj musimy sobie jakoś tą transakcję zrobić, zrobić bardziej na piechotę.
Tak, ale to jest też taka rzecz do zrobienia na takim wyższym poziomie abstrakcji, nawet w sensie nie musimy tutaj jakoś strasznie, chyba strasznie niskopoziomowo schodzić, żeby taki wyrzeźbić nasz ten end do eventów, żeby on to wszystko obsługiwał, bo możemy to nawet zrobić customowe, jak byśmy chcieli, i być może tylko w niektórych przypadkach. Bo tak naprawdę, jak mamy te eventy, mamy zapisaną całą historię tego, co w naszej domenie się dokonało. To, no nie chcę tutaj jakimiś szacunkami rzucać, ale w pewnej ilości przypadków w ogóle nie potrzebujemy tych transakcji. Wystarczy, że będziemy w stanie zrobić kompensację dla tego, co się zepsuło. I tutaj właśnie fajnie się to spina z jakimś debugowaniem nawet na produkcji. Zresztą te systemy są bardzo podatne na debugowanie takie na żywca – można trafić na prawdziwych danych i z pełną, pełną historią. Coś takiego jakby debugger na żywo w systemie. Ponieważ te nasze eventy to są takie właśnie już twory, które zawierają cząstki informacji o zmianach i odtwarzanie ich w sekwencji już nam daje taki ciąg do bazowania. Możemy, wiedząc, że na przykład dla pewnego wycinka eventów gdzieś tam na produkcji wystąpił błąd, czyli mamy w naszym modelu czy w stanie domeny wartości niepożądane, możemy wziąć ten wycinek, na przykład zanonimizowany (żeby, jeżeli mamy tam jakieś dane klientów czy w ogóle cokolwiek, czy w ogóle jakiekolwiek dane wrażliwe, no to pobieramy je z produkcji, anonimizując w locie, ale możemy sobie metadanych nie anonimizować, nawet jest to wskazane), żeby to się dało odtworzyć w naszym testowym środowisku. I to nasze testowe środowisko, zaczynając od snapshotu, który możemy sobie na żądanie zrobić na przykład tuż przed eventem, od którego zaczynamy, możemy sobie dosłownie odtworzyć stan dla tego danego przypadku. I odegrać wszystkie zdarzenia po kolei i zobaczyć, w którym momencie coś się zepsuło. I jak już wiemy, co się zepsuło, to wprowadzić poprawkę oraz dołożyć do naszego event streamu eventy kompensujące dokładnie ten błąd. I po prostu na przykład zrobić nowy snapshot, od którego system ruszy, i już po wprowadzeniu na produkcję plus tego fixa już mamy magicznie, a jednak nie musieliśmy. Nie musieliśmy ingerować w ten stan, który był zapisany w przyszłości. Po prostu sobie go wzięliśmy. Pożyczyliśmy go sobie do odczytu, przygotowaliśmy poprawkę i zrobiliśmy kompensację tego stanu tak, żeby się aktualny snapshot zgadzał. Nic więcej nie trzeba zrobić. Brzmi prosto? Brzmi prosto. Prosto. Wspominamy za to proste. Wspominamy cały czas ładowanie eventów. Odtworzymy sobie, zrobimy _snapshot_a.
Zarządzanie zmianami modelu i ewolucja kodu
No dobrze, ale ten magnetofon, na którym to mamy nagrywane, już to sygnalizowaliśmy – ten problem eventów i kodów, czyli rzeczy, które są stałe (eventy historyczne), ale kodu, który się zmienia i też może ogólnie trochę zmiany modelu. No bo co jeśli załóżmy chcemy sobie dodać jakiś port? Wiadomo, nasz model też może się mimo wszystko zmienić, nieważne jak go tam super modelujemy. Czasem możemy też chcieć coś dorzucić. Może chcemy zrobić jakiegoś tsunami? I co wtedy? Jak, jak bardzo, jak podatny system na tego typu zmiany? Co możemy wtedy zrobić? Bo jednak prędzej czy później takie wyzwanie od naszego Product Ownera się pojawi. I wtedy jesteśmy w kropce. W kropce. W kropce. Jesteśmy. Zawsze możemy wrócić do tych tabelek. Zawsze może po prostu usłyszymy schemat, albo i tak nie udało się. Z tych to jednak nie. Ale możemy też wprowadzić tą zmianę i gdzieś tam… I chciałem powiedzieć inteligentnie, no to zawsze się staram inteligentnie, inteligentnie nad tym zapanować. Ale po prostu jeżeli jest konieczność przemodelowania kawałka naszej domeny, to ta nasza nowa logika, która to przemodelowanie wspiera, musi uwzględniać te dwa stany. Czyli jeżeli dwa lub więcej. Możemy. Może nas napadnie jakaś dzika potrzeba robienia jakichś testów A, B, A, B, C itd. To wtedy musimy ileś tam wariantów rzeczywistości rozpatrywać, ale wtedy wiadomo, ta cała nasza logika staje się coraz bardziej skomplikowana. Ale ona dalej. Tak długo jak przykładamy wagę do tego, ona dalej podąża za tymi wszystkimi możliwymi ścieżkami. Dopiero kiedy nam się coś zmieni, to owszem, możemy ją zechcieć odłożyć na bok. I zastąpić ją jakąś tam logiką, taką ustaloną, uproszczoną, która teraz już od tego momentu zmiany (tylko że on musi być jasno określony) już podąża za nowymi wytycznymi, ale dobrze by było gdzieś tam mieć pod ręką ten, ten, ten czy ów tam Dark (chodziło chyba o debugger – przyp. aut.), żeby jeszcze jakieś bugi naprawić. Użyć tego w takim kodzie debugującym, gdzie sobie zaczynamy te eventy z magnetofonu i odtwarzając będziemy właśnie wpuszczać w tą starą logikę po to, żeby dostać ten stary stan.
Jeszcze też słyszało się tu i ówdzie jakieś wersjonowanie takich schematów domeny, czyli że można mieć ileś, powiedzmy, handlerów, może chcieć utrzymywać ileś handlerów równolegle obok siebie, które na przykład na tym samym typie eventu operują (typie logicznym), tylko że z określonych zakresów czasu, czyli od zmiany do zmiany. I on sobie siedzi tak jakby w takiej poczekalni, kiedy zostanie wywołany ze swoją wersją, to wtedy wejdzie i coś nam tam zrobi. No i wiadomo, że im więcej takich komplikacji, tym bardziej musimy pewnie przykładać wagę do tego, żeby to było sensownie zamodelowane, zaimplementowane, przetestowane. Utrzymywać to w dobrej formie, aż dojdzie do takiego momentu, że jednak ta stara logika faktycznie odchodzi do lamusa, więc odkładamy to. Eventy trafiają do archiwum, tam sobie leżą. Nowy snapshot powstaje. I tak długo, jak nie mamy wykrytej jakiejś nieprawidłowości, która się pewnie nie da skompensować łatwo, to one mogą sobie tam poczekać tutaj w tym archiwum.
Tak, tutaj pewnie są takie dwa trochę podejścia. Z jednej strony możemy niejako zrobić sobie migrację tych naszych eventów na nowy model. Możemy sobie tak, jak robimy migrację z live schematu bazy danych. Możemy sobie też przy jakiejś większej zmianie, która naprawdę nam psuje ten model, możemy pokusić się o przebiegunowanie tych naszych starych eventów do nowej postaci. Czyli dodanie jakiegoś pola, usunięcie czegoś. Może anonimizacja, chociaż to zawsze jest troszkę bardziej kłopotliwe. I wtedy, jeśli wszystkie eventy nasze stare otworzymy na nowy model, to nie musimy utrzymywać w kodzie naszej starej logiki, tylko zawsze mamy ten jeden stan naszej logiki, bo nasze eventy podążają za tą logiką. Więc to jest pewnie z punktu widzenia kodu jest to najczystsze rozwiązanie, bo wtedy mamy tylko tą jedną logikę. Eventy są też w innej postaci, ale oczywiście wymaga stworzenia jakiegoś procesu migracji tych eventów. Jeśli oczywiście mamy te eventy gdzieś tam na tej taśmie, no to ten proces będzie trudniejszy. Sama migracja to oczywiście mocno zależy pewnie od ilości tych eventów. Natomiast z drugiej strony możemy pójść w tym kierunku, że eventy zostają takie, jakie są, czyli że nigdy ich nie zmieniamy. Natomiast tak jak wspomniałeś, utrzymujemy pewne malutkie gierki, które w zależności, czy to od wersji, czy od jakiejś daty, czy od czegokolwiek innego aplikują troszkę inną logikę dla różnych eventów. To oczywiście komplikuje nam pewne podejście w kodzie, ale czasami nie musimy grzebać w naszych eventach, nie musimy tutaj przeprowadzać żadnych migracji. To są dwa podejścia w zależności od systemu, od ilości eventów, od naszych umiejętności wpięcia tego wszystkiego w proces. Bo taka migracja eventów pewnie będzie ciut bardziej skomplikowana niż po prostu wpięcie flag, zrobienia Event Store i lecimy dalej. No tak, coś za coś.
Event logi, Kafka i zewnętrzne systemy
Ale właśnie ta migracja. Zdaje się, czy ogólnie taki sposób przetwarzania takich eventów… Tutaj posłużę się troszkę inną nazwą. Event logów legły u podstaw w ogóle Kafki w LinkedIn czy jak to się tam wcześniej nazywało, chyba jakoś inaczej się nazywało. Chyba tak. Ale czytałem właśnie taki fajny artykuł, przez twórców Kafki wypuszczony, przez jednego z nich. I zapomniałem nazwiska, ale to się wytnie. I tam właśnie było opisane dokładnie ta technika. Czyli jeżeli mamy jakiś tam strumień eventów i chcielibyśmy coś innego z niego uzyskać, to możemy sobie go zawsze przemoderować. Przy czym jest jedno takie założenie święte i tutaj chyba było jak świętość. Nie wygaśnie. Po prostu trzymamy ten stary jako taki i referencyjny, bo a nuż. Coś jednak przegapiliśmy i może nie byłoby dobrze pozbyć się tamtych danych, więc tamte dane trafiają do takiego najbardziej gdzieś tam najniższego archiwum, gdzie już bardzo rzadko się sięga tego, co dzieje się tego w piwnicy, tego co jest zalewane. Najczęściej więc sięga się tam bardzo rzadko, ale czasem może jeszcze istnieć konieczność sięgnięcia tam, więc nie usuwamy tych danych. Możemy migrować, ale tak jak wspomniałeś, jest to obarczone też trudnością migracji. No ale tutaj z drugiej strony w tych przykładach, które ja przytoczyłem, mamy trudność z nadużywaniem, z logiką i w ogóle godzenia różnych wariantów logiki, więc generalnie najlepiej to jest jednak pomyśleć zawczasu. Dokładnie.
I to jeszcze warto wspomnieć o tym, że zmienia się nasz, zmienia się nasz kod. Nasze eventy powiedzmy się nie zmieniają, ale wciąż mamy historyczne rzeczy. Tylko że przy replikacji, replay naszych eventów, odtwarzaniu, trzeba się zastanowić, czy my nie polegamy na jakichś rzeczach, na jakichś zewnętrznych systemach. Bo warto pamiętać, że jeśli chcemy odtworzyć jakiś event, a nasza logika woła jakiś inny serwis, jakieś inne API z zewnątrz, to jednak ta odpowiedź musi też być konsekwentna. Za każdym odwołaniem z takimi samymi parametrami. Czy tak naprawdę ten proces odtwarzania naszych eventów od dowolnego momentu musi być zawsze idempotentny, czyli daje nam zawsze ten sam wynik? Startując od tego samego punktu. Bo inaczej jeśli za każdym razem będziemy wołać jakieś API zewnętrzne odtwarzając nasz event i przyjdzie inny wynik, to nasz model też będzie miał inny wynik. Więc pewnie może warto by było też zastosować sobie na przykład zwrotkę z tego API, jeśli to jest potrzebne do wyliczenia czegoś. Może jakiś inny sposób? To jest też taka drobna rzecz. Nie zawsze oczywiście to jest w projekcie w ten sposób zrobione, to jest jakieś zewnętrzne API, ale jeśli to się zdarza, no to trzeba pamiętać, że to są te trzy wektory zmian, czyli eventy, kod, ale też zewnętrzne zewnętrzne, zewnętrzne środowisko, zewnętrzne środowisko, w którym my istniejemy.
Tak, i ponieważ tak się składa akurat, że mikroserwisów mamy teraz, że psami rzucać, to to pewnie tak się będzie działo. Na szczęście też mamy wujka Boba. A wujek Bob? Czy żeby odcinać zależności, tam stawiać sobie jakieś takie zaślepki na końcach? I właśnie to może być ratunkiem w tej sytuacji, żeby sobie przechwytywać to, co przychodzi nam z tego, co na zewnątrz byśmy chcieli zasięgnąć. Bo to jest de facto wtedy też faktem w naszym systemie. Czyli ta odpowiedź z API jest też tym faktem. I tak chyba to będzie, będzie trzeba traktować. Czyli to też to jest zdarzenie biznesowe? Tak, na tamten stan, taki był fakt tego zewnętrznego systemu. To nie jest coś, co my za każdym razem musimy odpytywać, więc też warto pamiętać, modelując to, co zapisujemy w tym evencie. Bo w normalnym podejściu to wiadomo, robimy sobie, robimy naszą logikę, wołamy restore API, lecimy dalej. I teraz proszę popatrzeć, jak to nam się ładnie spina z naszym wzorcem, bo możemy sobie zamodelować ten fakt, którym odpowiedział nam zewnętrzny system przez swoje API, jako event biznesowy, który notabene został sprowokowany komendą biznesową, którą wysłał jakiś jeden z komponentów z naszego systemu. Powiedzmy, że jeden fragment domeny wygenerował potrzebę zapytania kogoś z zewnątrz, więc posłał tam komendę. To coś zapytało i odpowiedziało eventem… To znaczy to coś odpowiedziało, a my przechwyciliśmy to naszą fasadą. I to do tego API zewnętrznego. To może być dowolne API testowe, kolejka, cokolwiek. Nie. Nawet można by wsadzić tam na chama odczyt z pliku, jakby ktoś chciał, bo z punktu widzenia naszej domeny to tak naprawdę nas nie interesuje. Interesuje. Że chodzi dokładnie technicznie, to mamy jakąś przelotkę. Tak? Do tego to jest nasza, nasza jakaś tam jakaś dana, którą chcemy skombinować albo i nie. Wszystko się tutaj ma własne i wszystko możemy sprowadzić do tych eventów, które znowu pchamy. Nasz strumień eventów. Tak. Tutaj event sourcing tam nawet pomaga to tak delikatnie zamodelować i zwrócić uwagę na pewne, pewne aspekty z tym związane. Dodałem event SOS i to jest nawet niezłe. A nawet nie tylko. Co by było, jakby nam wepchnęli dwa eventy o tym samym numerze wersji? Też mają takie trochę konkurencji i sił, bo nam się tutaj może coś wykrzyczeć. Tak leciutko.
Gdzie przechowywać eventy?
To chyba, chyba przejdziemy do punktu: gdzie to wszystko trzymać? Czyli… Bo tutaj padło też słowo Event Store. Przewijała się Kafka, przewijała się baza relacyjna. Nawet plik na dysku się przewijał. No i. I gdzie? Gdzie to trzymać? W prawdziwym Event Store czy? Czy może samemu sobie napisać? Czy może użyć Javy? Wracając do poprzedniego wniosku to gdziekolwiek. Mnie to nie interesuje. Oczywiście dopóki nie dojdziemy na poziom detali. To właśnie zeszliśmy na ten poziom eventów i możemy to sobie zapisać też gdziekolwiek. W zależności od tego, czego potrzebujemy. W chmurze. W chmurze pewnie też możemy, nie. Pytanie czy w tej Kafce. Te Kafka taka popularna i tam nie dają i w ogóle tam. Mocna, mocna. I Event Logi swoje robili.
Jak sobie pogrzebałem, to tutaj zdania ekspertów są podzielone. Jak to zwykle bywa. Eksperci od tego są, eksperci od tego są. Każdy, każdy ma własną opinię. Na pewno jeśli… Jeśli chcemy tylko się pobawić, zrobić jakiegoś PoC? Może, może jakieś krótkie rozpoznanie bojem? Kafka pewnie będzie jak najbardziej okay, zwłaszcza jeśli tą Kawkę znamy, a nie znamy jakiś dedykowanych narzędzi do event sourcingu. Wtedy to pewnie będzie jak najbardziej pasujące rozwiązanie. Natomiast jeśli grzebiemy tak głębiej, mogą być pewne ograniczenia Kafki, które nam troszkę pokrzyżują szyki. Pomijając natomiast takie ograniczenia na zasadzie: do Event Store’a potrzebujemy, potrzebujemy bazy danych, a Kafka nie do końca jest bazą danych, bo jest de facto kolejką. Event Store tak naprawdę potrzebujemy klucz-wartość i tak naprawdę więcej nas nie interesuje. Fajnie, jeśli to się nam transakcyjnie zapisze. Jeśli mamy całego ACID’a i to w ogóle będzie nam bezpiecznie i trwale się zapisywać. Kafka oczywiście da nam trwały zapis i możemy sobie ustawić retencję na bardzo długi okres czasu i dużo różnych parametrów. Natomiast Kafka nie da nam na przykład optimistic locking. Czy tutaj nie do końca panujemy nad wersjonowaniem tych eventów? Nie do końca będzie, będzie to jasno. Tu trzeba będzie po prostu jakiś ręczny mechanizm właśnie tego wersjonowania wprowadzić, żeby wiedzieć, żeby wiedzieć czy napisaliśmy te eventy poprawnie, czy nie, bo one będą przychodziły w kolejności, oczywiście tak jak tam partycjonowanie gwarantuje, więc tu kolejność nie ma żadnego znaczenia w sensie nie jest to problemem. Natomiast nie wiemy, czy tak naprawdę ten nowszy event on nie był pobrany ze starszych danych, więc tutaj ten optimistic locking, w zasadzie bazodanowy, czyli ktoś, kto przychodzi ze starszą wersją jest automatycznie odrzucane. No to tutaj tego niestety nie będzie, więc to może być problem. Nie musi, ale warto wiedzieć, że w ten sposób tego nie zagwarantujemy. Trzeba to będzie zrobić na piechotę.
Problemy z wersjonowaniem i konkurencją
Ale to chyba nie przeszkadza jakoś zbytnio, bo pewnie się to da zrobić na piechotę i też byśmy chcieli zobaczyć, czy nie Kafkowy event store na zwykłej prostej bazie. Obojętnie teraz czy weźmiemy sobie bazę relacyjną, czy jakąś dokumentową. To i tak potrzebujemy tam jednego wora na wszystko. Więc to doskonale zadziała na MySQL i na ten sam przykład i w podobny sposób też będziemy sobie jakieś tam błędy z wersjonowania eventów rozpatrywać, bo to tutaj nie bardzo. Może nie tyle nam chodzi o to, że będzie nam się mieszała kolejność gdzieś tam na szynie danych. Z szyny danych jeszcze nie wspomnieliśmy, ale to też jest jakiś taki element potrzebny do tego. Kolejka bardziej szyna, czyli w ogóle jest wielu piszących i wielu odczytujących. I oni mogą wrzucać tam swoje rzeczy w dowolnym momencie i może być. Może być tutaj zaburzenie kolejności odbioru tych eventów, które my byśmy chcieli zapisywać właśnie z tego tytułu, ale częściej pewnie będzie nam się zdarzało tak jak przy optimistic lockingu w takich zwykłych klasach biznesowych, że to jednak doszło do równoczesnej modyfikacji podobnych i technicznie podobnych do tych samych naprawdę, czy też podobnych danych domenowych przez różnych użytkowników. Taki popularny przykład z tym związany, co to modyfikacja jednego rekordu takiego logicznego gdzieś tam do nowego przez dwóch operatorów tego samego systemu, czyli oczekiwalibyśmy, że pojawi nam się event o wersji 100 w systemie. Tylko że jeden operator myślał się trochę dłużej, a drugi submitował swój formularz wcześniej. No i ten event o numerze 100, co do którego obydwa agregaty biznesowe były pewne, że to one właśnie wyprodukują, został wyemitowany przez tego pierwszego, a drugi w trakcie swojego zakończenia pracy wyemitował już event, który się nie nadaje do zapisu. I tutaj się przyda na przykład taka jeżeli mamy bazę transakcyjną do transakcji już przy tym zapisie eventu. Czyli możemy od razu sobie sprawdzić, że nie możemy w Event Store zapisać takiej wersji eventu. I od razu wracamy z błędem na górę, jeżeli nie mamy tej transakcji. Dobrze by było to zamodelować, bo inaczej trafią nam do Event Store eventy o wersji 100 w ilości 2 i wtedy nam się odczyt posypie. To tak, bo powinniśmy to mieć. Z drugiej strony, przepraszam bardzo. Z drugiej strony powinniśmy tutaj. Jeżeli już mamy to wersjonowanie przewidziane, to powinniśmy sobie jakąś unikatowość na tym akurat skrawku naszego eventu założyć, jakąś jakiś tam unique index i on też nam nie pozwoli zapisać eventu, który, który tu de facto wersjonowanie się do tego sprowadza. Tylko że już schodząc z bardzo niskopoziomowych, bo mechanizm wersjonowania to jest taka jakby nakładka na. I dlatego o tym wspominam, że sam goły unique index nawet tutaj może nas uchronić przed zapisaniem niewłaściwych danych.
Obsługa niewłaściwych eventów i wysoka dostępność
No właśnie, to są takie problemy, których może na początku nie widać gołym okiem, ale są może troszkę bardziej techniczne. Ja też pamiętam taki problem, który mieliśmy w systemie. Tutaj ostatnio pracowaliśmy w dużym. To był dość duży system oparty o event sourcing. On jest, on już niejako może też bardziej związany z samym event-driven architecture, niekoniecznie z event sourcingiem. Ale dość długo czasu nam zajęła odpowiedź na pytanie, co robić z niewłaściwymi eventami? I przez niewłaściwe eventy tutaj rozumiemy: bądź niepoprawne pod kątem biznesowym (czyli te, które nie przeszły walidacji, ale ogólnie są poprawnie sformatowane pod kątem technicznym), czyli poprawny JSON albo poprawny protobuf (czy tam cokolwiek przechowujemy w tych eventach)? Czyli składnię mają dobrą, mają dobrą składnię, ale biznesowo są niewłaściwe. Lub druga kategoria niewłaściwości to eventy, które są w ogóle nieczytelne, czyli jakiś tam nasz bitowy zestaw znaków i tego eventu się zupełnie rozjechał. Nie potrafimy go nawet zredukować. I oczywiście każdy powie, że każdy kto słyszał, powie zapewne, że dobrze, to wrzućmy to do dead-letter queue, bo tam one będą sobie leżeć i ktoś się tym zajmie. Natomiast dużo czasu nam zajęło odpowiedź na pytanie: kto ma się tym zająć? Co ten ktoś ma z tym zrobić? Takie to już bardziej, bardziej biznesowe. Odpowiedzi na to pytania. No bo to, że to sobie pójdzie do dead-letter queue to to nie jest żaden problem. To sobie tam może leżeć. Tylko co dalej z tym zrobić? Gdzie rzeczywiście tutaj biznesowo odpowiedź na to pytanie nie jest taka prosta. No bo trzeba sobie wymyśleć jakąś strategię, czy rzeczywiście te eventy zupełnie odrzucamy, czy manualnie weryfikujemy i patrzymy okiem człowieka, czy one się nadają, czy może bardziej maszynowo analizujemy? Czy zupełne pominięcie tych eventów psuje nam jakoś system, czy musimy je jakoś skompensować, uwzględnić? Więc to są takie rzeczy, nad którymi może w takim zwykłym rozwiązaniu, powiedzmy testowego API, nawet się nie zastanawiamy. Przychodzi nam po prostu 400 czy 500 i nie zastanawiamy się. Czy ta odpowiedź, która, czy ten request, który przychodzi gdzieś tam z frontu, czy mamy go jakoś kompensować? No nie, no przecież jest 500 tam, to co tu kompensować? To w ogóle nic się nie zostawia. Natomiast jeśli coś przejdzie na naszą. Czy to kolejka? Akurat tam używaliśmy Javy. Rzeczywiście dość Javy. Zadanie do event sourcingu oczywiście. Więc. Więc tam to te eventy już były na naszym topicu, więc jeśli już coś trafia do naszej przysłowiowej rury, to wypadałoby się tym zająć. Więc tak naprawdę to jest taka drobna, drobna rzecz, która wydaje się dość początkowo prosta, ale później rodzi bardzo, bardzo, bardzo wiele pytań.
I też jeszcze taki z tamtego projektu pod kątem właśnie event sourcingu. Tam akurat event sourcing stosowaliśmy jako sposób zapewnienia wysokiej dostępności systemu, czyli tam event sourcing zapewnił nam fajne rozpropagowanie i fajne. Fajne, fajne właśnie. Sposób wysłania do danego zestawu wszystkich eventów na inne klastry. Dzięki temu mogliśmy w łatwy sposób w razie awarii danego klastra przełączać się na inne klastry, bo one zawsze podążały za tym głównym aktywnym klastrem. Ponieważ zawsze czytaliśmy najnowsze eventy i on sobie budowały ten read model odbudowywał swój własny swoje własne źródło prawdy, więc wtedy jeśli jeden klaster padnie, następny automatycznie przyjmuje rolę mastera, bo podążamy za eventami. To oczywiście da się zrobić w różnych sposobach replikacji baz danych również, ale event sourcing też warto wspomnieć, że może też pełnić rolę właśnie takiej synchronizacji i umożliwienia wysokiej dostępności aplikacji w razie awarii. Więc to są takie fajne i z życia wzięte, którym które mieliśmy okazję programować w rzeczywistym życiu, a nie tylko w jakimś. To jest fajny przykład, bo wprawdzie po takiej awarii, kiedy musimy odtworzyć ten na tym nowym node’zie cały nasz read model. To chwilę nam zajmie i może jakieś tam opóźnienie wygenerować dla użytkownika na przykład, czy dla jakiegoś innego sektora, ale jednak to sobie działa i może się zdarzyć automatycznie. Czyli mając odpowiednio wyrafinowaną logikę panowania nad tym modelem przed nami, nad wersjonowaniem, możemy sobie zapewnić takie fajne scenariusze przywracania w przypadku awarii. Na pewno ciekawa zaleta takiego systemu. Się okazuje, że nie są aż tak wielkie nakłady w kodzie wymagane, żeby tego typu funkcjonalność zapewnić w porównaniu do systemów pisanych klasycznie, bardziej klasycznie, gdzie faktycznie gdybyśmy chcieli to samo osiągnąć, to musimy całą nadbudowę zrobić na to co mieliśmy, bo tam to. To coś co o co było oparte nas na przodkach klasycznych na na chwilowym stanie danych zapisywanych właśnie teraz i później. Modyfikowany bez żadnej tam historii. Wymagałoby właśnie jakiś takich mechanizmów. No chyba bardziej wymyślnych. Przywracania tego stanu historycznego. Natomiast to co przytoczyłeś z tym też jest fajnym przykładem i chyba też też to się da rozwiązać na kilka przynajmniej sposobów, bo na przykład można sobie wymyśleć takie podejście, żeby w przypadku takiego eventu, który. Który by nam symbolizował 500-kę klasyczną wyemitować jakiś. Potraktować go w ogóle jako event biznesowy, że proces utknął w tym miejscu i koniec, kropka. Na skutek błędu na przykład, nie. To jest taki z pogranicza biznesu i technikaliów, ale tak naprawdę klasyczna 500-ka, jaką dostajemy gdzieś tam z czegokolwiek. To jest też coś takiego. Taki event po trosze biznesowy, bo nasz proces biznesowy właśnie utknął w jakimś dziwnym miejscu. I techniczny, bo jeszcze mamy być może jakąś wskazówkę, dlaczego nie, bo bo nie, bo jakiś timeout gdzieś tam poleciał, bo bo coś tam. No i wtedy, jeżeli właściwie modelujemy ten nasz proces biznesowy, to naprawdę możemy opisać go takimi eventami, które mają sens i jest je dość trudno zniszczyć.
Kiedy używać event sourcingu?
Dobra, więc dużo sobie powiedzieliśmy o tym, więc musiał używać tego event sourcingu czy nie używać? Ja bym tam używał. Nie, tak naprawdę to zdecydowanie używać, ale może nie do grube. Może nie do krótszych. Używać tam, gdzie ma sens, używać tam, gdzie ma sens. Tam gdzie są wymagania zarysowane do tego, żeby faktycznie było widać korzyści z tego. Czyli na przykład takiej bankowości powiedziałbym: używać. Czy używać na przykład do jakiegoś, nie wiem, systemu uprawnień użytkowników. Na dwoje babka wróżyła. Jeżeli potrzebujemy taki audyt dowolności, bo jest to bardzo kluczowy system, to co do czegoś kto coś zrobi to jak najbardziej, bo wtedy mamy wszystkie, wszystkie dane mamy jak na dłoni. Jeżeli z drugiej strony jest to prosty system. Adminie admin to może niekoniecznie, bo to znowuż przypomina trochę takiego CRUDa. Nie tworzymy użytkownika, zapisujemy go, logujemy się, odczytujemy go, sprawdzamy co ma ustawiony admin. Dobra admin też dobra, może wtedy nie. Czyli zawsze trzeba popatrzeć na wymagania biznesowe, jakie mamy do zrealizowania i wtedy nam wyjdzie. A może może nie tylko biznesowy? Zresztą dobra, to będzie się przeplatało, bo jeżeli mamy wymagania biznesowe, które wskazują na taką dość zaawansowaną architekturę i na użycie wzorców CQRS, może event sourcing będzie też pasował. Czyli to może być jakaś taka. Może to być jakiś pierwszy trop, że jeżeli biznes jest odpowiednio skomplikowany i też ma jakieś zależności czasowe i widzimy, że brak w nim transakcji. I jeszcze Product Owner upiera się na te raporty historyczne i w ogóle wszystko się zgadza. To nie ma wątpliwości, warto sobie ulżyć. Z drugiej strony jak widzimy część przesłanek to może też warto użyć, bo już mamy część przesłanek, więc mamy uzasadnienie, a w przyszłości mamy potencjał na to, żeby wykorzystać pozostałe feature’y.
Tak, tak, i na pewno nie musimy tego używać w całym systemie. Jeśli widzimy tylko, że jakiś kawałek naszej domeny pod to pasuje, to użyjmy tylko tam. Nie musimy od razu części kodowej też pod to wpinać, tylko do tego, że mamy rozbudowany model uprawnień i tam byśmy chcieli to zrobić. Ale mamy też użytkowników, którzy edytując zdjęcie profilowe, to tam może niekoniecznie chcemy to mieć w pełni, a oddawali śledzone każdą zmianę. Więc używać, ale z głową.
Z głową. Tak jak mój dziadek.
Dokładnie. Znał ten sourcing. Proszę.
Przeczuwał.
Przeczuwał. Dobra, no to chyba tyle.
Chyba tyle. Pogadaliśmy trochę i pewnie to nam znowu otwiera kolejne ciekawe tematy do przegadania następnym razem. Może przyjrzymy się, kto wie, za jakimś stolikiom na te nasze eventy?
Może bardziej tudzież CQRS-owi?
Zobaczymy.
Dobra, dzięki na dzisiaj. Dzięki Michał.
Dzięki Wojtek.
Do następnego.
Do następnego.
Cześć.
Cześć.