W pełni asynchroniczna komunikacja między mikroserwisami

Wątek przeniesiony 2020-11-07 20:56 z Nietuzinkowe tematy przez cerrato.

4

cześć,
Czy ktoś z was miał do czynienia z systemem który do komunikacji między mikroserwisami używał wyłącznie jakiegoś msg brokera/kolejek typu Kafki/Pulsara/Rabbita/PubSub itp. (w pełni asynchroniczne)?
Chodzi mi o brak wywołań po REST API/GraphQL/RPC albo inne tego typu; a zamiast tego takie eventowe, asynchroniczne przetwarzanie.

Z perspektywy klienta takiego backendu (który powiedzmy z jakims wejściem do systemu komunikuje sie za pomocą REST API) - najlepiej aby komunikacja przebiegała synchronicznie. Tzn wysyla request, coś tam na backendzie jest przetwarzane i serwis odpowiada już po przetworzeniu całego zapytania.

W przypadku asynchronicznego backendu to staje się niemożliwe. Pierwszy serwis przyjmie request, wysle jakis event na kolejke i moze jedynie od razu odpowiedzieć że przyjął zapytanie i przetwarza. Oczywiście moznaby ustawic zaraz po wrzuceniu wiadomosci na kolejke jakiegos consumera, ktory od razu (blokujaco) poczeka na jakis konkretny event sygnalizujacy koniec procesu (coś jak wywołanie REST API ale z kolejką jako pośrednik). Ale tu jest problem bezstanowosci - bo jesli jest kilka kopii takiego serwisu to jaka jest gwarancja ze jak ten event sie pojawi to trafi akurat do tej samej instancji? (takie sticky-session, ale eventowe :P sticky-events)

Także pytanie: mieliście okazję pracować nad takim w pełni async systemem? Jak było? Jak to rozwiązaliście? Ciekawią mnie szczegóły :)

Dzięki!

1

Niestety u mnie to jest głównie wiedza teoretyczna, ale jest takie podejscie:
"Asynchronous by default, synchronous when necessary"
Komunikacja blokująca jest właśnie stosowana kiedy potrzebujemy natychmiastowej odpowiedzi czyli np. jak składamy zamówienie. Ale gdy np mamy mikroserwis odpowiedzialny za płatności, nie musi on wysyłać zapytanie HTTP informujące że na przyszedł transfer z usługi zewnętrznej w przypadku subskrypcji.
Więc przypadek gdzie jest tylko komunikacja przez kolejki sensu nie ma.

2

Ale tu jest problem bezstanowosci - bo jesli jest kilka kopii takiego serwisu to jaka jest gwarancja ze jak ten event sie pojawi to trafi akurat do tej samej instancji? (takie sticky-session, ale eventowe :P sticky-events)

To już zależy od konkretnego rozwiązania którego używasz do przesyłania eventów, ale np. w Kafce jak podzielisz kolejkę na partycje, to eventy w ramach jednej partycji mają zawsze trafiać do tego samego consumera w danej grupie consumerów. Oczywiście jak ta konkretna replika serwisu klęknie i przestanie je konsumować, to siłą rzeczy klient nie dostanie nigdy tej odpowiedzi, ale

  • przy synchronicznej komunikacji w takim scenariuszu i tak by się wszystko wywaliło
  • jak dla mnie to ogólnie stoi trochę w sprzeczności z ideą asynchronicznej komunikacji, i jak klient musi koniecznie dostać synchroniczne potwierdzenie, to już lepiej chyba zrobić to poprzez synchroniczne żądania
  • jak nie musi koniecznie dostać tego synchronicznie to nie ma po co tak kombinować
  • nawet jak musi to równie dobrze repliki serwisu mogą sobie zrzucać te eventy do jakiegoś loga w bazie/redisie/memcached, zamiast polegać bezpośrednio na consumerze, i bez znaczenia będzie kto odebrał event - zawsze będzie można podejrzeć loga. Tym bardziej że co niby consumer miałby robić z niepowiązanymi z tym konkretnie żądaniem eventami, które też mogą przyjść?

U mnie takie spojrzenie na pure asynchronous też jest zupełnie teoretyczne, aczkolwiek jestem zdania że tam gdzie to możliwe, lepiej odejść od synchronicznej komunikacji. Powód jest prosty - większa odporność na błędy, które w systemie rozproszonym są w zasadzie pewne, choćby nasz wspaniały cloud provider (bo pewnie wszystko w chmurce) obiecywał że wszystko będzie super i wszystko ma 99.9999% reliability. Synchroniczna komunikacja wymaga, by żaden uczestnik nie wywalił się po drodze, w przypadku asynchronicznej jest trochę większa tolerancja w tej kwestii (choć oczywiście nie bez kosztów, no i kolejka będzie wtedy naszym single point of failure).

0

