Ciemna strona mikroserwisów

Mikroserwisy to temat, który w moim odczuciu jest nadal bardzo popularny na wszelkiego rodzaju meetupach, czy konferencjach porgramistycznych (sam się do tego poniekąd przyczyniam). Implikacją tego jest fakt , że wielu programistów odchodzi od oklepanych i bardzo niemodnych monolitów, na rzecz systemów rozproszonych. Pytanie brzmi, dlaczego? Osobiście uważam, że hype wszyskitgo co „distributed” i „micro” jest poniekąd efektem kuli śniegowej popchniętej kilka lat temu. Ktoś kiedyś wspomniał o SOA, nazwał to nieco inaczej i bum… mamy mikroserwisy. Ktoś dodał, że skalowanie horyzontalne to podstawa w dzisiejszych czasach i bum… mamy architekturę idealną, a w dodatku super łatwą do utrzymania bo przecież nie mamy jednej, wielkiej kupy kodu tylko małe reużywalne usługi! WOW! Wydawać by się zatem mogło, że to podejście praktycznie nie posiada wad, prawda? Ile znasz prezentacji, artykułów czy książek, które opisują mikroserwisy w trochę mniej kolorowym świetle? Bardziej szarym, a czasami całkowicie czarnym? Powiem otwarcie, że kiedy myślę o mikroserwisach to bardzo szybko nasuwa mi się skojarzenie z tym obrazkiem (lekko zmodyfikowanym):

 

 

Wszyscy jak jeden mąż powtarzają w kółko, że ten typ architektury zapewnia nam wysoką dostępność systemu, łatwość w skalowaniu, transparentność, modularność, asynchroniczność i wiele innych, ale przede wszystkim… Netflix ma mikroserwisy więc chyba musi to być coś fajnego. Gwoli ścisłości, nie twierdzę, że to podejście jest swoistą wydmuszką marketingową i nie posiada zastosowania we współczesnym świecie. Wręcz przeciwnie, jest to świetne narzędzie do zadań specjalnych, które dopiero rozkręca się tam, gdzie „typowe” systemy już się poddają. Jednak jak to w życiu bywa, każdy kij ma dwa końce.

 

Mikroserwisy == modularność

Cała „zabawa” i uświadomienie sobie, że to wszystko może nie jest takie łatwe, zaczyna się już chwilę po tym jak uruchomimy nasze IDE. Przychodzi czas na utworzenie solucji, a następnie dodanie do niej N projektów, czyli naszych usług. Jak to wszystko podzielić, żeby było dobrze? Niestety w tym momencie jestem zmuszony udzielić odpowiedzi generycznej – to zależy. W głównej mierze od projektu i jego domeny. Oczywiście są pewne klasyki (jak sklep online :D), które możemy zaimplementować bez głębszego pochylenia się nad tematem i na 99% będzie to nawet składne, ale nie zawsze jednak mamy taką możliwość. Wtedy pozostaje rozbijanie domeny i całego systemu na mniejsze fragmenty i patrzenie czy ma to sens. W zasadzie istnieją trzy rezultaty takiego procesu:

 

 

