Kodujże!

blog krakoskiego programisty

Tag: Microservices

Mikroserwisy i Spring Cloud. Projekt bazowy.

Artykuł ten jest częścią serii zatytułowanej „Mikroserwisy i Spring Cloud”.

1. Mikroserwisy i Spring Cloud. Wprowadzenie.
2. Mikroserwisy i Spring Cloud. Projekt bazowy.

Projekt bazowy

We wpisie tym opiszę projekt bazowy, który z każdą kolejną odsłoną cyklu będzie coraz bardziej rozbudowywany, i który posłuży nam jako podstawa do testowania rozwiązań dostarczanych przez biblioteki z rodziny Spring Cloud.

Sklep Internetowy

Projektem tym jest sklep internetowy składający się z trzech współpracujących ze sobą mikroserwisów. Są nimi:

  • WarehouseService – serwis udostępniający metody do przeszukiwania magazynu i pobierania z niego produktów,
  • CartService – serwis zarządzający koszykami klientów zalogowanych do systemu,
  • OrderService – serwis odpowiedzialny za proces realizacji zamówienia. W swej pracy komunikuje się z CartService, od którego pobiera informacje o stanie koszyka, i któremu zleca jego czyszczenie (po przetworzeniu zamówienia).

Aby lepiej zrozumieć role poszczególnych mikroserwisów, spójrzmy jak mogłaby wyglądać ich wzajemna komunikacja w procesie przygotowywania i realizacji zamówienia:

  • Po wejściu na stronę sklepu klient otrzymuje od WarehouseService listę dostępnych w sklepie produktów.
  • Klient wybiera z listy interesujący go produkt i wysyła do CartService żądanie dodania tego produktu do swojego koszyka.
  • Klient realizuje zamówienie poprzez wysłanie żądania do OrderService, który kolejno pyta CartService o zawartość koszyka klienta, przetwarza zamówienie, a na koniec ponownie komunikuje się z CartService, tym razem w celu wyczyszczenia koszyka.

Czy taki model ma sens z biznesowego punktu widzenia? Prawdopodobnie nie. Nie jest jednak moim celem implementowanie skomplikowanych procesów, a jedynie stworzenie „piaskownicy” do zabawy springowymi rozwiązaniami. W związku z powyższym tworzony kod będzie w wielu miejscach uproszczony i „dziurawy” (nie pokryje wszystkich przypadków użycia, brak będzie dokładnej obsługi błędów). Początkowo też zrezygnujemy z tworzenia dedykowanego interfejsu graficznego oraz z implementowania security i logiki do zarządzania użytkownikami.

WarehouseService

Zadaniem WarehouseService jest udostępnianie metod służących do zarządzania stanem magazynu sklepowego. Stan magazynu definiowany jest przez zbiór znajdujących się w nim produktów, w związku z czym głównym obiektem domenowym w tym serwisie jest Product.

Tworzymy dla niego repozytorium.

Oraz serwis implementujący logikę biznesową dotycząca pobierania i przeszukiwania listy produktów.

Powyższy serwis nie zawiera żadnych metod służących do dodawania/usuwania przedmiotów z magazynu. Nie implementujemy ich, ponieważ póki co żaden z klientów nie potrzebuje takiej funkcjonalności.

Ostatnim kluczowym elementem jest kontroler.

CartService

Tak jak dla WarehouseService głównym obiektem domenowym jest Product, tak dla CartService jest nim Cart.

Każdy koszyk przypisany jest do konkretnego użytkownika (userId) oraz zawiera listę znajdujących się w nim produktów (products). Produkty te oczywiście odnoszą się do obiektów z WarehouseService, lecz do ich reprezentacji używamy lekko „odchudzonej” wersji klasy Product z tamtego serwisu.

W porównaniu do Product z WarehouseService nie mamy tutaj pola name. Z punktu widzenia koszyka jest ono nieistotne, więc nie zaprzątamy sobie nim głowy.

