Mikroserwisy muszą być wspomniane przynajmniej raz na kilka artykułów, podobno w innym wypadku blog jest uznawany za archaiczny. W ostatni dzień lutego serdecznie Was zapraszam do przeczytania tego wyjątkowego wpisu. Dlaczego jest specjalny? Gdyż po raz pierwszy będę opisywał moją opinię na dany temat, zamiast skupiać się na konkretnym zagadnieniu technicznym. Dlatego już na wstępie bardzo gorąco zapraszam do dyskusji, gdyż ciekawi mnie, jaka będzie Wasza opinia na ten temat. Niemniej, siadajcie wygodnie, zapnijcie pasy – witam Was w kolejnym wpisie w Cesarstwie-Dev!

W trakcie mojej pracy programistycznej miałem przyjemność pracować przy kilku projektach opartych na architekturze mikroserwisowej. Dzięki temu zauważyłem, co najbardziej pomagało przy projektowaniu i implementacji takich rozwiązań. Mikroserwisy, które korzystały z tradycyjnego modelu „request-driven” w mojej opinii w wielu polach przegrywają rywalizację z systemami „event-driven”. Mam nadzieję, że w tym wpisie Was do tego przekonam!

Request-driven a event-driven

Przed wytłumaczeniem powyższych nazw, w wielkim skrócie powiedzmy sobie czym są mikroserwisy. Jest to architektura, w której zapominamy, że sieć jest zawodna i powolna, a niektóre serwery chwilowo niedostępne. Jest to architektura, która pozwala nam wdrażać niezależne, skalowalne i odporne na awarie serwisy, gdzie każdy z nich spełnia sprecyzowaną funkcjonalność oraz może być właścicielem konkretnych informacji (a każda informacja, może mieć wyłącznie jednego właściciela). W powyższym zdaniu kluczowe jest, że mikroserwisy pozwalają nam osiągnąć te rzeczy, jednak żadnej z nich nie zapewniają – zapewnić musimy je sami.

are you ready for microservices? geek comic
https://turnoff.us/geek/are-you-ready-for-microservices/

Implementacja systemów mikroserwisowych wnosi wiele wyzwań, o których nie musimy myśleć przy monilitach – zcentralizowany system logowania, korelacja żądań, warstwy zapobiegające uszkodzeniu i wiele innych. Powyższy obrazek, poza oczywistą wartością humorystyczną, jest również dobrym początkiem na dyskusje o modelu request-driven. Polega on na niczym innym, jak synchronicznej komunikacji pomiędzy różnymi usługami (request-response). Usługa A wysyła żądanie (np. poprzez http) do usługi B. Usługa B to żądanie przetwarza i zwraca odpowiedź usłudze A. Co jeśli usługa B aktualnie nie działa (bez względu na przyczynę)? Wtedy użytkownik naszej aplikacji nie może wykonać funkcjonalności, którą miała zapewnić usługa A. Nie zostaje nic innego, jak rozpłakać się i zawołać architekta systemu.

Alternatywą jest tu komunikacja asynchroniczna, która zwykle oparta jest na zdarzeniach. W takiej sytuacji to klient B wysyła na pewną szynę (czy to system kolejkowy, czy tabela SQL, czy nawet pamięć, jeśli jesteśmy w systemie monolitycznym) pewne zdarzenie (np. UtworzonoKlienta), a klient A tego zdarzenia nasłuchuje i odpowiednio aktualizuje potrzebne dane. Takie podejście można określić jako event-driven. W jednym wpisie opisałem zwięźle czym jest event-driven architecture, więc jeśli chcesz się z tym zapoznać, to otwórz ten link w nowej karcie i sprawdź go później!

Odporność na awarię

Pierwszy problem, który rozwiązuje komunikacja asynchroniczna, poruszyłem w poprzednim akapicie – odporność na awarie. Zobaczmy przykładowe sekwencje, przedstawiające komunikację synchroniczną oraz asynchroniczną.

