Komunikacja między mikroserwisami

Jednym z pierwszych wyzwań stojących przed mikroserwisami jest wybór sposobu komunikacji.

photo-1475691058852-37b5d9b6a878

Wybór rodzaju interakcji

Wybierając odpowiedni w danym przypadku styl interakcji musimy zbadać jakiego rodzaju komunikacją mamy do czynienia. Mamy tutaj do wyboru:

  1. Jeden do jednego – inicjator wykonuje żądanie, które jest przetwarzane przez dokładnie jeden serwis odbierający żądanie
  2. Jeden do wielu – w tym modelu wiadomość może być przetwarzana przez wiele serwisów odbierających (w szczególnym wypadku może być to jeden lub zero serwisów). Zazwyczaj dla serwisu inicjującego konwersacje, będzie transparentne to ile serwisów przetwarza to żądanie

Styl interakcji

Wybór stylu interakcji sprowadza się do pytania czy komunikacja ma charakter synchroniczny czy asynchroniczny:

  1. Komunikacja synchroniczna – inicjator czeka na odpowiedź
  2. Komunikacja asynchroniczna – inicjator nie czeka na odpowiedź

tony-stoddard-DgZxBnZeQo8-unsplashsource:Photo by Tony Stoddard on Unsplash

Najpopularniejszymi wzorcami interakcji są:

  1. Odpal i zapomnij (ang. fire-and-forget) – inicjator konwersacji wysyła wiadomość, która dociera do odbiorcy. I to w zasadzie tyle. Ciężko sobie wyobraźić prostszy schemat konwersacji.2017-07-23 18_22_11-BlogGD - microservices pt3.png - Paint
  2. Zapytanie/odpowiedź – najczęściej stosowana metoda interakcji. Strona inicjująca odpytuje drugą i czeka na odpowiedź, która powinna pojawić się w określonym czasie.2017-07-23 18_24_32-BlogGD - microservices pt3.png - Paint
  3. Zapytanie/asynchroniczna odpowiedź – polega na wysłaniu zapytania przez klienta do serwisu, który odpowiada asynchronicznie. Jakie ma to implikacje? Otóż klient może w sposób nieblokujący czekać na odpowiedź. Nic nie stoi na przeszkodzie by w tym czasie robić inne obliczenia. Do klienta należy obsługa całkiem prawdopodobnej sytuacji, iż takiej odpowiedź nie dojdzie.2017-07-23 18_24_43-BlogGD - microservices pt3.png - Paint
  4. Publikacja/subskrypcja – przesyłanie wiadomości polega tutaj na udostępnianiu informacji przez publikatorów, a ich odbiorcami są wszystkie jednostki w systemie, które są zainteresowane danym komunikatem.2017-07-23 18_24_55-BlogGD - microservices pt3.png - Paint

Technikalia

Posiadając do wyboru kilka możliwych wzorców interakcji musimy zdecydować się jeszcze na techniczne szczegóły co do sposobu w jaki taka interakcja zostanie zrealizowana.

By zapewnić jak najlepszą niezawodność systemu przy wysokiej wydajności i skalowalności, najczęściej najlepszym wyborem będzie kombinacja technologii komunikacji. W różnych miejscach systemu możemy zastosować różne sposoby komunikacji:

  1. HTTP/REST API
  2. Wiadomości (messaging)
  3. Inny, specyficzny dla domeny protokół komunikacji

W zależności od tego który ze sposobów komunikacji wybierzemy musimy liczyć się z konsekwencjami. I tak:

1. HTTP/REST API

pavan-trikutam-71CjSSB83Wo-unsplashSource:Photo by Pavan Trikutam on Unsplash

Szybki, elastyczny sposób na implementację komunikacji pomiędzy serwisami. Dla większości programistów będzie to także po prostu najprostsze w użyciu narzędzie. Często wykorzystywany do wykonywania zapytań (w odróżnieniu do komend). Dużym plusem jest brak narzutu co do konkretnej technologii. Serwisy wykorzystujące komunikację poprzez HTTP/REST API mogą korzystać z szerokiej gamy rozwiązań. Co najważniejsze, nie ma problemu by po obu stronach komunikacji występowały całkowicie różne technologie.