Dlaczego nie współdzielimy kodu klasy Product pomiędzy tymi dwoma serwisami? Bo tak jest najprościej. 🙂 Gdybyśmy mieli bibliotekę enkapsulującą wszystkie encje używane w systemie i byłaby ona zależnością dla każdego z tworzących system mikroserwisów, to mogłoby to sprawić wiele problemów. Co gdyby jeden z serwisów chciał dołożyć jedno pole więcej do encji i operować tak zaktualizowaną jej postacią? Wtedy każdy inny serwis musiałby się dostosować, a być może tylko jeden z nich byłby faktycznie zainteresowany tą zmianą.

Logika biznesowa zarządzania koszykiem znajduje się w klasie CartService.

Jednym z pól tej klasy jest cart. W celu uproszczenia implementacji przyjęliśmy założenie, że z systemu będzie korzystał tylko jeden użytkownik. W związku z tym CartService póki co będzie miał na stałe „zaszyty” koszyk tego jednego użytkownika.

Pozostał jeszcze kontroler.

OrderService

Ostatnim serwisem w naszym skromnym systemie jest OrderService. Jako że pomijamy szczegóły techniczne procesu realizacji zamówienia, to jest to jeden z prostszych implementacyjnie komponentów. Punktem wejścia do niego jest OrderController.

Tak jak i pozostałe dwa kontrolery, tak i ten jest bardzo „chudy” i jedyne co robi to oddelegowuje pracę do warstwy serwisów.

Implementacja OrderService raczej nie wymaga komentarza. CartService natomiast to warstwa abstrakcji, pod którą kryją się rest-owe wywołania do faktycznego serwisu.

Przykład działania

Kody źródłowe powyższych projektów można znaleźć na moim GitHubie. Aby uruchomić cały system wystarczy w folderze  00 - base project wywołać komendę  ./gradlew bootRun --parallelWarehouseServiceCartServiceOrderService zaczną nasłuchiwać na portach odpowiednio 8030, 8040 i 8050.

Przykładowa sekwencja zapytań może wyglądać następująco.

Podsumowanie

Przedstawiony projekt pozbawiony jest jakichkolwiek zależności względem rozwiązań z rodziny Spring Cloud. Postaramy się dodawać je cegiełka po cegiełce, stopniowo budując pełnoprawny system. Zaczniemy, już w następnym wpisie, od biblioteki Open Feign.

Mikroserwisy i Spring Cloud. Wprowadzenie.

Chcąc rozszerzyć nieco zakres tematów poruszanych na blogu i wyjść poza obszar zagadnień niskopoziomowych postanowiłem rozpocząć serię wpisów o mikroserwisach. W tym i w następnych artykułach postaram się przybliżyć założenia, na których opiera się ten model architektury, a także pokazać jak w realizacji tych założeń może pomóc nam projekt Spring Cloud. Mam nadzieję, że informacje zawarte tutaj okażą się dla osób zainteresowanych tematem przydatne, a forma ich przekazu przystępna.

Zapraszam do lektury!

Poniższy spis treści będzie aktualizowany wraz z pojawianiem się kolejnych wpisów.

1. Mikroserwisy z użyciem Spring Cloud. Wprowadzenie.

Słowem wstępu…

Historia branży informatycznej to historia odkrywania na nowo istniejących rozwiązań. Wybierzcie sobie trzy dowolne buzzwordy, jakie królowały na konferencjach na przestrzeni ostatnich kilku lat, a po bardziej szczegółowym zgłębieniu tematu najprawdopodobniej okaże się, że reprezentowane przez nie koncepty nie są niczym nowym i istniały w świadomości programistów na długo przed tym, jak branża spopularyzowała i zgloryfikowała je na wszelkie możliwe sposoby. Architektura mikroserwisowa nie jest tutaj wyjątkiem. Można bowiem o niej myśleć jako następcy SOA (Service Oriented Architecture) powstałym na drodze ewolucji sterowanej chęcią wykorzenienia z SOA tych elementów, które ograniczały możliwości tego modelu.

