Od monolitu do mikrousług – migrować czy nie migrować?

Mikrousługi (ang. microservices) to styl architektoniczny zorientowany na szybkość rozwoju oprogramowania, rozumianą jako  liczba funkcjonalności tworzonych w jednostce czasu oraz czasochłonność całego procesu wytwórczego – od koncepcji do wdrożenia (time to market). Coraz większej popularności podejścia mikrousługowego sprzyja obecna wysoka zmienność otoczenia biznesowego, co zmusza firmy do szybkiego reagowania celem uniknięcia sytuacji kiedy  dobre rozwiązanie wdrożone zbyt późno staje się złym rozwiązaniem.

Większość funkcjonujących obecnie systemów klasy enterprise posiada architekturę monolityczną. Ich niezaprzeczalną zaletą jest oczywiście to, że działają i zapewniają posiadającym je organizacjom przychody lub oszczędności. Architektura monolityczna powoduje jednak, że wraz z rozrostem tych systemów stopniowo maleje tempo ich rozwoju. Właściciele biznesowi muszą coraz dłużej czekać na zamówione funkcjonalności. Co gorsza, skalowalność procesu tworzenia oprogramowania okazuje się być daleka od liniowej. Angażowanie do pracy przy takich systemach kolejnych ludzi lub zespołów, przynosi coraz mniejsze korzyści. Wprowadzanie nowych pracowników trwa coraz dłużej, a obecni tracą motywację i żądają dodatków za pracę w trudnych warunkach lub zaczynają rozważać ścieżkę kariery poza organizacją. Takie symptomy wskazują wyraźnie, że architektura systemu przestała spełniać wymagania przedsiębiorstwa. Rozwiązaniem problemu niedostatecznego tempa rozwoju systemu jest zastosowanie architektury ewolucyjnej – mikrousług.

Aby zobrazować efekt zastosowania podejścia mikrousługowego możemy posłużyć się pewną metaforą. Załóżmy, że chcemy zbudować i utrzymać stację kosmiczną. W tym celu będziemy potrzebowali regularnie dostarczać na orbitę różnego rodzaju ładunki: ludzi, materiały, sprzęt itp. Obecnie jedyną dostępną formą transportu, są loty kosmiczne, które pomimo ostatnich osiągnięć firm takich jak SpaceX, są bardzo drogie i wymagają czasochłonnych przygotowań. Możemy więc spróbować opracować inne rozwiązanie – windę kosmiczną. Nakłady na jej budowę z pewnością będą wielokrotnie większe od kosztu pojedynczego lotu, ale każdy kolejny transport będzie możliwy w zasadzie od ręki i praktycznie za darmo (w porównaniu do kosztów lotu).

Powyższa metafora, oprócz zobrazowania wizji świetlanej przyszłości, jaką obiecują nam mikrousługi pozwala poczynić inną istotną refleksję. Mianowicie wdrożenie takiego podejścia jest sporym wyzwaniem i wymaga znaczącej inwestycji. A więc zanim zaczniemy budować windę kosmiczną powinniśmy się upewnić, że potrzebujemy dostać się na orbitę i że będziemy się tam wybierać często. W przeciwnym razie całe przedsięwzięcie stanie się jedynie sztuką dla sztuki.

Migrować czy nie migrować?

Przed podjęciem decyzji o zastosowaniu podejścia mikrousługowego należy rozważyć kilka kwestii:

  • czy produkt (system) uzyskał potwierdzenie rynkowe,
  • czy oczekiwane tempo rozwoju produktu będzie wymagać zaangażowania więcej niż jednego zespołu (~10 osób),
  • czy system posiada wysokie wymagania dotyczące niezawodności i skalowalności bądź czy są one istotnie zróżnicowane pomiędzy poszczególnymi jego elementami?

Moment, w cyklu życia systemu, w którym te kryteria zostaną spełnione jest optymalnym do podjęcia decyzji o zastosowaniu podejścia mikrousługowego.

Pamiętać przy tym należy, że podejście mikrousługowe ma swoje granice stosowalności – przykładowo nie należy go stosować w przypadku systemów czasu rzeczywistego.

Architektura mikrousługowa