Ta technologia, w najprostszej wersji, charakteryzuje się synchronicznością działania co w zależności od kontekstu będzie zaletą bądź też wadą. Dzięki powszechnej znajomości tego rozwiązania zazwyczaj nie będzie problemu z zastosowaniem tego rozwiązania. Definitywnym minusem tego rozwiązania jest wrażliwość na problemy z dostępem do sieci, w chwili wykonywania zapytania obie strony interakcji muszą działać. Inną wadą jest wymaganie znajomości adresu serwisu przez klienta. Intuicyjnie może to nie wydawać się jak duży problem, aczkolwiek skalowanie systemu wymaga od nas by klient mógł i wiedział jak może połączyć się z wszystkimi instancjami danego serwisu.

2. Wiadomości (messaging)

andrew-buchanan-2EdeFVe8TOE-unsplash

Wymiana informacji pomiędzy serwisami za pomocą wiadomości to świetny sposób na rozluźnienie powiązania pomiędzy nimi. Ta, z natury asynchroniczna, komunikacja polega na tym, że jedna ze stron komunikacji zapisuje wiadomość, natomiast druga ją odczytuje. Dobrze to współgra z wzorcami architektonicznymi takimi jak CQRS i EDD (Event-Driven Development). Decydując się na ten sposób komunikacji, musimy podjąć decyzję jakiej technologii użyjemy. Przykładowymi, popularnymi rozwiązaniami są: Apache Kafka, AWS Kinesis Streams, Azure Service Bus, RabbitMQ, NServiceBus, MassTransit, MSMQ. Wspomniane rozwiązania różnią się znacznie podejściem do architektury systemu, poziomem abstrakcji i sam opis tychże różnic to spory temat. Dodatkowo mamy tutaj spory wachlarz wzorców komunikacji jak np. : publikacja/subskrypcja czy też zapytanie/odpowiedź. Możemy tutaj uzyskać wysoką skalowalność jak i wysoką dostępność systemu za cenę dodatowej złożoności systemu spowodowanej wykorzystaniem infrastruktury umożliwiającej komunikację przez wiadomości.

3. Specyficzny protokół komunikacji taki przesył danych binarnych, JSON czy też XML.

Czasami sytuacja zmusza lub też zachęca nas do użycia jakiegoś innego formatu danych. Często jest to uwarunkowane specyficznymi wymaganiami w konkretnej domenie i umożliwia to np. wydajny streaming multimediów.

Komunikacja między serwisami wymaga od nas znajomości charakteru interakcji co potem wpływa na wybór technologii. Każda z tych decyzji niesie za sobą zalety ale i wady, ale mając ich świadomość jesteśmy w stanie zbudować system, który w wydajny sposób się komunikuje i wykorzystuje infrastrukturę by uzyskać zarówno wysoką dostępność jak i niezawodność.

Bibliografia i inne ciekawe materiały:

Mikroserwisy – API gateway, czyli drzwi wejściowe do naszego systemu

Zapomnijmy na chwilę o mikroserwisach pomimo, że mowa o nich w tytule. Wyobraźmy sobie serwer udostępniający dane o użytkownikach pewnego portalu społecznościowego. Niech będą to szczegółowe dane takie jak imię, nazwisko, ale i ostatnie aktywności na portalu czy też opublikowane posty. Taki serwer udostępnia pewnie jakieś API do pobierania tych informacji zgromadzonych w bazie danych. Powiedzmy, że mamy metodę GetPerson(), która to wykorzystując warstwę aplikacyjną odpytuje repozytoria o dane osoby z innej tabeli wczytujemy ostatnie aktywności a jeszcze z innej ostatnie posty użytkownika. W tej sytuacji aplikacyjna kliencka odpytuje nasze API, odpowiednio prezentuje dane i mamy to.