Pierwsza opcja to stworzenie systemu, który stoi gdzieś pomiędzy dwoma konceptami. To okrzyknięty mianem antywzorcamikro monolit, który można określać zdaniem „robię mikroserwisy na 50% bo się trochę boję”. Niby system został podzielony na mniejsze składowe, ale jakoś nie do końca widać zalety tego podziału. Usługi enkapsulują zbyt duże fragmenty domeny niż powinny, więc coupling dalej uniemożliwia ich szybkie podgrywanie czy chwilowe zatrzymanie, gdyż rzutowało by to na duży fragment systemu. Baza danych dla usług jest wspólna więc jest to nadal wąskie gardło systemu. Jedyne co się zmieniło to liczba aplikacji do deployowania na produkcji i utrudniony development.
Po drugiej strony barykady mamy wzięcie sobie idei mikrousług zbytnio do serca. Ja lubię określać to mianem nanoserwisów, choć ten termin raczej nie występuje w publikacjach naukowych. Osobiście nie widziałem na żywo takiego „potworka”, ale czytałem o przypadkach gdy jedna usługa równała się praktycznie jednej metodzie. To duża przesada, która może być dopuszczalna w bardzo specyficznych przypadkach, ale zdecydowanie dla małego fragmentu systemu. Efektem tych prac będzie kod bardzo trudny do analizowania, utrzymania, rozwijania i debugowania.
Widzisz więc, że sztuką jest dobranie rozmiaru konkretnej usługi tak, aby logicznie zamknąć w niej część domeny aplikacyjnej przy jednoczesnym zachowaniu komfortu pracy z kodem. Co jak co, ale to Ty będziesz go rozwijał przez następne miesiące. Myślę, że z powodu „względności” całego procesu, nie powinniśmy przyjmować jakiś ogólnych zasad projektowania systemu opartego na mikroseriwsach jak „serwis nie powinien być większy niż 1000 linii kodu”. To system, procesy biznesowe i Twoje umiejętności jako programisty powinny wyznaczyć ich granice.

 

Mikroserwisy == asynchroniczna komunikacja

Posiadasz już usługi. Teraz czas na sprawienie, aby w jakiś sposób ze sobą rozmawiały. To jest istota tej architektury – wiele małych usług, które dla użytkownika końcowego mają zachowywać się jak spójna jednostka. Przyjęło się, że komunikacja odbywa się na dwóch płaszczyznach:

  • do odczytu danych wykorzystujemy synchroniczny protokół HTTP
  • do zapisu danych wykorzystujemy komunikację asynchroniczną np. poprzez kolejki

Dlaczego tak? Zauważ, że z perspektywy użytkownika końcowego tak się to właśnie odbywa. Kiedy wchodzisz na konkretną stronę to czekasz na jej pobranie i wyrenderowanie w przeglądarce. Wiesz dobrze, że przejście na inną stronę/zakładkę powoduje przerwanie całego procesu i nie dojdzie do sytuacji gdy nagle przeglądarka podmieni Ci widok na ten, który próbowałeś otworzyć 10 min temu tylko dlatego, że ukończyła jego pobieranie. Jest to więc proces synchroniczny. Sprawa ma się zgoła odmiennie przy zapisie danych. Ile razy dokonywałeś jakiś zmian w systemie (opłaciłeś zamówienie na jedzenie, przegenerowałeś plan zajęć na uczelni) i myślałeś sobie „Kto to projektował?! Dlaczego od trzech minut muszę oglądać spiner z napisem ‚proszę czekać’!!!!”. W większości przypadków chcemy, aby cały proces trwał możliwie krótko i jesteśmy w stanie zaakceptować fakt, że jakiś popup wyskoczy nam np. za 5 minut z informacją, że wszystko przebiegło pomyślnie. Tak działają spora część portali jak np. Twitter, który wyświetla czerwone serduszko (like) zaraz po kliknięciu mimo, że tak na prawdę w systemie nie jest to jeszcze odnotowane. Użytkownicy szczęśliwi, a ewentualna szkoda w przypadku niepowodzenia mała (w tym konkretnym przypadku).
No dobrze, ale dlaczego właściwie o tym wspominam? Implikacją opisanego modelu komunikacji jest to, że propagowanie informacji z serwera do użytkownika jest znacznie utrudnione. Zobrazuję to klasycznym przykładem – użytkownik zakłada konto w aplikacji podając maila i hasło. Dane zostają odebrane w API i przekazane asynchronicznie (przez kolejkę) do konkretnej usługi zajmującej się tym właśnie fragmentem domeny. Usługa przed utworzeniem użytkownika dokonuje walidacji posługując się jakimiś przyjętymi kryteriami. Pytanie, co w przypadku gdy z jakiegoś powodu nie będziemy mogli zaakceptować danych użytkownika bo np. email jest już zajęty albo hasło jest za krótkie? W klasycznym (synchronicznym) modelu jest to relatywnie proste:

 

 

