Wiele instancji, jedna baza, eventy i odpowiedzialność za utrwalanie danych

0
  • Załóżmy że mamy sobie jakiś serwis X, który ciągnie sobie dane z jakiejś SQLowej bazy danych i tam też chciałby utrwalać zmiany.
  • Załóżmy że możemy mieć N instancji tego serwisu.
  • Załóżmy że chcemy tam użyć subscription z GraphQL, więc chcemy informować frontend o zachodzących zmianach (czyli frontend nie robi nowego requestu po dane, tylko dostaje od nas websocketem delty)
  • Załóżmy, że chcemy trzymać stan aplikacji w pamięci i nie ładować przy każdym requeście rzeczy od zera z bazy (to tylko dla ustalenia uwagi, bo niczego to specjalnie nie zmienia tutaj)

Problem: przychodzi jakiś POST z frontendu (czy tam mutacja z GraphQLa) i teraz potrzebujemy:

  • Przepuścić to przez logikę domenową, żeby sprawdzić czy ta operacja w ogóle ma sens
  • Jeśli tak, to potrzebujemy poinformować frontend o zmianie + potrzebujemy zmianę utrwalić w bazie
  • Jak nie trudno zauważyć, potrzebujemy też jakoś poinformować inne instancje o tym że zaszły zmiany, żeby mogły odpowiednio updatować swój stan oraz poinformować klientów frontendu którzy są do nich wpięci

Najbardziej oczywiste rozwiązanie byłoby takie:

  • Instancja która odebrała request przepycha to przez logikę domenową i jeśli request ma sens, to utrwala zmiany w bazie, emituje event dla innych instancji i informuje swoich klientów o zmianach
  • Pozostałe instancje odbierają event, przepychają go przez logikę domenową i informują swoich klientów o zmianach

Nie podoba mi sie tutaj trochę to, że w sumie mamy tu 2 flowy. Można by to mocno uprościć, gdyby instancja która odebrała request po prostu emitowała automatyczne event, a potem wrzucenie tego w logikę domenową plus informowanie klientów odbywało się z event handlera. Tylko że pojawia się tutaj problem persystencji, bo ktoś to finalnie musi zapisać do bazy a w takim modelu nie bardzo wiadomo kto powinien być za to odpowiedzialny.

Oczywiście można by dołożyć jeszcze drugi serwis, który łykałby event i zajmował się tylko zapisywaniem zmian w bazie, czyli takie trochę CQRS, bo wtedy serwis X byłby tylko od czytania a ten nowy serwis zajmowałby się pisaniem, ale mam wątpliwości czy uda mi się przepchnąć taki design...

Da się to jakoś sprytniej zrobić? Nitka idź!

1

Te N instancji rzeczywiście daje do myślenia jednak mimo, że godzina robi się już późna to obstaję, że Najbardziej oczywiste rozwiązanie byłoby całkowicie OK.
Jakie korzyści da Ci ten drugi serwis?
Czy już masz wiedzę o tym, że będą kłopoty z wydajnością (to by uzasadniało)?
Czy jest jakaś inna przyczyna żeby to rozdzielać?

I skoro gdyby instancja która odebrała request po prostu emitowała automatyczne event to mam naiwne pytanie:

  1. Czy do wszystkich frontendów czy do aplikacji (kto jest odbiorcą tego zdarzenia)?
  2. Jeśli do front-endów to czy one na 100% nie dostaną go zanim zmiany zapiszą się w bazie?

No i w sumie czy te poszczególne instancje o sobie wiedzą? Czy to w nich jest zaszyta logika biznesowa a część wspólna to jedynie baza? Jeśli się nie "widzą" to zakładając, że ilość zapisów jest znacząco mniejsza i mniej obciążająca niż odczyty to może ten nowy "serwis" zapisujący wepchnąć właśnie między instancje a bazę. Wówczas on będzie emitował komunikat po zmianie danych.