@superdurszlak:
faktycznie - zamodelowanie tego tak, że liczba partycji w Kafce zawsze równa się liczbie consumerów faktycznie jest jakimś rozwiązaniem, ale mam wrażenie, że to nie do końca fajne bo np. ogranicza ci skalowanie jak np ruch się zwiększy (nie postawisz więcej consumerów niż masz partycji; a dodanie partycji nie jest prostą operacją [i chyba też nieodwracalną])

zastanawia mnie - jak sensownie zrobić odczyt z w pełni asynchronicznego backendu?

bo operacje write w takim modelu sa powiedzmy do zrobienia (wysłanie requestu a potem czytanie z API czy processing sie skonczyl czy nie albo jakies sockety/SSE)
natomiast jak zrobić odczyt...? (jeśli zaangażowane jest więcej niż 1 serwis na backendzie; a komunikacja przeciez przebiega async)

2
azalut napisał(a):

natomiast jak zrobić odczyt...? (jeśli zaangażowane jest więcej niż 1 serwis na backendzie; a komunikacja przeciez przebiega async)

Może jakiś serwis służący do serwowania read modelu? Ma pod ręką jakieś dane, które może se odświeżać asynchronicznie, ale już sam strzał do niego po dane jest synchroniczny (tylko odpowiedź nie jest aktualna na 100%) :)

2
azalut napisał(a):

@superdurszlak:

faktycznie - zamodelowanie tego tak, że liczba partycji w Kafce zawsze równa się liczbie consumerów

Nie jest powiedziane, że tak musi być - IIRC możesz mieć dużo więcej partycji, niż consumerów (np. 20, 100, 1000). Problem jest w drugą stronę - jak będziesz miał partycji na styk i chcesz skalować, to nagle się nie da bo tylko jeden consumer może podpiąć się pod partycję. W dodatku repartycjonowanie może być bardzo kosztowne, o ile w ogóle uda się je przeprowadzić.

0

@baant to ma swoja nazwe - CQRS(+ES) :) tylko ja mam wrażenie ze taka architektura ma sens tylko w niektorych przypadkach (wynika to z rodzaju domeny biznesowej); a zazwyczaj to po prostu massive over engineering :D

@superdurszlak no dokładnie - mogę mieć 100 partycji i 20 konsumerów - każdy wtedy dostanie po 5; ale czy robienie za dużej liczby partycji nie powoduje rowniez problemow? (przy rebalancingu, nie wiem co jeszcze?)
oczywiście mowimy tutaj o Kafce, ale inne brokery/kolejki moga dzialac na innych zasadach - czyli to troche lockowanie sie na brokera (bo sposob dzialania serwisu podytkowany jest rodzajem działania brokera/kolejki) - ale to raczej nieduzy problem

2

Architektura 100% eventowa wg. mnie nie ma praktycznego zastosowania choćby dlatego, że w przypadku np. sprawdzania dostępności musisz mieć synchronicznego calla. W ogóle nie myślałbym o tym w taki sposób, że coś jest lepsze, a inne gorsze - po prostu inne kejsy ogrywa się w inny sposób. Asynchroniczność jest trudna, nie zawsze się spłaca i nie zawsze da się zastosować.

3

synchronicznie tam gdzie ma to sens
asynchronicznie tam gdzie ma to sens

nie ma tak, że 100% w jedna czy druga strone

3

Tak robiłem taki system. tematy do poczytania to CQRS, Event Sourcing, Message bus, Task Based UI. Następnie aplikujesz to tam gdzie to ma sens i 'da się'.
Po stronie backendu wszystko łądnie można złożyć, jeśli zrobione zostało z głową i jest skalowalne i dośc odporne (chociaż to zwykle trudne tematy swoją drogą). Masz kolejki i ktoś wysyła wiadomość, ktoś(od 0 do wielu innych serwisów) ktoś odbiera, procesuje i może też wysłać swoją. Z punktu widzenia infra to są wiadmości. Z punktu widzenia architektóry to mogą być Commands i Events. Beżstanowośc? Tak, tu trzeba by wiadomości były idempotentne. Trzeba wiedzieć czy dana wiadomośc już była przeprocesowana czy nie i uwzględnić race conditions. Czyli powinna mieć swoje unikalne ID. A co do ID to jeszcze fajnie by było mieć do monitorowania itp itd collerationID i causationID też. Tutaj dużo zależy od danego rozwiązania technicznego też i platformy z której korzystasz. Co masz out of the box, a co musisz dokodzić sam. Np jeśli masz 3 instacje serwisu X, który sunbskrybuje wiadmości z danego topica, to możesz ustawić tzw round robin, i tylko jedna instancja dostanie wiadomość. Albo możesz ustawić żeby dostała każda, jeśli tak ma to działać u Ciebie. Zależy co chcesz osiągnąć. Problem się zaczyna przy unhappy path, co jeśli instacja 1 dostała, ale nie było zwrotki że przeprocesowała? Wtedy trzeba znów wysłać, ale nie wiesz czy wiadomość została przeprocesowana czy nie, gdzie jest problem. I tu właśnie idempotentnośc wiadomości wchodzi w grę, przy tych scenariuszach. Czy to rozwiązuje problem? Tak. Czy łatwo to zaodować... to zależy zwykle nie tak łatwo jak synchroniczną komunikację. Mikroserwisy mają swój koszt, dlatego nie warto ich stosować na ślepo.