Sam Newman, powszechnie uznawany za twórcę koncepcji architektury mikrousługowej, sformułował następującą jej definicję:

“small autonomous services modelled around business domain that work together

Wynika z niej, że podstawowym elementem konstrukcyjnym w tym podejściu są usługi, które będziemy wyodrębniać stosując dekompozycję według obszarów (zdolności) biznesowych (ang. business capabilities). Usługi te mogą być niezależnie rozwijane i wdrażane, ale muszą ze sobą współpracować w celu realizacji procesu biznesowego.

Anatomia usługi

Jeśli komponent, będący częścią systemu, tylko przechowuje dane, to zasadniczo jest bazą danych, jeżeli zawiera samą logikę – nazywamy go funkcją. Usługa natomiast z założenia zawiera oba te elementy –  logikę i dane. Takie połączenie jest właśnie podstawą autonomii na którą zwraca uwagę Sam Newman. Podchodząc do problemu dekompozycji systemu, warto jest mieć tę definicję z tyłu głowy.

Linie podziału

Poszczególne style architektoniczne różnią się między sobą sposobami wyodrębniania komponentów. Istotną cechą architektury mikrousługowej jest zorganizowanie funkcjonalności usług wokół zdolności biznesowych (ang. business capabilities), co pozwala zapewnić ich wewnętrzną spójność (ang. high cohesion) oraz stabilność uzyskanego podziału. Taką metodę okiełznania złożoności logiki biznesowej spopularyzował Eric Evans pod nazwą “Domain Driven Design”. Opisuje ona sposób podziału dziedziny na poddziedziny a następnie wyodrębnienia w nich kontekstów ograniczonych, które posłużą nam jako granice usług.

Praktyczną techniką identyfikacji kontekstów ograniczonych jest “Event Storming” zaproponowany przez Alberto Brandolini’ego. Jej pierwszy krok polega na zidentyfikowaniu zdarzeń zachodzących w domenie biznesowej. Takie podejście pozwala zorientować proces modelowania na zachowanie zamiast skupiać go na statycznej strukturze przetwarzanych informacji. Ta, z pozoru subtelna  zmiana perspektywy, jest kluczowa dla architektury mikrousługowej, ponieważ umożliwia zaprojektowanie systemu cechującego się niskim sprzężeniem i dużą autonomią jego usług składowych (ang. loose coupling).

Wyznaczając granice usług trzeba pamiętać, że staną się one granicami transakcji i silnej, natychmiastowej spójności (ACID). Dla operacji angażujących kilka usług system będzie posiadał charakterystykę BASE (Basically Available, Soft state, Eventual consistency), która oferuje gwarancję żywotności (ang. liveness) ale nie gwarantuje bezpieczeństwa (ang. safety). W odróżnieniu od ACID, BASE oznacza, że system osiągnie w końcu spójność, ale nie wiadomo, jak będzie ten stan wyglądał ani, jak będzie się system zachowywał w międzyczasie. Istnieje możliwość uzyskania gwarancji bezpieczeństwa (strong eventual consistency) w modelu BASE, bez wykorzystywania tradycyjnych mechanizmów kontroli współbieżności. Wymaga to jednak zastosowania tak zwanych CRDT (conflict-free replicated data types).

Świat rzeczywisty nie jest transakcyjny. Użytkownicy stosunkowo rzadko wymagają natychmiastowej spójności. Za przykład może tu posłużyć domena finansów, która wydawałoby się, powinna stawiać najwyższe wymagania dotyczące spójności. Wszyscy jednak jesteśmy przyzwyczajeni do faktu, że realizacja przelewów międzybankowych trwa godziny lub nawet dni, a w trakcie operacji nie wiadomo co się dzieje ze środkami – nie widać ich ani na rachunku źródłowym, ani docelowym.

Często sami inżynierowie oprogramowania wykazują tendencję do forsowania natychmiastowej spójności tam, gdzie nie jest ona konieczna, a czasami może być wręcz szkodliwa. Zetknąłem się kiedyś z pewnym rozbudowanym systemem do zarządzania przedsiębiorstwem. W momencie przyznania klientowi statusu VIP w module CRM, podsystem logistyczny generował automatycznie zlecenie wysyłki butelki szampana, która miała dodatkowo podkreślić wyróżnienie. Cała operacja była wykonywana w jednej transakcji. Spowodowało to, że w momencie wyczerpania zasobów magazynowych szampana próba złożenia zlecenia wysyłki kończyła się błędem, cała transakcja była odwijana i w efekcie nie można było ustawić klientowi statusu VIP. Podzielenie operacji na dwie transakcje i zastosowanie wzorca sagi, byłoby w tym przypadku, lepszym rozwiązaniem.

Reasumując, należy więc zweryfikować, jakie gwarancje spójności są wymagane dla poszczególnych funkcjonalności i upewnić się, że ustalone granice zabezpieczają te wymagania.

Przygotowanie

Wprowadzenie architektury mikrousługowej niesie ze sobą szereg wyzwań, które należy zaadresować przed rozpoczęciem procesu transformacji. Tworzony w ten sposób system wymagać będzie automatyzacji procesów budowania, konfiguracji, testowania i wdrażania. Niezbędne staną się również narzędzia do gromadzenia i agregacji logów oraz metryk a także analizy zachowania (śledzenia, profilowania, etc) w środowisku rozproszonym.

Na wczesnym etapie procesu transformacji konieczne jest ustalenie sposobu integracji i koordynacji usług, architektury danych, metod zapewnienia spójności transakcyjnej i niezawodności, konfiguracji, service discovery oraz innych aspektów wskroś-funkcjonalnych. Wprowadzanie lub zmiana tych rozwiązań na późniejszych etapach transformacji będzie znacznie kosztowniejsza niż na jej początku.

Wspomniane powyżej zagadnienia są na tyle istotne i szerokie, że zasługują na poświęcenie im osobnego artykułu . Zaniedbanie zwłaszcza obszaru integracji może skutkować brakiem autonomii usług i spowodować, że w efekcie zamiast spodziewanej architektury mikrousługowej otrzymamy rozproszony monolit.

Ewolucja czy rewolucja?

Kiedy mamy już wyznaczone granice usług, które będą tworzyły docelową strukturę rozwiązania musimy zdecydować, w jaki sposób przeprowadzić transformację: czy będziemy modernizować system etapowo, stopniowo wydzielając kolejne komponenty, czy stworzymy cały system od od podstaw i przekażemy do eksploatacji po zakończeniu całej operacji. Drugi wariant jest z pewnością dużo łatwiejszy i bardziej kuszący, jednak w praktyce, w większości przypadków nieakceptowalny. W warunkach silnej konkurencji i dużej dynamiki rynku niewiele firm jest w stanie sobie pozwolić na wstrzymanie rozwoju systemu informatycznego, od którego zależą kluczowe procesy biznesowe, na dłuższy okres czasu. Jeśli przy okazji modernizacji architektury, chcielibyśmy dodatkowo zmienić technologię lub kluczowy framework, to  pierwsze podejście również nie będzie miało zastosowania. W takiej sytuacji możemy zastosować podejście nazywane zaduszaniem (ang. strangler pattern).

Transformacja

W jaki sposób możemy wykonać stopniową transformację do architektury mikrousługowej?

Jako punkt wyjścia dla tego procesu przyjmijmy system o strukturze monolitycznej:

From Monolith to Microservices

Pierwszym krokiem jest częściowe logiczne rozdzielenie interfejsu użytkownika od warstwy usług. Obsługa poleceń logiki biznesowej jest delegowana do warstwy usług, a zapytania służące do zasilenia widoków są kierowane bezpośrednio do bazy danych. Na razie nie modyfikujemy samej bazy danych:

From Monolith to Microservices

Drugi krok to pełna separacji logiczna interfejsu użytkownika:

From Monolith to Microservices

W trzecim kroku należy wykonać separację fizyczną interfejsu użytkownika a po stronie backendu udostępnić API i wykorzystać je do komunikacji pomiędzy rozdzielonymi komponentami:

From Monolith to Microservices

Czwarty krok polega na stopniowym wydzielaniu kolejnych usług. Tym razem wykonujemy pełną separację – do poziomu bazy danych włącznie. Jeżeli organizacja nie stosowała wcześniej architektury mikrousługowej dobrze jest rozpocząć dekompozycję od obszaru małego i prostego do wyodrębnienia . To pozwoli zespołowi w krótkim czasie i przy relatywnie niskim ryzyku zdobyć niezbędne doświadczenie.