0

Załóżmy że możemy mieć N instancji tego serwisu.

mieć możemy, a chcemy?

0

Czy do wszystkich frontendów czy do aplikacji - kto jest odbiorcą tego zdarzenia?

Nie no jak do frontendów niby? Wiesz ty jak działają websockety? :D Mówie o emitowaniu eventów do innych instancji serwisu, bo tylko one mają websocketowe połączenia z klientami.
Problem z tym oczywistym rozwiązaniem jest taki, ze masz w aplikacji bardzo podobną logikę w dwóch miejscach (w obsłudze requestu i w obsłudze eventu) i to mi się nie podoba. Korzyść z drugiego serwisu jest właśnie taka, że ta druga logika wylatuje w inne miejsce i ten dualizm znika. Nie ma to nic wspólnego z kwestią wydajności akurat, bo przecież bottleneck to i tak będzie baza.

Jeśli do front-endów to czy one na 100% nie dostaną go zanim zmiany zapiszą się w bazie ?

Jakie to w ogóle ma znaczenie? Przecież frontend nie czyta nic z bazy o_O Niemniej event leci do backendowego serwisu, więc jak frontend dostanie informacje o zmianach to dana instancja już będzie miała te zmiany "zaaplikowane" tak czy siak. Zresztą te websocketowe informacje do frontu są jakoś magicznie używane przez graphql, tak ze on nie robi dodatkowego requestu do backendu, tylko modyfikuje sobie stan klienta na ich podstawie :)

0
Shalom napisał(a):

Nie no jak do frontendów niby? Wiesz ty jak działają websockety?
Jakie to w ogóle ma znaczenie? Przecież frontend nie czyta nic z bazy o_O

Wiem jak działają .. zapędziłem się.

Nie wiem natomiast jaka jest komunikacja pomiędzy aplikacją=instancją a podłączonym do niej klientem=front-endem. Czy aplikacja zna "stan" front-endu? Czyli wie jaka "encja/opcja" jest w danym momencie otwarta i jakie rodzaje komunikatów jest sens wysyłać? Z opisu zrozumiałem, że aplikacja śle do forntu każdą zmianę z danymi (deltą)? Co jednak w przypadku kiedy front-end tej zmiany w danym momencie nie potrzebuje - czy wtedy te komunikaty latają na darmo?

0

@katakrowa to jest trochę obok tematu, bo nawet gdyby tych notyfikacji do frontu nie było, to nadal nie zmienia wyjściowego problemu ;) Z tym graphqlem to takie trochę testy robimy, możliwe że w ogóle ten pomysł wyleci, ale póki co frontedowiec chciał spróbować z tego korzystać. Niemniej: https://www.apollographql.com/docs/react/data/subscriptions/ więc klient decyduje o tym co go interesuje generalnie.

1

Czyli jak dobrze rozumiem (ja to bym chyba potrzebował jakiś rysunek, żeby to zrozumieć). Przychodzą dane wpadają na jedną z instancji i zostają przetworzone. Jak jest ok to idą do zapisu na bazie i pozostałe instancje są informowane o tym, że muszą podesłać przez websoceta dane gdzieś w świat. Czy instancje powinny zaczekać na zapis danych i dopiero je odesłać?
Nie rozumiem, po co te same dane pchać przez logikę domenową "odczytujące" i wysyłając przez websoceta - jak jedna instalacja przejedzie je logiką to potem można jest wysłać w świat (nie znam GraphQLa i to może tu się tak nie da)? Tak na mój prosty rozum to by się przydała jakaś kolejka - jedna instancja przerabia dane pakuje w kolejkę a reszta odczytuje. Do tego DB zapisywacz - który tez jest zapięty na kolejkę.

0

Czy instancje powinny zaczekać na zapis danych i dopiero je odesłać?