Jak widać z powyższego wykresu, komunikacja asynchroniczna wspomaga odporność na awarię. Mimo chwilowego braku dostępności usługi Shipment użytkownik nic nie zauważył, a system ostatecznie osiągnął stan spójny. Dobór słów jest nieprzypadkowy. Implementując komunikację asynchroniczną w architekturze mikroserwisowej, musimy się godzić ze spójnością ostateczną. Czy zawsze jest to dopuszczalne? Pewnie nie – o to musimy pytać interesariuszy. Pamiętajmy tutaj, że lepiej nie zadawać pytania „czy system może być przez jakiś czas niespójny”. Lepiej spytać konkretniej: „czy czas oczekiwania na potwierdzenie dostawy może wynosić do kilku minut”.

W tym miejscu uważni czytelnicy mogą zauważyć, że zamiast Shipment’u to sam system kolejkowy mógł być niedostępny. Co prawda obecnie wykorzystywane systemy takie jak Kafka czy RabbitMQ cechują się wysoką dostępnością, to przed tym warto zabezpieczyć się programistycznie. W tym celu możemy zastosować wzorce projektowe, takie jak Outbox i Inbox. W skrócie polegają na tym, aby przy zapisie agregatu do odpowiedniego systemu persytencji, w tej samej transakcji zapisać również zdarzenia. Zdarzenia te są następnie odczytywane przez pewien proces, wysyłane na system kolejkowy oraz aktualizowane w bazie. Jeśli wysłanie się nie powiodło, to nie zostaną one zaktualizowane w bazie. Dzięki temu mamy pewność, że zostaną dostarczone przynajmniej raz. Odbiorcy powinni wtedy zapewnić, że każde żądanie przetworzą wyłącznie raz – dzięki temu osiągamy exactly-once delivery. Szeroko ten temat poruszył Oskar Dudycz w swoim blogu.

Mikroserwisy a wydajność

Implementując mikroserwisy często zapominamy o narzucie, jaki na czas wykonywania zapytań nakłada sieć. Warto co jakiś czas przypominać sobie o pewnych błędach poznawczych, które dotykają nas przy projektowaniu systemu rozproszonego. Osobiście polecam również naklejenie ich na ścianie w biurze! Opierając się na komunikacji synchronicznej, często traktujemy wywołania zdalne jak wywołania lokalnej metody. Nieraz w końcu słyszeliśmy, że mikroserwisy świetnie się skalują, więc od razu poprawią wydajność naszych aplikacji!

Załóżmy, że mamy trzy moduły (A, B i C). Moduł A do zapewniania swojej funkcjonalności, potrzebuje pewnego podzbioru danych z modułu B, a ten z kolei potrzebuje modułu C. Przy komunikacji synchronicznej tworzymy pewien łańcuch, przez który muszą przejść pakiety sieciowe. Takie łańcuchy pogarszają wydajność naszych aplikacji, jak i zmniejszają ich dostępność – co było poruszone w poprzednim akapicie. Co w takim wypadku dają nam zdarzenia? To bardzo proste! Moduł A nasłuchuje potrzebnych sobie zdarzeń z modułów B i C, by odpowiednio aktualizować kopie tych danych, które trzyma u siebie. Możemy to traktować jako cache pozwalający na szybki dostęp do danych. Dzięki takiego podejściu zyskujemy co najmniej trzy rzeczy.

  • Zmniejszamy coupling między modułami.
  • Zwiększamy odporność na awarię
  • Zwiększamy wydajność

Oczywiście wiąże się to między innymi ze spójnością ostateczną, gdzie nie zawsze jest to możliwe. W takich sytuacjach musimy zostać przy tradycyjnym modelu request-response.

Znajdź oszusta

Jest pewna zasada programistyczna, którą wydajemy się naruszać, stosując poradę z poprzedniego akapitu.

Jak pewnie się domyślasz, chodzi o DRY – Don’t repeat yourself. Pewnie też zastanawiasz się co oznacza SLAP. Jest to jeden z mniej znanych skrótów – kto by pomyślał, że do kiss dojdzie nam slap. Oznacza to Single Level of Abstraction Principle – funkcje powinny wykonywać tylko jedną rzecz na jednym poziomie abstrakcji. Wracając jednak do tematu! Oczywiście zasada DRY jest ważną regułą, którą powinniśmy starać się praktykować. Czasami jednak widzę, że jest ona brana do serca zbyt dosłownie. W oryginale brzmiała ona tak.