W przypadku asynchronicznej komunikacji wygląda to w ten sposób:

 

 

Widzisz zatem, że odpowiedź z serwera nie ma charakteru „kontraktu”, który poświadcza o persystencji danych na serwerze. Jest to jedynie promesa. Trzeba zatem w jakiś sposób otrzymać z API informację czy cała operacja przetworzyła się poprawnie. Sposobów jest kilka, a jeden z nich opisze wkrótce na blogu.

 

Mikroserwisy == dostępność systemu

No i przyszedł czas na koronny argument „za” mikroserwisami, czyli dostępność systemu. O co chodzi? W przypadku monolitu mamy de facto doczynienia z jedną, dużą usługą, która odpowiedzialna jest za obsługę całego systemu. W praktyce oznacza to, że jej niedostępność spowodowana np. podgrywaniem wersji lub awarią, jest równoznaczna niedostępnością całego serwera (i tym samym aplikacji). Sprawa ma się zgoła odmiennie w przypadku mikroserwisów, ponieważ dzięki modularyzacji posiadamy wiele, niezależnych usług, z których każda odpowiedzialna jest tylko za mały fragment systemu. Zatem, jeżeli jedna z takich usług nie będzie dostępna to w najgorszym przypadku tylko mały fragment naszej aplikacji nie powinien działać. W teorii wszystko brzmi super… a potem przychodzi CAP:

 

 

Trzy składowe tego schematu to:

  • Consistency (pol. spójność) – każdy odczyt z systemu skutkować będzie pobraniem najnowszych danych lub otrzymaniem błędu
  • Availability (pol. dostępność) – każdy odczyt z systemu skutkować będzie pobraniem danych, ale bez gwarancji że są one najnowsze
  • Partition tolerance (pol. ?) – system działa mimo zakłócenia propagacji wiadomości między węzłami

Jeżeli kiedykolwiek widziałeś podobny schemat to zapewne gdzieś obok umieszczona była cięta riposta „wybierz dwa!”. To trochę jak w tym starym żarcie:

 

Przychodzi klient do programisty i mówi:

– chciałbym tani i dobry system

Programista na to:

– A po co panu dwa systemy?

 

Przepraszam, musiałem 🙂 Tak czy siak, moim zdaniem powinniśmy patrzeć na ten magiczny trójkąt z trochę innej perspektywy. Nie jako „wybierz dwa” bo zakładanie, że problemy natury sieciowej nie będą się pojawiać tylko dlatego, że „ja tak mówię” jest nadto optymistyczne. Bardziej powinniśmy rozważać to w ten sposób – jak powinien zachować się system w przypadku gdy problemy sieciowe między węzłami wystąpią? Czy odpytany o dane powinienem zwrócić to co aktualnie znajduje się bazie danych (dostępność) czy zwrócić błąd, ale mieć pewność, że stare dane nie zostały zwrócone do klienta (spójność). Odpowiedź na to pytanie znów w głównym stopniu brzmi od charakterystyki systemu. Kiedyś słyszałem nawet bardzo trafne porównanie. Reddit jest przykładem portalu, dla którego pierwsza opcja ma większy sens. Co się stanie jeżeli kilka tysięcy osób zobaczy wpisy sprzed kilku minut, a nie najnowsze? Nic. Ludzie najprawdopodobniej tego nie zauważą, a przynajmniej nie będą narzekać, że coś nie działa. Dla porównania weźmy systemy medyczne, które przechowują dane pacjentów. W tym przypadku kluczową rolę odgrywa spójność danych, ponieważ nie można pozwolić sobie na sytuację, w której pacjent otrzymuje np. dwie dawki leków tylko dlatego, że system nie wyświetlił aktualnych danych. Widzisz więc, że mikroserwisy znów komplikują nam życie, przedstawiając problemy o których nie myśleliśmy nawet pisząc monolity.

 

