Głównym zadaniem architektury opartej o Modularny Monolit jest przygotowanie aplikacji do migrowania w stronę architektury Mikroserwisowej. Jednak zanim podzielimy aplikację na gromadkę serwisów, powinniśmy przemyśleć, w jaki sposób będziemy je ze sobą komunikować. Ponieważ nie jest to łatwe zadanie, przyjrzyjmy się potencjalnym problemom, z którymi możemy spotkać się, kiedy wybierzemy już którąś opcję.

Hej, ale dlaczego lubimy mikroserwisy?

Na początku były monolity. Potem poznaliśmy mikroserwisy, których używaliśmy chyba wszędzie. Kiedy mikroserwisy się nie sprawdziły, wróciliśmy do monolitu, tym razem z DDD. Pomimo powrotu do stabilności, ciągle gdzieś tęsknimy do mikroserwisów. Jak widać, historia lubi zataczać koło. Należy zatem zastanowić się, dlaczego tak bardzo ciągnie nas do tych serwisów.

Jednym z (poważniejszych niż hype) powodów jest oczywiście performance. Im więcej mniejszych serwisów, tym lepiej to wpływa na wydajność. Mamy wtedy mniejsze bazy danych oraz mniejsze indeksy. Dodatkowo, możemy korzystać z osobnych fizycznie serwerów dla danych każdego serwisu, co również pozwoli lepiej skupić się bazom/indeksom tylko na swoich obszarach funkcjonalnych. Szczególnie pozytywnie na wydajność wpłynie połączenie rozdzielonych wcześniej serwisów za pomocą systemów kolejkowych (o których będzie nieco więcej w dalszej części wpisu).

Drugim powodem jest bezpieczeństwo. Mowa oczywiście o bezpieczeństwie deployu. Kiedy wpuszczamy na produkcję jeden z wielu serwisów, to w razie niepowodzenia nie ciągniemy za sobą całej aplikacji. Aplikacje rozproszone dzielą się na dwa rodzaje serwisów: te, które muszą być ciągle dostępne dla swoich klientów, oraz te, które mogą pozwolić sobie na chwilową niedostępność. Te serwisy, które nie muszą być ciągle dostępne, zazwyczaj służą do takich operacji jak analiza danych, wysyłka notyfikacji, importowanie danych i podobne. Szczególnie na chwilową niedostępność mogą pozwolić sobie te serwisy, które dostają informacje z systemów kolejkowych (najwyżej wiadomości na kolejce poczekają, aż serwis będzie z powrotem dostępny).

Po trzecie: mniejsze zespoły. A skoro mniejsze zespoły, to lepsze zarządzanie, lepsze dostarczanie i częściowo łatwiejsze deploye. Dodatkowo, w takim serwisie, fizycznie odseparowujemy się od kodu pozostałych części aplikacji. Nie mamy więc problemów typu „zmiany robione przez kogoś, kto nie zna tematu”.

Dwie drogi do wyboru – API vs kolejki

Jak zatem zauważamy, mikroserwisy potrafią być potrzebne. Przejdźmy zatem do przeglądu mechanizmów, które pozwalają na komunikację serwisów między sobą.

Wybór standardowy – komunikacja przez API

Najprostszym na start mechanizmem komunikowania aplikacji ze sobą jest udostępnienie API dostępnego przez protokół HTTP. Jest to rozwiązanie, które nie wymaga od nas dodatkowych zasobów serwerowych. Dodatkowo, implementacja API najczęściej nie jest zbyt skomplikowanym przedsięwzięciem: polega to na wystawianiu endpointów, dzięki którym możemy otrzymywać informacje z pozostałych serwisów. Bardzo dobrą praktyką jest korzystanie ze standardu REST oraz bardzo semantyczne wykorzystywanie kodów odpowiedzi. Jeżeli decydujemy się na wystawienie API w obrębie serwisu, to dodatkowo powinniśmy dostarczyć wszystkim zespołom (które chcą się z nami integrować) jego dokumentację.

O wyborze drogi komunikacji decydują zazwyczaj wady rozwiązania. Przejdźmy więc do kilku problemów, z którymi należy się liczyć przy wyborze API.

Autoryzacja JWT a wpływ na bezpieczeństwo

To, że wystawione API należy zabezpieczyć przed światem, powinno być dogmatem. Istnieje wiele sposobów na autoryzowanie dostępu do zasobów. Jednym z bardzo zdobywających ostatnio popularność jest JWT, które polega na generowaniu tokenu, który składa się z trzech części: nagłówka, danych oraz hasha zabezpieczonego kluczem.

Pierwszym, na co należy zwrócić uwagę, to czas życia tokena. Pamiętajmy o tym, aby był on nie-za-długi. Drugim, co również może być groźne, to same informacje, które udostępniamy w tym tokenie. Nie należy tam zamieszczać danych wrażliwych, takich jak nazwy użytkownika, hasła, zbędne identyfikatory itp. Generalizując: im mniej tam wsadzimy, tym lepiej 🙂 Informacje, które w tym miejscu zamieścimy powinny służyć jedynie poprawnej pracy aplikacji. Warto więc wrzucić tam np. informację o autoryzowanych poziomach dostępu.

Duży ruch = wynajęcie Load Balancera

Komunikacja przez API działa w oparciu o protokół HTTP. Im większy mamy ruch w aplikacji, tym więcej równoległych żądań może przybywać do naszego serwisu. Samotny serwer obsługujący ruch, prędzej czy później nie podoła. Dobrą opcją będzie więc śledzenie obciążenia serwerów i dołożenia kolejnych, które spięte będą przez Load Balancer – dodatkowy serwer, który będzie przekazywał żądania do najmniej obciążonych w danej chwili jednostek.

Uwaga na 500ki!

Chyba największą wadą API są 500ki. Kiedy stanie się coś zupełnie nieprzewidzianego przez programistę, to serwer wyrzuca błąd, co równa się najczęściej z utratą żądania. Jeżeli klient API jest ogarnięty, to powinien wyłapać tą sytuację i próbować ponownie uderzyć pod endpoint, lecz z reguły nie powinniśmy na to liczyć.

Bardzo ciekawa opcja – GraphQL

Jeżeli do naszego serwisu komunikuje się kilka innych serwisów, to mogą mieć one nieco inne potrzeby. Jeden z nich może potrzebować rozległych informacji tekstowych o zasobach. Drugi natomiast może potrzebować jedynie powierzchownych informacji takich jak nazwa, cena oraz ilość. Trzeci może potrzebować z kolei wyłącznie informacje o zdjęciach. W takiej sytuacji warto rozważyć przejście ze standardu REST na GraphQL, który de facto jest w pewnym sensie jego odwrotnością. Różnica między jednym oraz drugim polega na tym, że w REST to serwis wystawiający API decyduje o tym, co zostanie zwrócone. W GraphQL to klient decyduje, jakich informacji oczekuje w zwrotce od API.

Wprowadzenie standardu GraphQL może mieć również pozytywny wpływ na performance; Im krótsze odpowiedzi będą zwracane przez serwer, tym większy ruch może potencjalnie zostać obsłużony przez serwer HTTP.

Wybór niestandardowy – systemy kolejkowe

Drugim, na pierwszy rzut oka bardziej stabilnym sposobem na komunikację w architekturze mikroserwisowej, jest połączenie przez system kolejkowy. W przeciwieństwie do API – już na starcie potrzebujemy dodatkowe zasoby, na których łamach operować będzie system kolejek. Jako system kolejkowy definiujemy program-serwer, który przez ustalony wewnętrznie protokół odbiera wiadomości, decydując o tym, w które miejsce powinna zostać wysłana. Wiadomości są organizowane w strukturę kolejek, które następnie są procesowane przez wolne zasobowo procesy.

Najważniejszą zaletą korzystania z systemów kolejkowych jest przede wszystkim bardzo pozytywny wpływ na performance aplikacji. To, czego w tym miejscu zapomina się dodać, jest zdanie: Kosztem ewentualnych opóźnień.

