Oskar Dudycz

Pragmatycznie o programowaniu

Snapshoty w EventStoreDB

2021-01-04 oskar dudyczEvent Sourcing

cover

Cześć!

Witam w nowym roku! Pozwól, że oszczędzę Ci podsumowania zeszłego roku, których teraz zalew wpada w newsletterach, blogach i ogólnie “internetach”. Ja nie robię postanowień noworocznych. Oczywiście jeśli komuś one pomagają, to nie mam nic przeciwko. Niemniej jednak, dla mnie postanowienia noworoczne to alternatywna wersja postanowienia - “zacznę od jutra”. Zatem do sedna!

Snapshoty przewijały się już kilkukrotnie w tym newsletter. Choćby w odpowiedzi na pytanie “Dlaczego konto bankowe nie jest najlepszym przykładem Event Sourcingu?”. Dla przypomnienia:

Co to jest snapshot? Jest to dosłownie “stopklatka”, czyli zrzut stanu naszego modelu w danym okresie czasu, np.: - aktualny stan obiektu - może być zapisany np. w znanej nam i lubianej tabeli relacyjnej, gdzie każde pole obiektu to osobna kolumna. Może również być zapisane w postaci klucz wartość, gdzie kluczem jest identyfikator encji, a wartością jej stan zapisany w JSON, a potem siup do tabeli - tak to robimy w Marten - zobacz więcej w https://martendb.io/documentation/events/projections/. - stan na dany moment czasu, np. stan konta na początek miesiąca, możemy wtedy pobrać np. snapshot z początku miesiąca, plus zdarzenia, które wydarzyły się później i je zaaplikować, - stan po kazdej transakcji - wtedy otrzymujemy z automatu historię i wszystkie stany naszego obiektu.

Taki snapshot (aktualny stan agregatu/strumienia) można zapisać do tabeli, możemy też po prostu zapisać w cache typu Redis. Dzięki temu możemy pobrać ostatnio zapisany stan oraz zdarzenia, które zaszły od momentu jego utworzenia. Następnie zaaplikować je i uzyskać aktualny stan.

Tak możemy zrobić gdy mamy osobny storage na Snapshoty, lub jak np. w Marten gdy używamy Postgres zarówno do zapisu zdarzeń jak i do Snapshotów/projekcji.

EventStoreDB podchodzi do tego inaczej. EventStoreDB jest tylko i aż bazą danych do zapisu/odczytu zdarzeń. Jest zoptymalizowana pod tryb “append only”. Dzięki czemu może osiągnąć potencjalnie lepsze wyniki wydajnościowe niż inne silniki. Tak naprawdę jedyne co możemy zrobić to dodać nowy wpis (zdarzenie). Jak zatem w takiej bazie zapisywać Snapshot, który z założenia jest “upsertem”?

Możemy zapisać każdy nowy stan agregatu (nowy snapshot) jako osobne zdarzenie w dedykowanym strumieniu. W EventStoreDB identyfikatory strumienia są stringami. Najczęściej mają strukturę np. “{NazwaStrumienia}_{unikalnyIdentyfikator}”. Zatem dla ułatwienia, jeśli nasz strumień ma nazwę “Order_{orderId}” to strumień ze snapshotami mógłby nazywać się “SnOrder{orderId}”.

Co dzięki temu zyskujemy? EventStoreDB umożliwia określenie maksymalnej ilości przechowywanych zdarzeń w strumieniu ($maxCount - https://developers.eventstore.com/server/20.6/server/streams/metadata-and-reserved-names.html#stream-metadata). Dlatego jeśli na ten “Snapshotowy strumień” nałożymy wymaganie, że może mieć maksymalnie jedno zdarzenie to de facto nowa wersja “nadpisze” starą. Po dodaniu nowej stara zostanie zarchiwizowana i pozostanie tylko najbardziej aktualny stan. Dzięki temu możemy najpierw odczytać zdarzenie-snapshot, sprawdzić dla jakiej wersji strumienia zaszło i pobrać zdarzenia, które nastapiły po nim.

Innym rozwiązaniem jest publikowanie zdarzenia snapshotowego w tym samym strumieniu. EventStoreDB umożliwia czytanie strumienia w dwie strony. Klasycznie od najstarszego zdarzenia do najnowszego, ale też odwrotnie czyli od najnowszego do najstarszego. Możemy zatem zacząć czytać strumień od końca dopóki nie natrafimy na zdarzenie Snapshotowe. Wtedy przestajemy czytać zdarzenia dalej. Bierzemy te zdarzenie snapshotowe jako punkt wyjścia stanu agregatu, na który aplikujemy już odczytane zdarzenia (w kolejności chronologicznej).

Według mnie pierwsze rozwiązanie jest prostsze i łatwiejsze w utrzymaniu. Co prawda musimy odpytywać dwa strumienie, ale wiedząc, że ten snapshotowy ma tylko jedno zdarzenie nie będzie to wcale narzutem. Trzymanie zdarzeń snapshotowych w tym samym strumieniu co zwykłe zdarzenia powoduje, że mogą być one nadmiarowe. Jeśli np. naszą taktyką snapshotowania jest zapis np. co 3 zdarzenia strumienia to dla każdych 10 000 zdarzeń dodatkowo otrzymamy 3000 dodatkowych zdarzeń snapshotowych.

Rozwiązanie drugie jest według mnie lepsze gdy nasze zdarzenia snapshotowe nie są zdarzeniami sztucznie wprowadzonymi ze “zrzutem” stanu agregatu, ale mają faktyczną wartość biznesową. Jeśli te zdarzenia są takimi “checkpointami” w cyklu życia agregatu jak np. OrderConfirmed i poza samym aktualnym stanem niosą specyficzną wartość biznesową to i tak będziemy je publikować.

Na koniec dodam, że Snapshoty powinny być używane dla modelu zapisu. Nie powinniśmy mieszać ich z modelem odczytu. Jest to technika optymalizacyjna. Jak z każdą taką techniką musimy uważać, żeby nie była ona przedwczesna. Możemy wylać dziecko z kąpielą i wpaść w pytania co zrobić i jak inwalidować snapshoty, albo je przemigrować.

Najlepiej zacząć od tego, żeby jednak nie dopuszczać do sytuacji gdy Snapshoty będą nam usilnie potrzebne. Napisałem o tym niedawno na blogu we wpisie “How to (not) version events?” https://event-driven.io/en/how_to_do_event_versioning/.

Daj znać co o tym sądzisz i czy masz jakieś pytania? Chętnie rozwinę to o praktyczne przykłady w kodzie jeśli Cię to zainteresowało.

Na koniec kilka linków ode mnie:

Pozdrawiam! Oskar

  • © Oskar Dudycz 2019-2020