Mikroserwisy == legendarny development

Ostatni akapit z tej wyliczanki chciałbym poświecić nietechnicznemu aspektowi. Wielu programistom ślinka cieknie od samej informacji zawartej w ofercie pracy, że będą oni pracowali przy mikroserwisach. O ile mogę się zgodzić, że fajnie jest się rozwijać w tym kierunku i eksplorować tą tematykę, ponieważ jest bardzo rozległa i ciekawa, o tyle muszę powiedzieć to otwarcie – praca z tym rodzajem architektury nie należy do najprzyjemniejszych. Szczególnie odczuwalne jest to na UNIX-owych systemach, które nie posiadają VS. Po pierwsze uruchamianie systemu już wymaga od nas dużo więcej pracy niż zwykłe kliknięcie „Build & Run” w VS, ponieważ musimy najpierw uruchomić naszą infrastrukturę (bazy danych, kolejkę itd.), a następnie N usług jednocześnie. W tym miejscu możemy albo:

  • otworzyć N konsol i uruchomić usługi poprzez dotnet CLI
  • napisać skrypt, który zrobi to za nas
  • uruchomić to poprzez docker compose

W niedalekiej przyszłości planuję publikację wpisu zawierającego kompletną instrukcję uruchomienia projektu DShop, która dokładniej przedstawi dwie, ostatnie metody. Oczywiście to nie koniec niespodzianek! Do tego dochodzą problemy z:

  • debugowaniem konkretnych mikroserwisów
  • utrzymywaniem spójności między usługami
  • commitowaniem zmian dla GITa. Znów, albo piszemy własne skrypty, albo myślimy o submodułach GITa.

 

A to dopiero początek….

Jak zapewne się domyślasz to co dziś przedstawiłem jest zaledwie czubkiem góry lodowej i to bardzo rozległej. Nie poruszyłem takich zagadnień jak partycjonowaniu danych, obsługa niedostępności konkretnych serwisów, modele wysyłania wiadomości, czy problemy natury DevOps jak orkiestracja.
Czy oznacza to, że mikroserwisy są zatem złem koniecznym i powinniśmy od nich stronić? Oczywiście, że nie. Moim celem było jedynie zwrócenie Twojej uwagi na fakt, że wszystkie gloryfikowane cechy tego podejścia mają swoją cenę, która często pogrąża nie jednego programistę. Z tego względu przed pochopnym pakowaniem się w mikroserwisy, zadaj sobie pytanie czy faktycznie jesteś zmuszony do wytaczania tak ciężkiego oręża… zwłaszcza gdy ma być to system dla Pani Krysi z lokalnego spożywczaka.