(źródło: https://docs.microsoft.com/pl-pl/dotnet/architecture/microservices/architect-microservice-container-applications/direct-client-to-microservice-communication-versus-the-api-gateway-pattern)

Problemy rozproszonej lokalizacji zasobów

Wróćmy do mikroserwisów. Sytuacja nieco komplikuje się, bo dane interesujące aplikacje klienckie są rozsiane po różnych zakamarkach systemu. W ostateczności moglibyśmy w końcu dociec gdzie co jest zlokalizowane, w jakim formacie musimy odpytać dany mikroserwis (albo zmusić wszystkie do wystawiania Rest API itd.), ale rodzi to wiele problemów:

  1. Co z uwierzytelnianiem? Czy każdy mikroserwis musi się tym przejmować?
  2. Jak zminimalizować wpływ zmian w strukturze i funkcjonalności mikroserwisów na działanie klientów?
  3. Co ze skalowaniem? Co jeżeli zwiększamy liczbę instancji danego serwisu? Kto decyduje o adresie docelowym zapytania?
  4. Załóżmy, że zmieniamy odpowiedzialności mikroserwisów, wprowadzamy nowe, usuwamy stare. Jakkolwiek zmieniamy wewnętrzną strukturę systemu. Jak wyeliminować zmiany po stronie klientów API?

Rozwiązanie

Jak można się tego spodziewać istnieje rozwiązanie, które upraszcza większość z problemów, które mogą się pojawić w kontekście interakcji między klientem a serwerem w architekturze mikroserwisów. Możemy wprowadzić dodatkowy komponent pośredni między warstwą aplikcji, a aplikacją kliencką, a jest nim API Gateway.

API Gateway to pewien rodzaj reverse proxy. Jego rolą jest odizolowanie reszty mikroserwisów od kwesti związanych z obsługą zapytań z aplikacji klienckich, a więc będzie on przekierowywał zapytania z klienta do odpowiednich serwisów. Odnosząc się do analogii z tytułu posta, API Gateway to drzwi wejściowe do naszego systemu. Z punku widzenia aplikacji klienckiej wszystko za tymi drzwiami jest ukryte i jeżeli nie podamy jej czegoś przez te drzwi to tego nie dostanie. Nie pozwalamy na wejście oknami czy też przez balkon.

Zalety

Co dzięki temu zyskujemy?

  1. Klient nie troszczy się ani o lokalizację mikroserwisów, ani o to w którym mikroserwisie zlokalizowany jest interesujący go zasób, ani o to ile instancji danego mikroserwisu jest dostęne, ani o to czy zmienimy strukturę mikroserwisów, ba, on nie troszczy się o to czy ma doczynienia z mikroserwisami w ogóle!
  2. Umożliwiamy odpytywanie systemu przez wygodne API, które może być dostosowanie API pod konkretnych klientów, bez konieczności implementacji tego w każdym mikroserwisie.
  3. Mamy tylko jeden punkt w którym uwierzytelniamy użytkownika.
  4. Możemy łączyć, filtrować, sortować i w różny sposób manipulować danymi w taki sposób by dostosować je do oczekiwań aplikacji klienckiej.
  5. Pozwalamy mikroserwisom rozmawiać między sobą w formacie, który jest dla nich wygodny, ponieważ nie ma to wpływu na klienta
  6. Możemy wersjonować API wystawione przez nasz Gateway w jeden sposób, a API wewnętrzne wykorzystywane przez mikroserwisy w naszym systemie w całkowicie inny sposób.
  7. Łatwiejsze zastosowanie load balancingu. W specyficznym przypadku API Gateway może być load balancerem.
  8. Łatwiej zastosować blue/green deployment. Dzięki Gateway’owi po deployu możemy przekierować ruch ze starych do nowych serwisów i staje się to bajecznie proste.

Wady

Jakie mamy minusy tego rozwiązania:

  1. Jako, że z natury mamy teraz pojedynczy punkt dostępu do systemu, w przypadku niedostępności tego serwisu, cały system staje się niedostępny. Jeżeli mamy problem z wydajnością w API Gatewayu wpływa on w znacznej mierze na cały system.
  2. Dodajemy następny komponent w systemie
  3. Minimalnie wydłużony czas względem bezpośredniego zapytania klient-mikroserwis

Miałem to szczęście, że dość szybko zostałem zaznajomiony ze wzorcem API Gateway dzięki czemu uniknąłem problemów, które przedstawiłem w początkowej części artykułu.Myślę, że zestawienie wad i zalet pokazuje tutaj jaki potencjał w nim drzemie zwłaszcza przy rozproszonych systemach.

Bibliografia i inne ciekawe linki:

Mikroserwisy – smutne(?) fakty i hejting

Ten cały hype związany z mikroserwisami nie jest przypadkowy. Mikroserwisy , że wydają się być jedyną słuszną drogą, jeżeli nasz projekt ma nie powodować niekontrolowanego wybuchu śmiechu lub też wyrazów politowania na twarzy innych, którzy podążają za trendami i dawno mają już kilkanaście serwisów z których każdy oczywiście korzysta z osobnej bazy danych.

Dlaczego ten entuzjazm w temacie mikroserwisów należy brać z dystansem?

Ceną zalet które uzyskujemy (a opisałem je we wpisie: Mikroserwisy – wprowadzenie i dlaczego chcesz je mieć ) jest spora lista wad:

1. Podróż w nieznane.

Tytuł tego punktu jest celowo przekolorowany aczkolwiek jest w nim sporo prawdy. Musimy się liczyć z mniejszym zrozumieniem tematu pośród developerów. Mimo całego zamieszania wokół mikroserwisów w ostatnich latach, mimo wszystko architektura monolityczna jest dominującym sposobem tworzenia oprogramowania i często czynnikiem limitującym wprowadzenie takiej architektury może być brak odpowiednich kompetencji. Gdy sam zaczynałem swoją pracę nad projektem wykorzystującym mikroserwisy musiałem niemalże z dnia na dzień przestawić się na całkiem inne myślenie. Nie mając wcześniej styczności z tą architekturą chłonąłem wiedzę na jej temat niemalże z wypiekami na twarzy. Decydując się na tą architekturę trzeba być świadomym, że każdy z developerów będzie musiał wyruszyć w tą podróż i tu rodzi się pytanie czy każdy z nich będzie zdolny i wystarczająco zmotywowany by brnąć tą drogą.

2. Testowanie

Testowanie systemu jest utrudnione poprzez fakt, że trudniej nam odtworzyć stan systemu z chwili wystąpienia błędu. Patrząc pod innym kątem łatwiej testować problemy jeżeli są wyizolowane i dotyczą tylko i wyłącznie jednego mikroserwisu. Logowanie pozwala na złagodzenie problemów z testowaniem, aczkolwiek monolit tutaj wygrywa.

3. Kolaboracja między zespołami i inne implikacje wynikające z prawa Conway’a

Prawo Convey’a zgrubsza mówi o tym, że architektura systemu dąży do odzwierciedlenia struktury organizacyjnej ludzi ją tworzących.

Implementacja funkcjonalności wymaga kolaboracji pomiędzy różnymi serwisami może wymagać kooperacji między różnymi zespołami. Podział odpowiedzialności za mikroserwisy pomiędzy zespoły może wywoływać problemy gdy implementacja funkcjonalności przebiega na pograniczu wielu mikroserwisów. Nie jest może to do wielka wada, ale raczej wyzwanie i rzecz na którą warto zwrócić uwagę.

4. Dodatkowa złożoność związana z tworzeniem rozproszonego systemu

Z natury jeżeli mamy do czynienia z monolityczną architekturą, a komunikacja odbywa się wewnątrz procesu, unikamy konieczności martwienia się o to czy inny mikroserwis jest akurat dostępny, czy dane na których operujemy są wystarczająco aktualne (synchronizacja pomiędzy mikroserwisami) i wiele innych.

5.  Mikroserwisy od początku trwania projektu mogą być ryzykowne

Tworzenie architektury zorientowanej na mikroserwisy od zera może przynieść więcej strat niż pożytku. Mając ograniczoną więdzę nt. wymagań odnośnie tworzonego systemu (kto się z tym nie spotkał?) możemy dokonać błędnych decyzji nt. podziału funkcjonalności na mikroserwisy co niesie za sobą dużo większe konsekwencje niż w wypadku monolitu. Dlatego też często zalecaną praktyką jest tworzenie monolitu z luźno powiązanymi modułami, a następnie rozbijanie go na mikroserwisy gdy kształt systemu się wykrystalizuje. Unikamy wtedy przerzucania kodu pomiędzy różnymi mikroserwisami, a nawet pomiędzy różnymi zepołami!

6. Konieczna większa wszechstronność zespołów

Więcej umiejętności DevOpsowych wymaganych od zespołów rozwijających i utrzymujących mikroserwisy. Często wymaganiem jest aby zespół przeprowadził proces rozwijania mikroserwisu od implementacji do deploymentu i utrzymania. W monolitycznej architekturze mniej z tych obowiązków spoczywa na programistach. Wypuszczanie wersji zazwyczaj jest dużo rzadsze i często są obsługiwane przez dedykowanych ludzi.

7. Infrastruktura

Przy mniejszych systemach duży narzut tworzenia i utrzymywania infrastruktury, który niekoniecznie musi się opłacać.

8. Trudna kontrola niezawodności systemu.

W monolicie pula spraw które mogą pójść nie tak jest mniejsza niż przy mikroserwisach. Oczywiście jeżeli monolit leży to leży wszystko. W przypadku mikroserwisów często jesteśmy w dużym stopniu uzależnieni od infrastruktury sieciowej, dostęności zależnych mikroserwisów, szyny wiadomości i innych. To wszystko sprawia, że musimy być w stanie reagować na problemy w każdym z tych obszarów gdyż jeżeli mamy kilka komponentów z których każdy jest krytyczny dla działania systemu to prawdopodobieństwo, że system jest w pełni dostępny w danej chwili jest iloczynem prawdopodobieństwa dostępności każdego z tych komponentów.

Można złagodzić tutaj wymaganie dostępności poprzez komunikację asynchroniczną i architekturę zorientowaną na zdarzenia.

9. Krytyczność posiadania monitoringu i logowania

Jest to poniekąd implikacja kilku poprzednich punktów. Wiele miejsc w systemie wymaga uwagi więc kluczową sprawą jest posiadanie monitoringu i logowania. Nie zliczę ilości razy gdy załamywała mnie tymczasowa niedostępność logowania w systemie w którym musiałem zdebugować jakiś błąd. Nagle złożoność takiego problemu zwiększa się kilkakrotnie. Podobnie brak monitoringu sprawia, że błądzimy po omacku.

Podsumowanie

Każda w wymienionych cech systemów opartych o mikroserwisy powoduje, że warto dwa, a nawet trzy razy zastanowić się nad wprowadzeniem ich do naszego systemu. Mądzej czasami jest wstrzymać się z taką decyzją jak już będziemy ich pewni, nie dlatego, że jest to modne, ale dlatego, że przyniesie to wymierne korzyści w naszym konkretnym projekcie.

Mikroserwisy – wprowadzenie i dlaczego chcesz je mieć

Architektura mikroserwisów

Podział systemu na mikroserwisy jest to styl projektowania w którym dzielimy funkcjonalność na niezależnie deployowalne serwisy. Każdy z tych serwisów odpowiada za część funkcjonalności systemu, są one często usytuowane na różnych maszynach a komunikacja między odbywa się najczęściej przez HTTP/REST API lub też przez wiadomości przesyłane pomiędzy nimi. Nasuwa się pytanie po co? Po to by ułatwić sobie życie, wyeliminować nieprzespane noce związane z wielkimi i ryzykownymi wdrożeniami oraz by móc powiedzieć, że stosujesz mikroserwisy. Z doświadczenia wiem, że każdy z tych argumentów jest niesamowicie ważny i sprawia, że nie będziesz chcieć wracać do „starego” podejścia.

Wśród najczęściej wymienianych firm wykorzystujących z sukcesami mikroserwisy wymienia się takich potentatów jak: Uber, Netflix czy też PayPal.

Netfix

Uber

PayPal

Architektura oparta o mikroserwisy powinna charakteryzować się tym, że każdy z jej elementów jest luźno powiązany (loosely coupled) z innymi. Tak tak, powinna, lecz „sprawny” programista może ukryć zależności przed niewprawionym okiem i dalej tkwić w monolicie. Tutaj też potrzebny jest stały wysiłek by utrzymywać luźne powiązania, to nie silver bullet (niespodzianka!). Dzięki temu zmiany mogą być wprowadzane niezależnie, przez różne zespoły w różnych częściach systemu.

Wymóg tego by każdy mikroserwis mógł być niezależnie deployowalny jak i to, że ma być luźno powiązany z innymi powoduje, że chcemy też unikać ukrytych zależności takich jak współdzielenie baz danych pomiędzy mikroserwisami. Każdy mikroserwis powinien mieć swoją bazę danych. Taki wymóg oczywiście niesie za sobą konsekwencje i musimy zapewnić spójność danych pomiędzy wieloma serwisami.

2017-07-24 20_42_50-BlogGD - microservices pt1.png - Paint

Co zyskujemy?

Jakie są zalety takiej architektury? Na początku nie byłem świadomy ich istnienia, zaraz po ich poznaniu lista zalet to były po prostu obietnice. Teraz, po latach spędzonych na pracy nad mikroserwisami widzę, że nie były to puste obietnice.

1. Każdy serwis może być osobno rozwijany.

Wybór technologii dla jednego serwisu często (ale nie zawsze) nie narzuca technologii dla współpracujących serwisów. Mikroserwisy komunikujące się przez HTTP/REST są tu szczególnie łatwe w izolacji. Przykładowo w projekcie nad którym obecnie pracuję wykorzystujemy mikroserwisy w językach c#, python czy też javascript. Umożliwiamy dzięki temu eksplorację innych technologii poprzez wykorzystanie jej np. w jednym z mikroserwisów. Dzięki tego typu architekturze takie eksperymenty, nawet w wypadku nie do końca pozytywnych rezultatów, nie rzutują negatywnie na cały system.

2. Drastyczne skrócenie interwału pomiędzy releasami.

Dzięki możliwości osobnego wdrażania pojedynczych mikroserwisów, taki release jest zarówno mniej ryzykowny jak i częstszy. Jeden deploy? Dzień jak codzień. 3 deploye? Czemu nie? 5+ deploymentów? Bywa i tak 🙂

3. Zmniejszenie złożoności.

System, poprzez zmniejszenie powiązań między poszczególnymi jego częściami charakteryzuje się mniejszą złożonością co wpływa m. in. na błędogenność i czas jego życia. Architektura monolitu ma tendencje do niekorzystnego spadku produktywności rozwoju na dalszych etapach trwania projektu. Mikroserwisy „zrobione dobrze” są bardziej odporne na próbę czasu przede wszystkim z racji lepszej enkapsulacji logiki, możliwości łatwiejszej izolacji problemów, a także wymuszenia lepszej modularności systemu.

4. Skalowalność.

Umożliwienie skalowania poprzez zwiększanie liczby instancji danego mikroserwisu. Jeżeli zidentyfikujemy mikroserwis, który jest wąskim gardłem w naszym systemie możemy zwielokrotnić liczbę jego instancji poprawiając tym samym wydajność całego systemu. Nie jest to coś co otrzymujemy „za darmo”. Wymaga to odpowiedniej kultury devopsowej. Ba, w moim obecnym projekcie po dłuższym czasie nadal nie wygląda to tak jak powinno. Na szczęście coraz popularniejsze wykorzystanie usług chmurowych upraszcza wprowadzenie skalowalności znacząco! Przykładowo AWS EC2  czy też AWS ECS .

5. Bardziej naturalny podział pracy.

Lepsza alokacja zasobów, łatwiejszy podział prac i zwiększona odpowiedzialność zespołów za serwisy, którymi się opiekują to następne z zalet tego podejścia do projektowania systemu. Architektura monolityczna w której odpowiedzialność jest bardziej rozmyta nie sprzyja efektywnej pracy.

6. Circut breaker.

Jeżeli coś nie działa to jest duża szansa, że reszta systemu jest w stanie działać mimo to. Jak uzyskać tego typu niezawodność? Możemy wykorzystać tu podejście w którym definiujemy pewnego rodzaju komponent, który przy odpytywaniu zależności, którą jest inny mikroserwis, a właśniwie jego API, kontroluje stan zdrowia tego API. Jeżeli stan zdrowia przez kilka kolejnych zapytań nie jest zadowalający, przykładowo brak jest połączenia z serwisem, bądź zwraca on HTTP 500 możemy zdecydować o wyłączeniu funkcjonalności jaką oferuje. Dzięki temu nie odpytujemy na darmo serwisu który być może tylko tymczasowo jest niedysponowany oraz dajemy użytkownikowi szansę skorzystania z innych funkcjonalności systemu.

Oczywiście lista zalet nie wyczerpuje w pełni tematu, gdyż architekturę mikroserwisów można zrealizować na różne sposoby przez co możemy uzyskać nieco inny ich zestaw. Osobiście wydaje mi się, że zmniejszona złożoność systemu to największy plus gdyż jest to istotny czynnik który wpływa na opłacalność projektów, a nadzwyczaj często jest niedoceniany.

Bibliografia i inne ciekawe materiały: