Oskar Dudycz

Pragmatic about programming

Czy to się będzie skalować?!

2021-03-15 oskar dudyczSztuka Programowania

cover

Cześć!

Często przechwalam się, że zacząłem karierę zanim powstał StackOverflow. Dopiero w zeszły piątek udzieliłem pierwszej odpowiedzi na Stacku. Moja odpowiedź nieco zaskoczyła pytającego (https://stackoverflow.com/a/66601249/10966454).

Jakie to pytanie? “Jak skalować projekcje danych w systemach opartych na zdarzeniach?”

Co to jest projekcja? Pisałem o tym już we wcześniejszych newsletterach: (np. “Jak robić projekcje zdarzeń dla rozbudowanych struktur obiektów?”).

“Dla przypomnienia - projekcja to interpretacja pewnego zestawu zdarzeń. Służy ona zwykle do budowy read modelu. Przykładowo - mając zdarzenia związane z zakupem biletu do kina: UtworzonoTymczasowąRezerwację, ZmienionoMiejsceRezerwacji, PotwierdzonoRezerwację i aplikując je po kolei wiemy, że rezerwacja została potwierdzona i jaki był ostateczny numer siedzenia. Taki wynik możemy zapisać po prostu jako wpis w bazie relacyjnej. Możemy też wyprodukować zdarzenie i zapisać je w osobnym strumieniu. Wtedy ostatnie zdarzenie w tym strumieniu reprezentuje aktualny stan read modelu.”

Ludzie w dzisiejszych czasach chcą wszystko skalować. Kiedyś receptą na wszystko było kupienie większego sewera, dzisiaj jest dodanie nowej instancji. Czy zawsze ma to sens?

W tym przypadku niekoniecznie. Aplikując zmiany, które zachodzą w innym module chcemy zwykle wykonywać w odpowiedniej kolejności. Jeżeli dodamy instancje (klony) tego samego nasłuchującego serwisu to możemy sami sobie strzelić w kolano. Może się okazać, że spowodujemy wtedy problem wyścigu o zasoby (tzw. “competing consumers”). Zasobem, o który będzie toczyła się walka będą zdarzenia z subskrypcji/kolejki, bo różne instancje będą pobierały z niej. Walka będzie też toczyć się o docelowy rekord w bazie danych, bo może się okazać, że kilka instancji dostanie zdarzenia dla tego samego rekordu docelowego.

W takiej sytuacji niezwykle trudno zachować kolejność zdarzeń, zdarzenia też są częściej powielone. Jeśli używasz lub znasz RabbitMQ - dokładnie ten sam problem jego dotyczy. Są różne sposoby jak ludzie próbują się ratować przed złą kolejnością zdarzeń. Niektórzy sprawdzają czy ostatnia zapisana wersja rekordu jest większa niż ta, która jest w zdarzeniu, jeśli jest to ignoruje zdarzenie (bo założenie, że zostało już przeprocesowane przez inną instancję). Ma to swoje wady gdy:

  • pierwszy handler pobrał z kolejki zdarzenia numer 1 i 2,
  • drugi handler pobrał z kolejki zdarzenie numer 3.

Jeśli ten pierwszy będzie procesował zdarzenia wolniej niż drugi to może się okazać, że drugi już przeprocesował zdarzenie numer 3. Z racji, że nie było do tej pory zdarzenia przeprocesowanego to je zapisze. Z koleji pierwszy gdy sprawdzi czy wersja jego zdarzeń jest większa to okaże się, że jest niższa i je zignoruje.

Nie zawsze kolejność zdarzeń jest krytyczna. Czasem jak są to ciągle wpływające zdarzenia transportowe (służace do replikacji). Takie zdarzenia zwykle robią na koniec upsert bez jakiejś logiki. Możemy wtedy przyjąć optymistycznie jeśli sytuacje są sporadyczne, że “kiedyś się to wyrówna”.

Inną sytuacja jest gdy zdarzenia pochodzą z wielu strumieni (np. z różnych topiców lub partycji w Kafce). Wtedy nie mamy żadnej gwarancji, że dane z różnych partycji będą miały odpowiednią korelację czasową. Możemy sobie radzić wtedy zapisując te dane, które damy radę (np. najpierw dane z update’a), a potem uzupełnić je tymi danymi, które w koncu do nas przyjdą (np. ze zdarzenia utworzenia rekordu). Minusem jest to, że wtedy musimy się pogodzić z tym, że nasze dane mogą być niepełne. Nie zawsze jest to problemem. Z założenia nie powinniśmy podejmować decyzji biznesowych na podstawie read modelu. No ale jeśli sytuacje często się powtarzają to może to być upierdliwe dla użytkownika. Plus programista musi te wszystkie korelacje oprogramować.

Tutaj też przychodzą dwie faktyczne porady:

  • użycie jednej instancji budującej read model - projekcja zdarzeń zwykle nie powinna mieć rozbudowanej logiki i robić prostą zmianę rekordu na bazie. Z założenia powinno to być szybkie. Może się okazać, że jedna instancja gdy nie musi konkurować o zasoby będzie szybsza w działaniu i do napisania niż złożony kod wielowątkowy. Nie wiem czy wiesz, ale na takiej zasadzie powstał NodeJS. Autorzy stwierdzili, że wielowątkowość jest przereklamowana, lepiej kolejkować zadania niż je wspóldzielić. Ma to sens również w naszej codziennej aktywności. Szybciej zrobisz coś jak będziesz próbować się skupić na kilku rzeczach, czy jak sobie ustalisz dobrze kolejność?
  • dobre partycjonowanie danych i sharding - jeżeli podzielimy sobie serwisy nie robiąc proste klony, ale dzieląc również, na które zdarzenia mają nasłuchiwać i przede wszystkim, po tym gdzie mają zapisywać to może się okazać, że wojna klonów o zasoby nie będzie konieczna. Możemy to zrobić np. rozdzielając typy read modeli pomiędzy różne instancje. W praktyce będziemy mieć zestaw niezależnych instancji skupiających się na obsłudze konkretnych zdarzeń.

No i tutaj wracamy do mitycznego pytania? “Czy to się będzie skalować?!” Jest to zdecydowanie pytanie z gatunku tych, które potrafią mi podnieść ciśnienie. Zwykle okazuje się, że pytający nie wie co to dla niego znaczy. Nie zna ilości requestów, rozmiaru danych, charakteru ruchu, itd. Dodatkowo zapominamy o podstawach. Nie zawsze jest sens dokładać nowe instancje, a jeśli już ma to sens to powinniśmy robić to z głową. Load Balancing, sharding, partycjonowanie to są podstawy, które działają również w dzisiejszym świecie mikroserwisów. Ba! Właściwe użycie Kafki, baz danych opiera właśnie się na dobraniu właściwego klucza. Bez przemyślenia jak nasze dane mają się do siebie i jak zbudować właściwy klucz nie osiągniemy dobrze skalujących się systemów. Wiadomo, nie jest to tak seksowne jak odpalenie nowego deploymentu Kubernetesa, ale konieczne. Pisałem o tym szerzej w Newsletter: “Bazy klucz-wartość - o czym warto pamiętać w ich użyciu”.

Jak Ty podchodzisz do skalowania?

Pozdrawiam Oskar

P.S. Pociągnąłem w tym tygodniu dalej temat CQRS i opisałem moje przemyślenia o tym czy komenda może zwracać wynik: https://event-driven.io/pl/can_command_return_a_value/.

P.S.2 Zrobiłem też szablon i opis jak skonfigurować saplikację NodeJS z serwerem WebAPI z frameworkiem Express. Do tego z TypeScript ESLint, Prettier, etc. Sprawdź tutaj: https://github.com/oskardudycz/EventSourcing.JS. Jest to zalążek pod planowaną serię Live Coding o tym jak robić Event Sourcing i Type Driven Development: https://github.com/oskardudycz/EventSourcing.JS#nodejs-project-configuration.

A oprócz tego jak co tydzień nowe Architecture Weekly: https://github.com/oskardudycz/ArchitectureWeekly#15th-march-2021

  • © Oskar Dudycz 2019-2020