Budując system oparty o mikroserwisy, musisz ze sobą jakoś skomunikować poszczególne usługi. No nie ma siły, nie domyślą się, co mają robić. W tym momencie należałoby się zatrzymać i zaprojektować komunikację.
No i tu zwykle pojawia się problem.
Niestety, z jakiegoś dziwnego powodu, w głowach większości developerów pojawia się myśl: “wystawmy RESTa”.
Dokładnie tak, wystawmy RESTa, ktoś tam do niego strzeli i pobierze dane lub wywoła coś. Później jakieś dogadywanie API, jaki endpoint, jakie parametry, jaki payload i tyle. Temat projektowania zamknięty, można kodować.
Po pewnym czasie, zależnym od doświadczenia teamu, pojawiają się problemy na środowiskach.
Takie standardowe technikalia jak:
- problem z autoryzacją,
- problem z formatem danych,
- nie ten host, nie ten path,
- błędny format nagłówków,
- źle interpretowany query param.
Oraz mniej standardowe:
- wysycenie liczby połączeń,
- bardzo duże opóźnienia,
- łańcuch awarii, bo gdzieś jakiś serwis przestał działać na chwilę.
No i o ile te standardowe, to chleb powszedni, wynikający raczej z niedojrzałego procesu wytwarzania oprogramowania, czyli np. brak kontraktów, brak odpowiednich testów itp. To te drugie, to już są poważniejsze problemy natury architektonicznej.
Temporal Coupling
Podstawową i najpoważniejszą wadą komunikacji REST jest to, iż jest to komunikacja synchroniczna. Czyli obie strony “rozmowy” muszą być w dokładnie tym samym momencie w pełni sprawne. Jest to tzw. Temporal Coupling, czyli powiązanie czasowe.
Jeżeli cały swój system oprzesz na tego rodzaju komunikacji, to praktycznie cały Twój system będzie związany czasowy, czyli awaria dowolnego komponentu będzie sprawiała, iż cały system przestanie działać. A już na pewno całe ścieżki biznesowe, które w jakikolwiek sposób z tego komponentu korzystały. Oczywiście można sobie z tym poradzić. Istnieją takie wzorce jak Circuit Breaker, czyli bezpieczniki (ta sama zasada co korki w domu), które starają się mitygować ten problem. Działają one bardzo prosto.
Wszystko jest po stronie klienta, który cały czas monitoruje swoje wychodzące żądania. W przypadku wykrycia błędu, w zależności od jego rodzaju, nasz bezpiecznik może uznać, iż druga strona ma awarie, więc nie ma sensu pchać tam następnych żądań – więc otwiera obwód (czyli blokuje cały ruch wychodzący).
W tym momencie w grę wchodzą scenariusze awaryjne, czyli fallback. To w nich musisz oprogramować, co ma się wydarzyć w takiej sytuacji. Możesz zwrócić błąd (czyli fail-fast), możesz zwrócić starsze dane z cache, możesz zwrócić jakieś domyślne dane. Musisz jakoś zaadresować tę sytuację.
Taki bezpiecznik co jakiś czas sprawdza, czy awaria ustąpiła, próbnie przepuszczając mały procent ruchu. o ile stwierdzi, iż jest ok, obwód jest zamykany i komunikaty mogą ponownie przepływać.
Trochę skomplikowane, co? Tak wiem, biblioteki pomagają, ale jest to dodatkowy “ruchomy” element.
To nie jedyny problem…
Service Discovery
Kolejna kwestia to ogarnięcie wielu instancji usług do których chcemy strzelać. zwykle docelowy serwis jest uruchomiony w wielu instancjach, każda z instancji może mieć inne ip i inny port na którym nasłuchuje. Co gorsza, mogą się one bardzo dynamicznie zmieniać, a nasz klient musi za tym nadążyć.
Mamy na szczęście różne mechanizmy tzw. Service Discovery, które starają się nam z tym pomóc.
Możesz spotkać rozwiązania, które pozwalają posługiwać się nazwami usług:
- oparte o Service Mesh, w którym dodatkowe komponenty (kontenery) dbają o odpowiedni routing,
- operte o obiekty Service w Kubernetesie, gdzie to właśnie Kubernetes, zajmuje się pilnowaniem wszystkiego za nas,
- rozwiązania Client-side, gdzie nasz serwis musi się skomunikować z infrastrukturą service discovery.
Nie jest to straszne, ale to kolejny element o którym musisz pamiętać.
Połączenia
No i kolejna kwestia, wiesz iż za każdym razem, kiedy chcesz wykonać zapytanie REST to musisz najpierw nawiązać połączenie? Na pewno wiesz, ale o tym się bardzo często zapomina. Umyka nam ta kwestia. Każde nawiązanie połączenia to drogocenny czas. Nie ma oczywiście wielkiego problemu, o ile wydajność nie jest kluczowym czynnikiem u Ciebie, jednak zdarzają się sytuację, iż jednak jest.
Co więcej, im więcej serwisów bierze udział w realizacji danego procesu biznesowego, tym dłuższy łańcuch wywołań REST nam się tworzy, i te minimalne opóźnienia na nawiązanie połączenia zaczynają się sumować i rosnąć. Serwisy bliżej “powierzchni” mają zajęte zasoby i czekają, aż te z “głębszych” warstw systemu ogarną się i zwrócą nam żądane wartości.
Jak do tego dodasz jeszcze komunikację po HTTPS, to jeszcze dochodzi czas na weryfikacje certyfikatów…
To jak zrobić komunikację?
Prawda jest taka, iż w większości przypadków, nie potrzebujesz synchronicznej komunikacji. Tak nam się tylko wydaje, iż jej potrzebujemy, bo jest ona wygodna! Łatwo ją zrozumieć, ogarnąć cały proces, jest request, jest response lub błąd. Prosta sprawa. Z komunikacją asynchroniczną jest trochę więcej zabawy.
Kiedy zatrzymamy się na chwilę i przeanalizujemy dostępne opcje, okaże się, iż w większości przypadków możemy zastosować właśnie asynca.
Command
Każde zlecenie wykonania operacji to tak naprawdę Command, nie potrzebujesz tutaj mieć od razu odpowiedzi o wyniku.
Event
Jeżeli chcesz się dowiedzieć o wyniku operacji, to możesz wykorzystać Eventy, które informują nie tylko Ciebie, ale wszystkich zainteresowanych.
Query?
Jedynie można się zastanawiać nad pobieraniem danych, gdzie operacje GET wydają się super opcją. Jednak też nie zawsze potrzebujesz mieć najbardziej aktualne dane. Bazując na takim podejściu, wprowadzasz temporal coupling, a możesz przecież budować sobie lokalnie cache na potrzebne Ci dane, na podstawie Eventów, które otrzymujesz.
Nie jest to przeszkoda. Zwiększasz zapotrzebowanie na storage, ale kolosalnie zmniejszasz coupling pomiędzy usługami, a w zdecydowanej większości przypadków nie potrzebujesz mieć danych super aktualnych, z dokładnie tej konkretnej milisekundy.
Tak naprawdę, REST ma najwięcej sensu tam, gdzie wystawiamy API naszego systemu na zewnątrz. Czyli jakieś API integracyjne, czy też API dla frontendu. Wykorzystanie go wewnątrz systemu wprowadza wiele ograniczeń i potencjalnych silnych powiązań między usługami.