To raczej bez znaczenia dla nich, jeśli w evencie dostaną informacje o tym co się zmieniło. Jak event zawiera informacje user zmienił maila na [email protected] to możemy sobie tą zmianę "wprowadzić" gdzieś do obiektów domenowych w pamięci niezaleznie od tego czy zmiana już jest w bazie czy jej tam nie ma.

Nie rozumiem, po co te same dane pchać przez logikę domenową "odczytujące"

Ja tu nie mówie o walidacji, albo nie tylko o walidacji, ale po prostu o "wykonaniu akcji". Tzn. user zmienia jakiśtam tryb działania aplikacji, więc chce sobie znaleźć tego usera, jeśli mam go załadowanego w pamięci, i zmienić mu ten tryb, plus wykonać wszystkie operacje które to za sobą pociąga. Tak żeby kolejny request do tej instancji zwracał poprawne wyniki. Pisałem gdzieś wyżej, że chce uniknąć "ładowania od zera" przy każdym requeście i chce trzymać spójny stan w pamięci. Wyobraź sobie że na "stan" składa sie nie tylko to co w bazie (bo to nie CRUD) ale też jakieś dane płynące z innych serwisów, więc "ładowanie od zera" jest ciężką operacją i nie chce jej robić co request.

jak jedna instalacja przejedzie je logiką to potem można jest wysłać w świat

To jest szczegół, ale websocket jest przecież połączony do konkretnej instancji więc nie da się mu nic wysłać w inny sposób niż z tejże instancji :) Plus jw. każda instancja musi sobie te zmiany "zaaplikować".

Tak na mój prosty rozum to by się przydała jakaś kolejka - jedna instancja przerabia dane pakuje w kolejkę a reszta odczytuje. Do tego DB zapisywacz - który tez jest zapięty na kolejkę.

Raczej event stream niż kolejka ;) No i to jest to co opisałem jako drugą możliwość, ale wymaga to dołożenia drugiego serwisu który zajmuje się "pisaniem", a tutaj zostawienia tylko "czytania" .

1

Te dwa flowy są osobne i tak ma być. Najpierw dostajesz command z frontu, młócisz i na końcu wypluwasz eventy opisujące zaaplikowane zmiany, dla innych instancji nie powinno mieć znaczenia, czy będą odtwarzały stan z historii z bazy + historii otrzymanej online od innej instancji, czy tylko z historii z bazy, czy tylko ze snapshotu + aktualizacji od innej instancji.

A jeżeli te dwa flowy coś Ci duplikują, to napisz więcej, co one dokładnie robią, bo pewnie masz problem X Y.

2
Shalom napisał(a):

Ja tu nie mówie o walidacji, albo nie tylko o walidacji, ale po prostu o "wykonaniu akcji". Tzn. user zmienia jakiśtam tryb działania aplikacji, więc chce sobie znaleźć tego usera, jeśli mam go załadowanego w pamięci, i zmienić mu ten tryb, plus wykonać wszystkie operacje które to za sobą pociąga. Tak żeby kolejny request do tej instancji zwracał poprawne wyniki. Pisałem gdzieś wyżej, że chce uniknąć "ładowania od zera" przy każdym requeście i chce trzymać spójny stan w pamięci. Wyobraź sobie że na "stan" składa sie nie tylko to co w bazie (bo to nie CRUD) ale też jakieś dane płynące z innych serwisów, więc "ładowanie od zera" jest ciężką operacją i nie chce jej robić co request.

Czyli tak naprawdę chodzi o współdzielenie/ustalanie wspólnego stanu (całego lub jego części) aplikacji przez wiele instancji. Takie cos można rozwiązać przez bazę w pamięci np przez redisa (jednego globalnego albo per instancja). Jedna instancja robi walidacje itp itd i zmienia stan aplikacji w redisie albo redisach w zależności od implementacji. Raczej spadek wydajności nie będzie znaczny względem trzymania danych w pamięci a będzie można go zmieniać z zewnątrz. Nowa instancja będzie miała skąd pobrać aktualny stan bez odpytywania wszystkich źródeł i będzie można co jakiś czas zapisać taki stan na dysk tak, że w przypadku faila aplikacja, wstając odczyta sobie poprzedni stan z pliku.

0

A jeżeli te dwa flowy coś Ci duplikują, to napisz więcej, co one dokładnie robią, bo pewnie masz problem X Y.

@Afish nie duplikują w sensie kodu, bo wołam tą samą logikę biznesową pod spodem. Nie podoba mi się tylko trochę to że mam dwa różne entry pointy do tej logiki -> jeden to request z frontu a drugi to event od innej instancji.
Stąd pomysł że np. instancja która dostała request mogłaby tylko emitować eventy o zmianiach bo wtedy entry point do logiki domenowej byłby tylko z event handlera, ale to wymagałoby dodatkowego serwisu który czyta te eventy i wprowadza zmiany do bazy :)

@UglyMan to co opisałeś w zasadzie nie różni się niczym od tego co opisałem na początku jako najbardziej oczywiste rozwiązanie, gdzie instancja która dostała request przetwarza go i wprowadza zmiany, a potem informuje innych o zmianach. To czy zmiany pójdą jakimś redisem czy eventem to już szczegół implementacyjny.

3
Shalom napisał(a):

A jeżeli te dwa flowy coś Ci duplikują, to napisz więcej, co one dokładnie robią, bo pewnie masz problem X Y.

@Afish nie duplikują w sensie kodu, bo wołam tą samą logikę biznesową pod spodem. Nie podoba mi się tylko trochę to że mam dwa różne entry pointy do tej logiki -> jeden to request z frontu a drugi to event od innej instancji.

Ta logika nie powinna być taka sama. Request z frontu powinien dostarczyć commanda, event od innej instancji powinien dostarczyć zweryfikowaną informację o sposobie aktualizacji danych. Intuicyjnie request z frontu prosi "przelicz fakturę" a event z innej aplikacji robi "cena faktury ustawiona na 123". Wspominasz, że CQRS może nie przejść, ale moim zdaniem koncepcyjnie powinieneś iść w takie rozdzielenie.

Shalom napisał(a):

Stąd pomysł że np. instancja która dostała request mogłaby tylko emitować eventy o zmianiach bo wtedy entry point do logiki domenowej byłby tylko z event handlera, ale to wymagałoby dodatkowego serwisu który czyta te eventy i wprowadza zmiany do bazy :)

Nie wiem, czy Ty czasem nie masz problemu z race condition. A obecnie aktualizujesz bazę i wysyłasz eventy w jednej transakcji? Jak nie, to w końcu się to sypnie. To się często rozwiązuje wzorcem Outbox, gdzie eventy do wysłania dodajesz do bazy w tej samej transakcji, co modyfikacje biznesowe, a potem jest osobny demon wysyłający wiadomości na szynę/kolejkę. "osobnego serwisu" do czytania eventów i tak raczej nie unikniesz.

1

@Shalom możesz zrobić rysunek tego co opisujesz? Ja z Twoich opisów rozumiem to tak:

kroki:

  1. event z front end do swojej instancji APP z danymi do zmiany.
  2. Instancja sprawsza sensowność danych i jeśli ok to zapisuje je w SQL i idzie dalej else jakieś message dla front-end.
  3. Zapis danych w SQL
  4. Powiadomienie instancji 2, 3, o zmianie i jej rodzaju i wysłanie komunikatu do własnego FE(6) ( np. potwierdzającego zmianę ).

w istancjach 2, 3 relizują sie kroki
5. odczyt danych z bazy
6. wysłanie komunikatów do swoich front-endów

screenshot-20201020123426.png

2