You may also like...

  • Mateusz

    Świetny artykuł, czekam na kolejne związane z mikroserwisami 🙂

  • Pingback: dotnetomaniak.pl()

  • A propos sekcji „Mikroserwisy == asynchroniczna komunikacja”. Jeżeli już ktoś idzie w mikrousługi to powinien kojarzyć takie pojęcia jak saga czy command sourcing (http://microservices.io/patterns/data/saga.html i http://allegro-restapi-guideline.readthedocs.io/en/latest/CommandPattern/) to rozwiązuje problem z tym że operacja na etapie Ntym się nie powiedzie. Walidacja zawsze powinna być wykonana na GUI (przykład gmail który sprawdza dostępność email’a już podczas wpisywania), API gateway (kolejna śmieszna nazwa) powinien przekazać żądanie walidacji do odpowiedniej usługi.

    To co piszesz bardzo przypomina mi artykuł z wiki Fowlera https://martinfowler.com/bliki/MicroservicePrerequisites.html. Z własnego doświadczenia dodam że pisanie w podejściu mikrousługowym podnosi poprzeczkę programistom, debugowanie jest o wiele trudniejsze (a jak nie ma infrastruktury do logów i metryk to praktycznie niemożliwe), design również. Co nie znaczy że nie ma korzyści. Za każdym razem jak buduje pojedynczy serwis czy puszczam testy to dzieje się to w mgnieniu oka (nowet z R#), buildy i deply’e są bardzo szybkie. Ale największa korzyść to sposób pracy i podziału na zespoły. Gdy uda się dobrze podzielić domenę na bounded contexty (DDD) i zbudować zespoły wokół BC to jest to naprawdę duża ulga. Każdy zespół zajmuje się swoim „podwórkiem”, komunikacja między zespołami może być znacznie ograniczona (jest wsteczna kompatybilność api + wersjonowanie na stykach BC, BC są chronione przed wyciekiem wiedzy domenowej do innych części aplikacji). I to jest dla mnie największa korzyść z mikrousług.

    • PS. A co do sekcji „Mikroserwisy == legendarny development”, hmmm… Docker + docker-compose?

      • Docker + Docker Compose – jasne, nie wyobrażam sobie innej opcji przy CI/CD i wdrażaniu tego na serwer. Natomiast bardziej chodziło mi o lokalny development (szczególnie jeżeli w gre wchodzi wczesniej wspomniane debugowanie). W tym przypadku bardziej używam
        Dockera do szybkiego postawienia lokalnej infrastruktury dla moich usług.

        • Pozostaje jeszcze opcja że masz postawione „żywe” środowisko developerskie obok UAT z wersjami wszystkich usług z master’a i to z nim się komunikujesz. Przy wersjonowaniu api działa to całkiem nieźle (sam używam), wada jest taka że jak nie masz internetu to nie ma developmentu (i wtedy Docker się przydaje). Wada rozwiązania z dockerem to np. do niedawna SQL server w ogóle na dokerze nie śmigał, wiele usług np. Azure CosmosDB postawione lokalnie jest dość mocno zasobożerne (+1GB RAM).

    • Saga jest dobrym rozwiązaniem jeżeli chcemy modelować transakcje rozproszone na kilka usług, ale dalej pozostaje nam temat komunikacji z użytkownikiem (jak web sockety). Co do walidacji na GUI to w większości przypadków jest to ok, choć istnieje szansa, że coś zostanie uznane za dane poprawne mimo, że nie powinny ( przez eventual constistency). Znów to od systemu zależy czy warto się tym zajmować. Sam Greg Young mówił, że jeżeli masz obsługiwać przypadek, który ma szanse na zajście ~1% to zazwyczaj warto odpuścić.

      Z debugowaniem się zgadzam w 100% 😀

  • Random Guy

    Nawet spoko sie czytało ale ostatni akapit mnie wkurwił i wszystko zepsuł.
    Jeżeli mikroserwisy są złem koniecznym to powinniśmy zaakceptować to, że ich development jest trudny, ale korzyści płynące z tego rozwiązania są dużo większe. Stronienie od zła koniecznego nie ma sensu…

    • Po pierwsze trochę kultury w komentarzach.
      Po drugie, polecam czytać uważnie tekst – „Czy oznacza to, że mikroserwisy są zatem złem koniecznym i powinniśmy od nich stronić? OCZYWIŚCIE, ŻE NIE.”. Nigdzie nie napisałem, że mamy stronić od mikroserwisów tylko dlatego, że próg wejścia jest większy niż w przypadku np. monolitów. Tak jak w podsumowaniu – moją intencją było jedynie zwrócenie uwagi na to, że wiele dobrego mówi się na konferencjach o tym podejściu, a dużo mniej o istotnych aspektach, które jednak zwiększają trudność implementacji lub nawet samego projektowania aplikacji.

  • Graty za ten artykuł, naprawdę nieźle zebrane kompendium wiedzy. Prowadzę prezentacje o DDD i Mikroserwisach to, jesli pozwolisz, wyrzuciłbym w materiały link do twojego postu.