Praca na systemach kolejkowych, podobnie jak praca na API, ma swoje wady, o których nie należy zapominać w momencie decyzyjnym.

Uwaga na brak kontekstu HTTP!

Kontener zależności potrafi być błogosławieństwem, ale i przekleństwem. Dostępność „wszystkiego wszędzie” kusi, szczególnie młodszych programistów, aby wstrzykiwać serwisy typu EntityManager czy RequestStack dosłownie wszędzie. Nie jest to dobra praktyka, szczególnie, kiedy mówimy o tym drugim. Wstrzykując RequestStack do zwykłego serwisu aplikacyjnego, prędzej czy później zapomnimy o tym, że tracimy możliwość używania tego serwisu poza kontekstem HTTP. Następnie (najczęściej pośrednio) korzystamy z tego serwisu w procesie consumera, który działa w kontekście CLI. Bum! Mamy buga. Czymś, co stanowczo pomoże nam w zapobiegnięciu tej sytuacji jest poprawne odseparowanie warstw w aplikacji.

Wiele consumerów = dodatkowe komplikacje

Mózg ludzki lubi pracować w sposób zorganizowany i liniowy. Kiedy myślimy o systemie kolejkowym, to zazwyczaj w głowie mamy pracę na tylko jednym consumerze per kolejka – przecież nie będzie potrzeby dokładania kolejnych. A jak będziemy chcieli dołożyć, to najwyżej popracujemy trochę nad kodem, zanim go dorzucimy. Następnie nadchodzi ten dzień, w którym dowiadujemy się, że dział marketingowy niezapowiedzianie zrobił dużą kampanię i serwery muszą się wyrobić. Dorzucamy więc consumery i modlimy się, aby wszystko było okej. I niestety okej nie jest. W bazie mamy sporo logów o tym, że zasób (a raczej cała masa zasobów) o identyfikatorze X nie istnieje.

Powrotem takiego zachowania aplikacji jest to, że dwie zależne od siebie wiadomości zostały wykonane w sposób asynchroniczny, przez dwa różne consumery. Najpierw przetworzona została operacja „Dorzuć coś do zasobu X” na consumerze nr 2, a następnie wykonana została wiadomość „Utwórz zasób X” na consumerze nr 1. Jest to klasyczny problem, z którym należy zmierzyć się już na etapie projektowania architektury. Pomimo przynależności do klasyki gatunku, nie są to jednak proste do implementacji tematy.

Pada kolejka – tracimy wszystko!

Każdy system potrafi być zawodny. Każda aplikacja potrafi się wysypać z bliżej nieokreślonego powodu. Powinniśmy zatem zabezpieczyć się szczególnie przed nieoczekiwanym wysypaniem się aplikacji pełniącej rolę serwera systemu kolejkowego. Jeżeli wiadomości przechowywane są wyłącznie w pamięci (a często tak jest), to przy padnięciu procesu wszystko gubimy. W aplikacjach biznesowych każda wiadomość wygenerowana w systemie ma swój konkretny cel, zatem nie powinniśmy brać pod uwagę możliwości jej stracenia.

Rozwiązaniem tego problemu może być wystawienie dodatkowego consumera, który służyłby jedynie logowaniu wiadomości przychodzących, które następnie możemy ponownie wrzucić na kolejkę w razie twardego resetu. Dodatkow, możemy zainteresować się tym, czy używany przez nas system kolejkowy wspiera zapis wiadomości na twardym dysku.

Dążąc do podsumowania

Zarówno opcja związana z API jak i systemami kolejkowymi ma swój zestaw zalet oraz wad. Powinniśmy być świadomi tego, że nie ma rozwiązania idealnego oraz, że już na starcie etapu projektowania musimy zrobić porządny research, aby nie zostać zaskoczonym w chwili prawdy 🙂

Profesjonalista, zajmujący się na co dzień aplikacjami biznesowymi w ekosystemie PHP. Jego pasją jest odkrywanie nowych konceptów programistycznych oraz wzorce architektoniczne. Uwielbia również pisać testy, gdyż jak sam uważa, dobry kod to przetestowany kod.

Comments are closed.