Przyznaje, że mam słaby dzień żeby się wczytywać, ale dość standardowe podejście to podział command (z serwisu), eventy
Komenda na podstawie stanu ustala jakie eventy zaszły (ale nic nie zmienia) - czyli wynikiem komendy (nie trzeba tego formalizować- to może być normalna funkcja w serwisie) - jest lista eventów.
Dopiero eventy określają perzystowane zmiany - i odpalasz tą samą listę na wszystkich instancjach.

Tu jest pewien myk - bo jeśli komenda ma zwrócić wynik po zmianach... no to trzeba jakoś poczekać aż będzie zrobiona tych eventów aplikacja, a to zwykle jest asynchroniczne.
(stąd podział na Command i Query zwykle się bierze, żeby nie mieć tego problemu).

Ale z drugiej strony, skoro i tak to klientów wysyłasz zmiany asynchronicznie - to akurat tego problemu nie masz.

Patrząc na oryginalny post:

Przepuścić to przez logikę domenową, żeby sprawdzić czy ta operacja w ogóle ma sens

Normalka - wynikiem tej operacji jest lista eventów

Jeśli tak, to potrzebujemy poinformować frontend o zmianie + potrzebujemy zmianę utrwalić w bazie

nie teraz

Jak nie trudno zauważyć, potrzebujemy też jakoś poinformować inne instancje o tym że zaszły zmiany, żeby mogły odpowiednio updatować swój stan oraz poinformować klientów frontendu którzy są do nich wpięci

wysyłamy eventy do wszystkioch instancjI (również do bieżacej)
i podczas przetwarzania tych eventów informujemy klientów o zmianach

3

Pewnie nie to chciałeś usłyszeć, ale CQRS i Event Sourcing idealnie się wpasowują w ten problem. Osobne serwisy do obsługi POST (commands) i osobne do obsługi widoków (eventów, powiadamiania frontendu). Dobry event store mogłby pomoc, ale można samo CQRS zastosować i bez event sourcingu. Zauważ że twój opis samego frontendu idealnie się wpasowuje w event sourcing - dostajesz zmiany, a nie całe zmienione obiekty.

Problem tkwi w szczegółach technicznych, tzn wszystkie unhappy pathy, gdzie np udało się zapisać do bazy, ale event nie wyszedł i nie dotarł gdzie trzeba, bo sieć zawiodła, bo serwis został ubity itp itd. (Nawet używając message bus, mógł się zapisać do bazy ale nie dotrzeć do kolejki). I tutaj właśnie Event sourcing i event store są pomocne, bo masz jedno źródło prawdy które jest zawsze poprawne. A widoki najwyżej z opóźnieniem się odświeżą. Dobry event store np Event Store, dużo oferuje 'out of the box' w tym temacie.
Jeżeli to nie wymagająca na tym poziomie spójności danych aplikacja, i w tych.... nieoszukujmy się, niezwykle rzadkich przypadkach przyjmujesz że gdzieś dane się zapodzieją i nie będzie katastrofy promu kosmicznego, to da się to łyknąć na wiarę, że jak się uda zapis do bazy, to uda się wysłać event z updatem. Ale to tylko na słowo honoru będzie działać. (7 fallacy of network computing daje się czasem w kość ;-) ). Wtedy będzie potrzebny pewnie restart serwisu zajmującego się widokami i powinien sobie wszystko odświeżyć, zależy jak to tam u Ciebie działa w tej waszej NSA, CIA, NASA, ESA czy PPPP ;-)

Inne rozwiązanie, to zapis od razu do bazy updatu i osobnego 'eventu' do tabeli w jednej transakcji. Następnie mamy osobny proces (albo osobny serwis) który czyta z tej tabeli eventów 'nowe' i je wysyła na właściwą kolejkę. Jeśli Ci się wydaje, że w tym przypadku poniekąd robisz z bazy kolejkę... to masz rację, ale tylko dzięki temu masz pewność spójności danych. I nie jest to ani fajne ani super wydajne.

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