Co do punktu widzenia klienta, jeśli to też jakieś API to powinno sobie z tym poradzić, tu mamy różne rozwiązania: może mieć dostęp do tego samego message brockera - i tu sprawa jest prosta. Jeśli to klient zewnętrzny - to znów specjalna widoczna na zewnątrz kolejka dla niego i gotowe. A ze starszych rozwiązań to callbacki czy tfu cykliczne odpytywanie naszego serwsiu po dane(tu warto uważąć żeby nas klienci nie DDOSowali). Nie polecam tego ostatniego.

A jeśli to jest UI?
Są miejsca że nie da się, albo będzie kosztowało więcej wysiłku niż tego jest warte (np jakiś CRUD, albo system który nie potrzebuje super wydajności i skalowalności dla miliona użytkwoników, bo będzie używany przez 30 osób w firmie - ale zwykle wtedy, jeśli chcesz zrobić tzw Task Based UI to uzytkownicy znają procesy w firmie i będzie OK jeśli będą musieli np odświeżyć stronę zeby załądować zmiany, albo pcozekać te kilka sekund na przeprocesowanie czegoś, bo wiedzą że np dodanie nowego kierowcy zajmuje systemowi 15 sekund, bo tworzy mu konta w 10 systemach a potem je aktywuje, wysyła 50 meili z gratulacjami i zaczyna zamawiać kubek z logo firmy u dostawcyy, i dopiero jak to skończy to kierowca będzie na liście).

Tak w dużym skrócie, bo napisano o tym sporo już i ja w jednym poscie tego nie skrócę sensownie:

Przede wszystkim zwykle wystarczy potwierdzenie że serwer przyjął request (np command, nie jest to teraz istotne) i procesuje. Czyli zewnętrznie dostajesz np 200 OK a wewnętrznie wiadomośc trafiła na kolejkę, i koło się kręci. A co się stanie później?

Co do UI i odświeżania, dziś są dostępne rozwiązania jak np google Firebase, które przy zmianie stanu może wysłać powiadomienie do UI, następnie ten UI może się reaktywnie odświeżyć dzięki temu. Stosując CQRS możesz użyć firebase jako bazę do odczytu. Przy Event sorcingu wtedy masz serwis procesujący stream i uaktualniający firebase. Inna opcja to jakieś streamowanie wiadomości do UI żeby ten sobie je odświeżył np przy tzw pizza trackerze. Tu też są gotowe rozwiązania któe działają lub nie z daną platformą (np nie działa na iOS albo Safari, albo gdzieś tam).
To pozwala nieco schować asynchronicznośc na poziomie UI.

Task based UI znów nie potrzebuje takich cyrków zawsze, bo wystarczy potwierdzenie że zostało przyjęte przez serwer. Przykład aukcja została zakceptowana, dostaniesz powiadomienie jak będzie aktywna i dopiero wtedy ją zobaczysz na liście aktywnych jak odświeżysz stronę.
Taki asynchroniczny system zwykle działa bardzo szybko. I o ile nie usiłujesz od razu w danej milisekundzie pokazać odświeżonej strony z aktywnymi, jest szansa że jak użytkwonik kliknie i wróci na np listę aukcji aktywnych to ona już tam będzie. Tu zależy sporo też od procesu biznesowego.

3

@azalut:
pozwolę sobie odkpować, polecam bardzo ciekawą prezentację jednego z lepszych polskich Jvmowców:

1

U nas większość operacji leci synchronicznie po RESTu, a zwłaszcza cały ruch z UI. Mamy jednak sporo kolejek kafkowych służących głównie do buforowania danych przychodzących do nas z zewnątrz. O ile ruch na UI jest dość stabilny (mamy maksymalnie kilkadziesiąt użytkowników jednocześnie zalogowanych) to te dane z zewnątrz (np nowe dane o funduszach czy nowe kontrakty) przychodzą często nagle w dużych partiach i obsługiwanie ich synchronicznie mogłoby zarżnąć jeden lub więcej mikroserwisów po drodze. Kafka używana jest też do testów integracyjnych - wrzucamy początkową wiadomość do pierwszej kolejki, to pociąga za sobą przetwarzanie w wielu mikroserwisach, ale interesują nas pewne mikroserwisy ze środka. Te mikroserwisy ze środka wrzucają wiadomości diagnostyczne na testowe kolejki, z których wczytują je testy i dzięki temu w testach można sprawdzić pośrednie kroki przetwarzania. ZTCW to w produkcyjnym kodzie nie ma żadnych prób zrobienia synchronicznej komunikacji po Kafce.

Zarejestruj się i dołącz do największej społeczności programistów w Polsce.

Otrzymaj wsparcie, dziel się wiedzą i rozwijaj swoje umiejętności z najlepszymi.