O różnicach między SOA a MSA (Microservices Architecture) będzie w jednym z kolejnych wpisów. Przed podjęciem takiego tematu niezbędne jest jednak zdefiniowanie czym w ogóle są mikroserwisy.

Czym jest MSA?

Architektura mikroserwisowa to podejście, w którym aplikacja tworzona jest jako zbiór niewielkich, autonomicznych, skupionych na wykonywaniu konkretnych zadań serwisów, współpracujących ze sobą w celu dostarczenia niezbędnej z punktu widzenia klienta funkcjonalności.

Rozbijmy tę definicję na części.

Niewielkie

Nie ma uniwersalnej miary, która pozwoliłaby z pewnością określić czy dany serwis jest mały, duży, a może w sam raz? Rozmiar w tym kontekście jest pojęciem względnym. Bardzo podoba mi się natomiast to, co na ten temat napisał Sam Newman w książce Building Microservices. Twierdzi on, że jeżeli nad kodem danego serwisu jest w stanie zapanować jeden mały zespół, to najprawdopodobniej wszystko jest w porządku.

Autonomiczne

Mikroserwisy powinny być od siebie niezależne, przy czym wyznacznikiem niezależności jest tutaj możliwość dokonania zmian i zredeployowania mikroserwisu bez konieczności modyfikacji pozostałych mikroserwisów w systemie. W praktyce oznacza to, że mikroserwisy nie powinny wiedzieć nawzajem o swoich szczegółach implementacyjnych, a w swej współpracy opierać się jedynie na dobrze zdefiniowanych interfejsach.

Skupione na wykonywaniu konkretnych zadań

Jest to nic innego jak przeniesienie Zasady Jednej Odpowiedzialności (Single Responsibility Principle) na grunt mikroserwisów. Dla przypomnienia – SRP głosi, że dany moduł (w naszym przypadku mikroserwis) powinien być odpowiedzialny tylko za jedną funkcjonalność dostarczaną przez system, i że ta funkcjonalność powinna być całkowicie w tym module zamknięta.

Współpracujące ze sobą

Ta część definicji nie wymaga głębszego komentarza. Chodzi oczywiście o wzajemną komunikację mikroserwisów i wzajemne wykorzystywanie dostarczanych przez nie funkcjonalności. Warto jedynie nadmienić, że komunikacja winna być oparta o lekkie protokoły (jak np. HTTP), nieograniczające spektrum możliwych do zintegrowania z nimi technologii.

Zalety

Podejście mikroserwisowe, jeżeli dobrze zaaplikowane, niesie ze sobą szereg korzyści. Należą do nich m.in.:

  • skalowalność – dzięki „rozbiciu” aplikacji na mikroserwisy mamy możliwość skalowania tylko tych komponentów, które stanowią wąskie gardło systemu. Jest to całkowite przeciwieństwo skalowania znanego z aplikacji monolitycznych, w których to operacji tej musi ulegać całość tworzonego oprogramowania.www.kodujze.pl - Skalowanie Mikroserwisów
  • odporność na awarie – awarie pojedynczych mikroserwisów mogą zostać odizolowane w taki sposób, by nie pociągały za sobą awarii całego systemu. Przykładowo, jeżeli w sklepie internetowym z powodu błędu w kodzie awarii ulegnie mikroserwis odpowiedzialny za generowanie spersonalizowanych ofert, to możemy na chwilę zrezygnować z tej funkcjonalności. W przypadku monolitu ten sam problem mógłby spowodować całkowite wstrzymanie pracy aplikacji.
  • łatwość wprowadzania zmian – jako że mikroserwisy komunikują się ze sobą za pomocą dobrze zdefiniowanych interfejsów, to wszelkie zmiany dotyczące ich szczegółów implementacyjnych mogą być wprowadzane w dowolnych momentach życia systemu, niezależnie od pozostałych jego komponentów. Dzięki temu nie trzeba robić deploymentu całej aplikacji w celu wprowadzenia niewielkich poprawek.
  • czytelność – podział na mikroserwisy wymusza na nas konieczność myślenia o systemie jako zbiorze komunikujących się ze sobą jednostek biznesowych. To z kolei przekłada się na lepszą jakość kodu, gdyż musimy z tego powodu myśleć o interfejsach między mikroserwisami, ukrywaniu ich szczegółów implementacyjnych, wymaganych zależnościach – elementach, które w świecie obiektowym pomagają pisać czysty kod. 🙂
  • możliwość mieszania technologi – użycie do komunikacji protokołu niezwiązanego z żadnym konkretnym językiem czy biblioteką (jak np. HTTP) powoduje, że każdy z mikroserwisów może być napisany z użyciem innej technologii. Dzięki temu nie musimy ograniczać się do jednego języka, a każdy komponent może być stworzony za pomocą narzędzi, które najlepiej adresują jego techniczne potrzeby.
  • możliwość eksperymentowania – z powodu ich niewielkich rozmiarów, łatwiejszym staje się eksperymentowanie w kwestii budowy i zasad działania poszczególnych mikroserwisów. Możemy na przykład stworzyć alternatywną wersję jakiegoś mikroserwisu (napisaną w innej technologii, czy z wykorzystaniem innych algorytmów) i „bezboleśnie” podmienić ją w produkcie.