Every piece of knowledge must have a single, unambiguous, authoritative representation within a system

Andy Hunt, Dave Thomas – The pragmatic programmer

Jest to związane z tym, co napisałem w pierwszym akapicie. Każda informacja jest jednoznacznie przypisana do konkretnego mikroserwisu. Tylko ten mikroserwis posiada zawsze aktualne dane, pilnuje zasad biznesowych, udziela informacji o zmianach stanu agregatów, które są w jego posiadaniu. Duplikacja tych danych w wielu usługach nie powinna poruszać naszych sumień, gdyż nasz system wciąż posiada wyłącznie jedno źródło prawdy o konkretnym zagadnieniu. Trzymanie tych danych po stronie usług postronnych musimy traktować jak zwykły cache.

Dodawanie funkcjonalności

Ostatnią kwestią, którą dzisiaj poruszę, jest dodawanie nowych funkcjonalności. Na konferencjach często słyszymy, że mikroserwisy pozwalają nam po prostu dodać nową usługę i mamy to! Przyjrzyjmy się pewnemu przykładowi. Załóżmy, że mamy moduły Order oraz Shipment. Chcemy dodać nowy moduł – Loyalty program. Zajmuje się on przydzielaniem odpowiednich bonusów dla użytkowników, którzy często korzystają z systemu. Rozważmy komunikację synchroniczną oraz komunikację opartą na zdarzeniach.

Posługując się tradycyjnym modelem request-driven dodanie nowego modułu wymusza zmiany w module Order. W tym miejscu dochodzi dodatkowe żądanie do nowego modułu, co zwiększa podatność na awarię i zmniejsza wydajność. Rozwiązanie ze zdarzeniami zapewnia nam elastyczność w dodawaniu nowych funkcjonalności. W takim wypadku nasz moduł subskrybuje zdarzenia OrderCreated, dzięki czemu od razu po wdrożeniu może rozpocząć swoje działanie – niezależnie od modułu Order.

Podsumowanie

Jeśli miałbym podsumować ten wpis w jednym zdaniu, to powiedziałbym: gdy to możliwe, stosuj komunikację opartą na zdarzeniach. Oczywiście, stoi za tym dużo więcej niż sam sposób komunikacji – musimy zapewnić sposób ich transportu, zagwarantować dostarczenie, zaprojektować domenę w odpowiedni sposób. Projektowanie systemu opartego na zdarzeniach daje nam wiele innych zalet – zmiejsza coupling, ułatwia komunikacje z biznesem, zwiększa elastyczność, czy pozwala łatwo stworzyć read-modele. Co więcej, uważam, że warto stosować zdarzenia również w aplikacjach monolitycznych. Ułatwia to komunikacje między modułami bez bezpośrednich zależności do nich, co pozwala na proste przeniesienie aplikacji monolitowej do architektury mikroserwisowej.

I na tym zakończę ten wpis. Mam nadzieję, że każdy mógł z niego wynieść coś interesującego. Jakie jest Wasze zdanie na ten temat? Niezależnie, ja Wam bardzo dziękuję za przeczytanie tego wpisu i życzę miłego dnia!


3 Komentarze

Leszek · 2021-03-31 o 15:28

Dzieki.

Patryk · 2021-03-29 o 19:31

Mega wartosciowy artykuł! Wiele osób nie ma pojecia o dobrych nawykach zwiazanych z architektura micro, tworząc przy tym duzy http coupling.

Niestety, ale jedyna forma odpytań http / http2 powinna isc z poziomu gateway -> service.

dotnetomaniak.pl · 2021-03-26 o 12:19

Dlaczego Twoje mikroserwisy potrzebują zdarzeń?

Dziękujemy za dodanie artykułu – Trackback z dotnetomaniak.pl

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *