Mikroserwisy – zbiór informacji

1172
views

Spis treści

Wstęp

Ostatnio miałem okazję uczestniczyć w szkoleniu „Nowoczesna architektura aplikacji web – Mikroserwisy, REST, noSQL”, które poprowadził Jakub Kubrynski z ramienia Bottega IT Minds. Z racji tego chciałbym podzielić się informacjami i przemyśleniami które wyniosłem właśnie z tego szkolenia.

Architektura rozproszona może dodać wiele korzyści dla aplikacji, która ma obsłużyć duży ruch. Można wtedy skalować te elementy, które są w danym momencie mocniej obciążone. Jest to największa różnica w stosunku do tradycyjnego podejścia budowania aplikacji, gdzie całość zawarta jest w obrębie jednego projektu – wtedy jedyna opcja skalowania to dokładanie większej ilości zasobów systemowych.

Awaria jednego elementu systemu nie musi oznaczać, że całość przestanie działać – trzeba jednak go dobrze zaprojektować i pamiętać o kilku istotnych sprawach.

Innym plusem jest możliwość dostosowania odpowiedniej technologii do danego problemu. Dzieląc aplikację na mniejsze części można sobie pozwolić na to aby każda z nich była napisana w innym języku. Pozwala to na wydajnie rozwiązywać napotykane problemy bez naginania rzeczywistości we wcześniej wybranej technologii.

Mikroserwisy nie są lekiem na całe zło

Na szczęście temat mikroserwisów nie jest już w świecie programistycznym hype o którym trzeba gadać na każdej konferencji jak cudownie rozwiązują one wszystkie problemy, które są znane z projektów informatycznych. Niektórzy zdążyli się już zachłysnąć tym rozwiązaniem i zauważyli, że jednak coś tu nie gra. Nie ma rozwiązań, które z góry gwarantują sukces – wszystko da się koncertowo spie.. zepsuć.

Istnieją sytuacje, że zastosowanie mikroserwisów sprawdzi się idealnie, warto jednak wiedzieć jak robić to dobrze. Jeśli pewne aspekty nie zostaną odpowiednio potraktowane zemszczą się na nas w najmniej oczekiwanej sytuacji.

Mikroserwisy są atomowe

Jedną z podstawowych zasad, które trzeba zapamiętać na początek – mikroserwisy są całkowicie autonomiczne. Serwis musi być niezależny od innych i nie powinna na niego wpływać w żaden sposób sytuacja, że inny serwis, z którym musi “rozmawiać” akurat nie działa.

Niezależność daje nam też inne korzyści – w odróżnieniu od aplikacji monolitu wdrażanie nowych zmian odbywa się dla mniejszych serwisów, a nie pojedynczej dużej aplikacji. Dzięki czemu sam proces wdrażania jest mniej bolesny zarówno dla zespołu deweloperskiego jak i biznesu.

Mikroserwisy nie są małe – mikroserwisy to byty biznesowe

Nazwa mikroserwisów wprowadza w błąd wiele osób, ponieważ może ona sugerować, że są to malutkie aplikacje, które wykonują pojedyncze akcje. To błąd. Często buduje się aplikacje wykonujące jakieś jedno zadanie, np. synchronizującą i konwertującą jakieś dane i okrasza się je mianem mikroserwisu. Tak naprawdę jest to usługa wpisująca się w architekturę SOA.

Mikroserwis powinien realizować zadania biznesowe i zawierać wszystkie niezbędne zależności. Najlepiej określać ramy mikroserwisów przy pomocy Bounded Context.

Liczba mikroserwisów nie może być za duża

Rozbijanie aplikacji na większą ilość mikroserwisów, które ze sobą się komunikują nie jest dobrym rozwiązaniem. Po pierwsze trzeba utrzymywać system działający w większym rozproszeniu, a po drugie trzeba zadbać o dostępność serwisów.

Załóżmy, że posiadamy 10 mikroserwisów, które się ze sobą komunikują i każdy z nich posiada SLA na poziomie 99,5% – jaki SLA będzie posiadał cały system? Nie, nie jest to 99,5% 😉 Do jego wyliczenia powinniśmy pomnożyć wszystkie wartości serwisów, które się ze sobą komunikują:

99,5% * 99,5% * 99,5% * 99,5% * 99,5% * 99,5% * 99,5% * 99,5% * 99,5% * 99,5% = 95%

Jak widać jest to spora różnica, dlatego zależnych od siebie mikroserwisów powinno być jak najmniej. Trzeba też pamiętać, że bierzemy pod uwagę wszystkie elementy systemu takie jak bazy danych, storage, kolejki, load balancery itp., a nie tylko aplikacje, które ze sobą się komunikują.

Service Discovery jest lepszym wyborem niż Load Balancer

Jedną z ważniejszych cech mikroserwisów jest to, że mogą być uruchamiane w wielu instancjach w zależności od potrzeby. Dlatego do komunikacji między nimi potrzebna jest usługa, która będzie posiadała wiedzę ile instancji danego serwisu działa i rozproszy ruch w równomierny sposób.

Naturalnym i najprostszym wyborem wydaje się Load Balancer, ponieważ załatwia on za nas całość roboty. Jest jednak problematyczny ze względu na to, że jest jedynym punktem dostępu i to przez niego musi przechodzić cały ruch. Load Balancer to po prostu dodatkowy narzut SLA.

Service Discovery wydaje się lepszym rozwiązaniem i jak się okazuje jest wykorzystywany przez większość dużych firm, takich jak Netflix. Chociaż jego poprawna implementacja wydaje się skomplikowana to istnieją gotowe rozwiązania, które mogą w tym pomóc, chociażby takie jak eureka od Netflixa i consul stworzony przez HashiCorp.

Async by default

Najlepszym sposobem do komunikacji między serwisami jest asynchroniczność, która pozwala na większą elastyczność w postaci lepszego i szybszego skalowania. Można przez to rozumieć, że stosowanie protokołów kolejkowych takich jak AMPQ powinno być wykorzystywane wszędzie tam gdzie się da. Stosowanie połączeń synchronicznych to wyjątek.

Oczywiście asynchroniczność zwiększa także złożoność i generuje kolejne problemy związane z komunikacją między serwisami.

Więcej na ten temat można posłuchać na prezentacji, którą przedstawił Tomasz Nurkiewicz.

Monitorowanie zamiast load testów

Load testy to popularny sposób sprawdzania wydajności aplikacji pod dużym obciążeniem. Problemem tego podejścia jest fakt, że nigdy nie da się w stu procentach odwzorować rzeczywistego ruchu na produkcji.

O wiele bardziej miarodajnym podejściem jest monitoring. Mowa tutaj o monitoringu aplikacji (np. ile czasu zajmują operacje na bazie, komunikacja z innymi serwisami) i systemowym (zużycie pamięci, procesora i innych zasobów). Dzięki tym pomiarom jesteśmy w stanie określić jak zachowuje się nasz system i wyciągać na tej podstawie odpowiednie wnioski.

W tym punkcie idealnie pasuje prezentacja “How NOT to Measure Latency”.

Testy e2e nie przyniosą korzyści

Testy e2e w przypadku mikroserwisów są bardzo czasochłonne i nakład pracy włożony w ich trzymanie nie przynosi wymiernych rezultatów. W przypadku gdy wewnątrz systemu istnieje kilka / kilkanaście serwisów to pokrycie przypadków testowych zaczyna być karkołomne, albo wręcz niemożliwe.

Dużo lepszym podejściem jest testowanie tylko tych serwisów, które się ze sobą bezpośrednio komunikują. To podejście jest dużo łatwiejsze jeśli razem z nim stosowana jest technika Consumer Driven Contracts. Najpopularniejszym i uniwersalnym narzędziem, które wspiera CDC jest Pack.

Utrzymanie mikroserwisów

Tworzenie aplikacji to jedno, lecz jej utrzymanie i rozwój to drugie. I ten proces trwa zdecydowanie dłużej. Bardzo rzadko jesteśmy świadkami sytuacji, że wdrażana aplikacja nie jest już modyfikowana w przyszłości. Z racji tego, że z jednym mikroserwisem często komunikuje się kilka innych trzeba zadbać o to żeby wprowadzane zmiany nie wpływały negatywnie na całość systemu.

Continuous Integration (CI)

CI to zapewnienie ciągłej integracji źródeł kodu, często elementów, które nie zostały jeszcze dokończone, ale nie wpływają negatywnie na całość aplikacji. W tym podejściu ważne jest aby jak najprędzej dostarczać zmiany do głównego repozytorium. Dzięki temu inne osoby, mogą zweryfikować na wczesnym etapie obrany kierunek. Z tych nowości mogą też wcześniej korzystać członkowie zespołu.

Feature toggle/switch

To bardzo proste podejście może zaoszczędzić sporo czasu i ułatwić pracę zespołu. Jeśli stosujesz zasady CI i często integrujesz swój kod z całością możesz oznaczyć niedziałający i nie dokończony jeszcze element jako wyłączony, tak aby nie był używany przez aplikację. Tak naprawdę czasem wystarczy zwykły if (false), który nie dopuści do wykonania odpowiedniego kodu.

Często ta technika jest rozszerzana i stosowana do A/B testów. Pozwala też uruchomić nowość tylko na wybranej grupie użytkowników. Jeśli wchodzi jakiś feature to zamiast wypuszczać to dla wszystkich odbiorców, najpierw sprawdzane jest na mniejszej liczbie osób aby zweryfikować czy wszystko działa poprawnie.

CD

Automatyzacja wdrożeń pozwala na unikanie błędów, które pojawiają się podczas manualnych wdrożeń. Jeśli robisz coś cyklicznie – musi to zostać zautomatyzowane. I nie ma opcji na wymówki, że nie ma na to czasu. Jeśli jest czas robić wciąż to samo to znajdzie się też czas żeby to zautomatyzować.

Do zapewniania ciągłości wprowadzania zmian stosuje się jedną z dwóch technik:

  • Continuous Delivery – zmiany wchodzą na kolejne środowiska automatycznie, jednak przed wdrożeniem na produkcję trzeba potwierdzić to ręcznie.
  • Continuous Deployment – cały proces wykonywany jest automatycznie

Wprowadzanie zmian kompatybilnych wstecz (Deferred change)

Podczas rozwoju aplikacji często pojawiają się zmiany czysto biznesowo lub techniczne – niektóre wprowadzają “łamiącą zmianę”. Nie można dopuścić do sytuacji, że jeden zespół wprowadza taką zmianę do serwisu A, a drugi zespół odpowiedzialny za serwis B nie zdążył jeszcze zaimplementować poprawek, w wyniku czego komunikacja między serwisami nie działa poprawnie.

W celu zaradzenia takiemu problemowi stosuje się technikę opóźnionych zmian. Dla przykładu, załóżmy, że serwis A udostępnia informację o miesięcznej wypłacie pracownika nazwane jako salary. Z serwisem A dogaduje się serwis B, który korzysta właśnie z tego pola. Jednak podjęto decyzję, że teraz będzie to roczna wypłata, a nie miesięczna. Kolejne kroki wprowadzanych zmian powinny wyglądać podobnie jak poniżej:

  1. Stan początkowy: istnieje pole salary.
  2. Dodajemy nowe pole w serwisie A o nazwie annualSalary i pozostawiamy stare salary. Serwis A będzie teraz wysyłał obie te wartości.
  3. Implementujemy w serwisie B możliwość odczytywania nowego pola.
  4. Jeśli serwis B poprawnie odczytuje nowe pole w trybie produkcyjnym można usunąć odczytywanie pola salary w serwisie B.
  5. Skoro wszyscy klienci serwisu A nie korzystają już z pola salary można je wyłączyć w serwisie A i nie nadawać.

Przygotuj się na najgorsze

W architekturze rozproszonej komunikacja między poszczególnymi serwisami jest podstawowym aspektem, z tego powodu trzeba być gotowym na każdą okoliczność. W każdym momencie któryś z elementów całej układanki może przestać działać i połączenie z nim stanie się niemożliwa. Bardzo obszerny zbiór wiedzy na ten temat został przygotowany przez Microsoft.

Design for failure

Istnieją dwa podejścia do zabezpieczenia się przed niepożądanymi sytuacjami:

  • safe to fail – nie dopuszczać do awarii za wszelką cenę i starać się przetestować każdy element w taki sposób, żeby mieć pewność, że nic się nie wydarzy
  • fail-safe – w tym podejściu zespół godzi się z tym, że zawsze coś może się wysypać i przygotowuje się na tą sytuację wprowadzając tzw. stan bezpieczny po wystąpieniu awarii

Życie pokazuje, że w tak złożonych systemach zabezpieczenie się przed każdą ewentualnością jest praktycznie niemożliwe, dlatego lepiej stosować technikę ‘fail-safe’, gdzie system zostanie odpowiednio przygotowany na wystąpienie awarii.

Circuit Breaker

Załóżmy, że mamy 2 serwisy, które ze sobą rozmawiają – jeśli w pewnym momencie, któryś z nich przestanie odpowiadać i ta sytuacja trwa już jakiś czas to nie ma żadnego sensu bombardować go ciągłymi zapytaniami skoro i tak wiadomo, że ten serwis nie żyje.

Właśnie w tym celu stosuje się tzw. bezpieczniki, które odcinają połączenie z tym serwisem na czas kiedy jest niedostępny. Wzorzec ten został opisany przez Martina Fowlera na jego blogu. Najpopularniejszą gotową implementacją jest Hystrix stworzony przez Netflixa, czyli lidera tematu związanego z mikroserwisami.

Fallback

W momencie kiedy bezpiecznik zadziałał i podczas danej akcji nie da się dostać do potrzebnego mikroserwisu warto zdefiniować akcję, która powinna się wydarzyć skoro zamierzone zadanie jest niemożliwe. To właśnie tzw. Fallback, który powinien zostać zdefiniowany przez biznes.

Załóżmy, że tworzony jest serwis sklepu, który komunikuje się z zewnętrznymi bramkami płatności. Jeśli taka bramka akurat jest nie dostępna w momencie kiedy klient chce opłacić swoje zamówienie powinna się wydarzyć jakaś alternatywna akcja, która pozwoli dokończyć transakcję.

Eventual Consistency

Oprócz zadbania o to co się stanie z logiką biznesową w poszczególnych mikroserwisach, trzeba też poświęcić trochę czasu na zastanowienie się jak powinny być przechowywane dane w bazie danych. Jeśli istnieje więcej instancji systemu bazodanowego trzeba zapewnić spójność przechowywanych danych.

Istnieją dwa podejścia:

  • Strong Eventual Consistency (SEC) – najnowsze dane są zapisywane bezpośrednio do wszystkich instancji bazy danych tak szybko jak to możliwe, dzięki temu istnieje pewność, że nie ważne z której instancji nastąpi odczyt danych zawsze będą one aktualne. Niestety podejście to może negatywnie wpływać na skalowalność i performance systemu.
  • Weak Eventual Consistency (WEK) – dane zapisywane są do jednej instancji systemu bazodanowego, a dopiero później kolejne instancje są aktualizowane o najnowsze dane. W konsekwencji nie ma pewności, czy właśnie odpytywana instancja posiada najnowsze dane.

Bardzo interesujący dokument na ten temat można zobaczyć tutaj

Infrastructure as a code (IaC)

W przypadku mikroserwisów jasne jest, że można się spodziewać większej ilości środowisk do utrzymania niż w przypadku tradycyjnego podejścia. Popularne jest też korzystanie z zewnętrznych dostawców takich jak AWS czy Azure. W gotowości na najgorsze trzeba brać również pod uwagę awarię całej infrastruktury.

Załóżmy, że cały twój system działa w jednym regionie na AWS. Co zrobisz gdy nagle zostanie on odcięty od świata? Jeśli stosujesz technikę IaC możesz w szybkim czasie rozstawić całą infrastrukturę na innym regionie bez paniki.

Wymaga to trochę pracy i odpowiedzialności, ale dzięki temu podejściu otrzymasz szereg korzyści. Oprócz wspomnianego wcześniej szybkiego odtwarzania systemu możesz też kontrolować każdą zmianę infrastruktury, ponieważ cały kod jest wersjonowany.

Do definiowania infrastruktury hardwarowej popularnym rozwiązaniem jest terraform, a softwarowej ansible.

Podsumowanie

Jak widać projektowanie architektury rozproszonej jest naprawdę wymagającym zadaniem i wymaga ogromnej liczby kwestii o które trzeba zadbać. Chciałem aby ten artykuł stworzony na podstawie notatek z kursu w przystępny sposób poruszył wszystkie najważniejsze aspekty związane z mikroserwisami.

W ramach podsumowania mogę zaproponować obejrzenie świetnej prezentacji Jakuba Kubrynskiego, który jest ekspertem w tym temacie: Utrzymywalne mikroserwisy.