Wady

Biorąc pod uwagę powszechną ostatnio „modę na mikroserwisy” można odnieść wrażenie, że jest to rozwiązanie pozbawione wad. Nic bardziej mylnego! Tworzenie oprogramowania w oparciu o MSA jest zdecydowanie trudniejsze niż tworzenie aplikacji monolitycznych. Problematyczne jest chociażby testowanie takich systemów, ich deployment (o czym świadczy gwałtowny rozwój gałęzi DevOps-owej w branży), obsługa błędów i transakcyjności. Co więcej, utrzymanie architektury mikroserwisowej wymaga bardzo dobrej komunikacji między zespołami pracującymi nad różnymi jej elementami.

Techniczne wyzwania

Jest kilka kwestii, o które musimy zadbać w systemach zbudowanych w oparciu o MSA. Są to:

  • zarządzanie konfiguracją systemu – jak sprytnie zarządzać konfiguracją kilkudziesięciu mikroserwisów, z których każdy może być uruchomiony w tym samym czasie w wielu instancjach?
  • dynamiczne skalowanie – jak wykorzystać naturalną zdolność mikroserwisów do skalowania i przeprowadzać tę operację automatycznie, w oparciu o aktualny ruch w systemie?
  • logowanie i śledzenie ruchu użytkowników – skoro każde żądanie może przejść przez dziesiątki różnych mikroserwisów przed powrotem do klienta, to jak możemy prześledzić jego ruch?
  • obsługa błędów – jak wykrywać i izolować uszkodzone mikroserwisy?

Gdzie w tym wszystkim jest Spring Cloud?

Spring Cloud to projekt, który zrzesza pod wspólną nazwą dziesiątki różnych bibliotek i frameworków stworzonych w celu zaadresowania wyżej wymienionych problemów.

Spring Cloud Config to serwer, który pomaga zarządzać konfiguracją mikroserwisów. Ribbon jest load-balancerem działającym w oparciu o dane otrzymywane od serwera Eureka (dane o mikroserwisach i ich instancjach). Hystrix dostarcza narzędzi służących do obsługi awarii, a Zipkin śledzi drogi przebyte przez nadchodzące do systemu żądania. W przypadku komunikacji opartej o protokół HTTP do grona zależności dołącza Feign, który znacząco upraszcza proces odpytywania zdalnych zasobów. Bramą do systemu bardzo często jest Zuul – serwer pracujący w charakterze API Gateway, czyli fasady, pod którą kryją się wywołania konkretnych mikroserwisów.

Projektów jest oczywiście więcej. Wymienione powyżej to te, o których postaram się napisać w tej serii, i które stanowią swoistego rodzaju fundament projektu Spring Cloud.

© 2019 Kodujże!

Theme by Anders NorenUp ↑