From Monolith to Microservices

Ostatnim krokiem jest dekompozycja frontendu, którą również możemy wykonywać etapowo – stopniowo wyodrębniając kolejne fragmenty interfejsu użytkownika:

From Monolith to Microservices

Niewątpliwą zaletą tego procesu jest jego ewolucyjny charakter. Za jego pomocą możemy zmieniać architekturę stopniowo, bez całkowitego zatrzymywania rozwoju systemu, dostosowując tempo zmian do wymagań biznesowych i dostępnych zasobów.

Kontekst

Architektura systemu nie dryfuje w próżni. Jest silnie powiązana z procesem wytwórczym oraz strukturą i kulturą organizacji. Kluczową cechą architektury mikrousługowej jest jej ewolucyjność. Oznacza to, że wybór tego podejścia przyniesie największe korzyści w organizacji zbudowanej z małych, autonomicznych zespołów stosujących zwinne metodyki tworzenia oprogramowania. Brak tych cech nie powinien nas jednak powstrzymywać przed wyborem tej architektury. Możemy je zaadoptować równocześnie z procesem modernizacji architektury. Nie możemy natomiast ich zupełnie zignorować. Zgodnie z prawem Conwaya:

“…organizations which design systems … are constrained to produce designs which are copies of the communication structures of these organizations.”

Jeśli zatem wprowadzimy podejście mikrousługowe w silnie scentralizowanej, hierarchicznej organizacji istnieje ryzyko, że z biegiem czasu architektura naszego systemu zacznie ponownie dążyć w stronę monolitu. Dlatego też proces modernizacji architektury systemu powinniśmy rozpocząć od przeprowadzenia tzw. Odwróconego Manewru Conwaya (ang. Inversed Conway Maneuvre), czyli stworzenia struktury organizacyjnej izomorficznej z oczekiwaną docelową architekturą systemu. Kiedy usuniemy tradycyjne silosy funkcjonalne (frontend dev, backend dev, dba, qa, ops, etc), a w ich miejsce powołamy skupione wokół strumieni wartości (ang. value streams) i zdolności biznesowych (ang. business capabilities) samodzielne zespoły wielofunkcyjne (ang. cross-functional teams), znacznie łatwiej będzie nam w analogiczny sposób zdekomponować system a następnie utrzymać uzyskaną architekturę.

“Design the organisation you want, the architecture will follow (kicking and screaming).”

Evan Bottcher, ThoughtWorks

Kryteria sukcesu

Kiedy zakończymy proces transformacji, warto sprawdzić, czy udało się nam uzyskać zakładany efekt. Czy to co osiągnęliśmy można nazwać sukcesem. Celem wdrożenia architektury mikrousługowej, jest przede wszystkim usprawnienie procesów związanych z rozwojem i utrzymaniem oprogramowania. Możemy to zmierzyć za pomocą kilku prostych wskaźników, np:

  • czasochłonność cyklu wytwórczego rozumiana jako przeciętny czas od koncepcji do wdrożenia (time to market);
  • wydajność procesu wytwórczego mierzona jako średnia liczba funkcjonalności (user stories) dostarczanych przez zespół (ew. per członek zespołu) w jednostce czasu;
  • skalowalność procesu wytwórczego mierzona jako zmiana wydajności procesu wytwórczego w funkcji rozmiaru zespołu i liczby zespołów;
  • przeciętny czas potrzebny do zlokalizowania i usunięcia usterki (mean time to repair).

Porównując wartości tych charakterystyk dla starej i nowej charakterystyki możemy ocenić efekt przeprowadzonej transformacji. Wymienione powyżej wskaźniki możemy monitorować również w trakcie procesu transformacji.

Tu, w Altkom Software & Consulting, jako inżynierowie fascynujemy się rozwiązaniami, które pozwalają udoskonalać otaczającą nas rzeczywistość. Mamy jednak świadomość, że każda zmiana, to inwestycja, która musi się zwrócić.

 

Autor: Robert Kuśmierek, Lead Software Engineer, ASC LAB