Building Domain Driven Microservices

Chandra Ramalingam

Follow

Jul 1, 2020 – 18 min read

Image credits:

Termin „mikro” w Microservices, choć wskazuje na rozmiar usługi, nie jest jedynym kryterium, które sprawia, że aplikacja jest Microservice. Kiedy zespoły przechodzą na architekturę opartą na mikroserwisach, ich celem jest zwiększenie zwinności – autonomiczne i częste wdrażanie funkcjonalności. Ciężko jest stworzyć jedną zwięzłą definicję tego stylu architektonicznego. Spodobała mi się ta krótka definicja od Adriana Cockcrofta – „architektura zorientowana na usługi, składająca się z luźno sprzężonych elementów, które mają ograniczone konteksty.”

Chociaż definiuje to heurystykę projektową wysokiego poziomu, architektura mikroserwisów ma pewne unikalne cechy, które odróżniają ją od architektury zorientowanej na usługi sprzed lat. Kilka z tych cech, poniżej. Te i kilka innych są dobrze udokumentowane – artykuł Martina Fowlera i Sam Newman’s Building Microservices, aby wymienić tylko kilka.

  1. Usługi mają dobrze zdefiniowane granice skupione wokół kontekstu biznesowego, a nie wokół arbitralnych abstrakcji technicznych
  2. Ukrywają szczegóły implementacji i ujawniają funkcjonalność poprzez interfejsy ujawniające intencje
  3. Usługi nie dzielą się swoimi wewnętrznymi strukturami poza swoimi granicami. Na przykład, brak współdzielenia baz danych.
  4. Usługi są odporne na awarie.
  5. Zespoły posiadają swoje funkcje niezależnie i mają możliwość autonomicznego wprowadzania zmian
  6. Zespoły przyjmują kulturę automatyzacji. Na przykład, zautomatyzowane testy, ciągła integracja i ciągłe dostarczanie

W skrócie, możemy podsumować ten styl architektury jak poniżej:

Luźno sprzężona architektura zorientowana na usługi, gdzie każda usługa jest zamknięta w dobrze zdefiniowanym kontekście, umożliwiając szybkie, częste i niezawodne dostarczanie aplikacji.

Domain-driven design and Bounded contexts

Siła mikroserwisów pochodzi z jasnego zdefiniowania ich odpowiedzialności i wyznaczenia granicy między nimi. Celem jest tutaj zbudowanie wysokiej spójności w obrębie granicy i niskiego sprzężenia poza nią. Oznacza to, że rzeczy, które mają tendencję do zmiany razem, powinny należeć do siebie. Jak w przypadku wielu rzeczywistych problemów, łatwiej powiedzieć niż zrobić – firmy ewoluują, a założenia się zmieniają. Stąd zdolność do refaktoryzacji jest kolejną krytyczną rzeczą do rozważenia przy projektowaniu systemów.

Domain-driven design (DDD) jest kluczowym i naszym zdaniem niezbędnym narzędziem przy projektowaniu mikroserwisów, niezależnie od tego, czy chodzi o rozbicie monolitu, czy wdrożenie projektu greenfield. Domain-driven design, rozsławiony przez Erica Evansa jego książką, jest zbiorem idei, zasad i wzorców, które pomagają projektować systemy oprogramowania w oparciu o bazowy model domeny biznesowej. Programiści i eksperci domeny pracują razem, aby stworzyć modele biznesowe we wszechobecnym wspólnym języku. Następnie wiążą te modele z systemami, w których mają one sens, ustanawiają protokoły współpracy pomiędzy tymi systemami i zespołami, które pracują nad tymi usługami. Co ważniejsze, projektują konceptualne kontury lub granice pomiędzy systemami.

Projektowanie mikroserwisów czerpie inspirację z tych koncepcji, ponieważ wszystkie te zasady pomagają budować systemy modułowe, które mogą się zmieniać i ewoluować niezależnie od siebie.

Zanim przejdziemy dalej, szybko przejrzyjmy niektóre z podstawowych terminologii DDD. Pełny przegląd Domain-Driven Design jest poza zakresem tego bloga. Gorąco polecamy książkę Erica Evansa każdemu, kto próbuje budować mikroserwisy

Domena: Reprezentuje to, co robi dana organizacja. W poniższym przykładzie będzie to Retail lub eCommerce.

Subdomain: Organizacja lub jednostka biznesowa w ramach organizacji. Domena składa się z wielu subdomen.

Język wszechobecny: Jest to język używany do wyrażania modeli. W poniższym przykładzie Item jest Modelem, który należy do języka wszechobecnego każdej z tych subdomen. Programiści, menedżerowie produktu, eksperci domeny i interesariusze biznesowi zgadzają się na ten sam język i używają go w swoich artefaktach – kodzie, dokumentacji produktu itd.

Fig 1. Poddomeny i Konteksty ograniczone w domenie eCommerce

Konteksty ograniczone: Domain-driven design definiuje Bounded contexts jako „Ustawienie, w którym pojawia się słowo lub stwierdzenie, które określa jego znaczenie.” W skrócie oznacza to granicę, w ramach której model ma sens. W powyższym przykładzie, „Pozycja” nabiera innego znaczenia w każdym z tych kontekstów. W kontekście Catalog, Item oznacza produkt, który można sprzedać, natomiast w kontekście Cart, oznacza przedmiot, który klient dodał do swojego koszyka. W kontekście Fulfillment, oznacza on pozycję magazynową, która zostanie wysłana do klienta. Każdy z tych modeli jest inny, każdy ma inne znaczenie i może zawierać inne atrybuty. Poprzez oddzielenie i wyizolowanie tych modeli w ich odpowiednich granicach, możemy wyrazić modele swobodnie i bez dwuznaczności.

Uwaga: Istotne jest, aby zrozumieć rozróżnienie pomiędzy Subdomenami i Kontekstami ograniczonymi. Subdomena należy do przestrzeni problemu, czyli tego, jak Twoja firma widzi problem, podczas gdy Bounded contexts należą do przestrzeni rozwiązania, czyli tego, jak wdrożymy rozwiązanie problemu. Teoretycznie każda subdomena może mieć wiele kontekstów związanych, chociaż dążymy do jednego kontekstu związanego na subdomenę.

Jak Microservices są związane z kontekstami związanymi

Teraz, gdzie pasują Microservices? Czy uczciwie jest powiedzieć, że każdy związany kontekst mapuje do mikroserwisu? Tak i nie. Zobaczymy dlaczego. Mogą być przypadki, w których granica lub kontur twojego kontekstu jest dość duży.

Fig 2. Kontekst związany i mikroserwisy

Rozważmy powyższy przykład. Kontekst ograniczony Pricing posiada trzy odrębne modele – Price, Priced items oraz Discounts, z których każdy odpowiada odpowiednio za cenę pozycji katalogowej, obliczenie całkowitej ceny listy pozycji oraz zastosowanie rabatów. Moglibyśmy stworzyć jeden system, który obejmowałby wszystkie powyższe modele, ale mógłby on stać się nieracjonalnie dużą aplikacją. Każdy z modeli danych, jak wspomniano wcześniej, ma swoje inwarianty i reguły biznesowe. Z czasem, jeśli nie będziemy ostrożni, system może stać się wielką kulą błota z zaciemnionymi granicami, nakładającymi się obowiązkami i prawdopodobnie z powrotem do miejsca, w którym zaczęliśmy – monolitu.

Innym sposobem modelowania tego systemu jest oddzielenie lub zgrupowanie powiązanych modeli w oddzielne mikroserwisy. W DDD, te modele – Cena, Pozycje w Cenie i Rabaty – są nazywane agregatami. Agregat jest niezależnym modelem, który składa się z powiązanych modeli. Możesz zmienić stan agregatu tylko poprzez opublikowany interfejs, a agregat zapewnia spójność i to, że inwarianty trzymają się dobrze.

Formalnie, Agregat jest klastrem powiązanych obiektów traktowanych jako jednostka dla zmian danych. Odniesienia zewnętrzne są ograniczone do jednego członka AGGREGATU, oznaczonego jako korzeń. W granicach AGGREGATU obowiązuje zestaw reguł spójności.

Fig 3. Microservices in the Pricing Context

Ponownie, nie jest konieczne modelowanie każdego agregatu jako odrębnej mikroserwisu. Okazało się, że tak jest w przypadku usług (agregatów) z rys. 3, ale nie musi to być regułą. W niektórych przypadkach może mieć sens hostowanie wielu agregatów w jednej usłudze, szczególnie gdy nie do końca rozumiemy domenę biznesową. Należy pamiętać, że spójność może być zagwarantowana tylko w obrębie jednego agregatu, a agregaty mogą być modyfikowane tylko poprzez opublikowany interfejs. Każde naruszenie tych zasad niesie ze sobą ryzyko zamienienia się w wielką kulę błota.

Mapy kontekstowe – Sposób na wyrzeźbienie dokładnych granic mikroserwisów

Kolejnym niezbędnym zestawem narzędzi w twoim arsenale jest koncepcja map kontekstowych – ponownie, z Domain Driven Design. Monolit składa się zazwyczaj z rozbieżnych modeli, w większości ściśle ze sobą powiązanych – modele być może znają intymne szczegóły jednego drugiego, zmiana jednego może spowodować efekty uboczne na innym itd. W miarę rozpadu monolitu, ważne jest, aby zidentyfikować te modele – w tym przypadku agregaty – i ich relacje. Mapy kontekstowe pomagają nam to zrobić. Są one używane do identyfikacji i definiowania relacji pomiędzy różnymi ograniczonymi kontekstami i agregatami. Podczas gdy ograniczone konteksty definiują granice modelu – Cena, Rabaty, itd. w powyższym przykładzie, mapy kontekstowe definiują zależności pomiędzy tymi modelami oraz pomiędzy różnymi kontekstami. Po zidentyfikowaniu tych zależności, możemy określić właściwy model współpracy pomiędzy zespołami, które będą implementować te usługi.

Pełna eksploracja map kontekstowych wykracza poza zakres tego bloga, ale zilustrujemy to na przykładzie. Poniższy diagram reprezentuje różne aplikacje, które obsługują płatności dla zamówienia eCommerce.

  1. Kontekst koszyka zajmuje się autoryzacją online zamówienia; Kontekst zamówienia przetwarza procesy płatności po realizacji zamówienia, takie jak Rozliczenia; Centrum kontaktowe obsługuje wszelkie wyjątki, takie jak ponawianie prób płatności i zmiana metody płatności użytej w zamówieniu
  2. Dla uproszczenia, załóżmy, że wszystkie te konteksty są zaimplementowane jako oddzielne usługi
  3. Wszystkie te konteksty enkapsulują ten sam model.
  4. Zauważ, że te modele są logicznie takie same. Oznacza to, że wszystkie są zgodne z tym samym językiem domeny Ubiquitous – metody płatności, autoryzacje i rozliczenia. Tyle tylko, że są częścią różnych kontekstów.

Innym znakiem, że ten sam model jest rozrzucony po różnych kontekstach jest to, że wszystkie one integrują się bezpośrednio z jedną bramką płatniczą i wykonują te same operacje co jedna

Fig 4. Nieprawidłowo zdefiniowana mapa kontekstów

Redefiniowanie granic usług – Mapowanie agregatów do właściwych kontekstów

W powyższym projekcie (Rys. 4) jest kilka problemów, które są bardzo widoczne. Płatny agregat jest częścią wielu kontekstów. Niemożliwe jest wymuszenie inwariantów i spójności pomiędzy różnymi usługami, nie wspominając już o problemach z współbieżnością pomiędzy tymi usługami. Na przykład, co się stanie, jeśli contact center zmieni metodę płatności powiązaną z zamówieniem, podczas gdy usługa Orders próbuje zaksięgować rozliczenie wcześniej złożonej metody płatności. Zauważ również, że każda zmiana w bramce płatności wymusiłaby zmiany w wielu usługach i potencjalnie wielu zespołach, ponieważ różne grupy mogłyby być właścicielami tych kontekstów.

Po wprowadzeniu kilku poprawek i wyrównaniu agregatów do właściwych kontekstów otrzymujemy znacznie lepszą reprezentację tych subdomen – rys. 5. Zmieniło się bardzo dużo. Prześledźmy te zmiany:

  1. Agregat Płatności ma nowy dom – usługę Płatności. Usługa ta abstrahuje również bramę płatności od innych usług, które wymagają usług płatniczych. Ponieważ pojedynczy kontekst jest teraz właścicielem agregatu, inwarianty są łatwe do zarządzania; wszystkie transakcje odbywają się w obrębie tej samej granicy usługi, co pomaga uniknąć problemów ze współbieżnością.
  2. Agregat płatności używa warstwy antykorupcyjnej (ACL), aby odizolować główny model domeny od modelu danych bramki płatności, który jest zazwyczaj dostawcą zewnętrznym i może ulec zmianie. W przyszłym poście zagłębimy się w projekt aplikacji wykorzystującej wzorzec Ports and Adapters. Warstwa ACL zazwyczaj zawiera adaptery, które przekształcają model danych bramy płatniczej na zbiorczy model danych usługi Płatności.
  3. Usługa koszyka wywołuje usługę Płatności za pomocą bezpośrednich wywołań API, ponieważ usługa koszyka może być zmuszona do zakończenia autoryzacji płatności, gdy klienci są na stronie internetowej
  4. Zanotuj interakcję pomiędzy usługą Zamówień i usługą Płatności. Usługa zamówień emituje zdarzenie domeny (więcej na ten temat w dalszej części tego bloga). Usługa Płatności nasłuchuje tego zdarzenia i kończy rozliczanie zamówienia
  5. Usługa Contact Center może mieć wiele agregatów, ale dla tego przypadku użycia interesuje nas tylko agregat Zamówienia. Usługa ta emituje zdarzenie, gdy zmienia się metoda płatności, a usługa Płatności reaguje na nie, odwracając wcześniej użytą kartę kredytową i przetwarzając nową kartę kredytową.

Fig 5. Zdefiniowana na nowo mapa kontekstowa

Zwykle aplikacja monolityczna lub starsza ma wiele agregatów, często o nakładających się granicach. Stworzenie mapy kontekstowej tych agregatów i ich zależności pomaga nam zrozumieć kontury wszelkich nowych mikroserwisów, które będziemy wydobywać z tych monolitów. Pamiętaj, że sukces lub porażka architektury mikroserwisów zależy od niskiego sprzężenia między agregatami i wysokiej spójności wewnątrz tych agregatów.

Ważne jest również, aby zauważyć, że ograniczone konteksty są same w sobie odpowiednimi spójnymi jednostkami. Nawet jeśli kontekst ma wiele agregatów, cały kontekst, wraz z jego agregatami, może być złożony w pojedynczą mikroserwis. Uważamy, że ta heurystyka jest szczególnie przydatna dla domen, które są nieco niejasne – pomyśl o nowej linii biznesowej, w którą wchodzi organizacja. Możesz nie mieć wystarczającego wglądu we właściwe granice separacji, a każda przedwczesna dekompozycja agregatów może prowadzić do kosztownego refaktoryzacji. Wyobraźmy sobie, że musimy połączyć dwie bazy danych w jedną, wraz z migracją danych, ponieważ okazało się, że dwa agregaty należą do siebie. Ale upewnij się, że te agregaty są wystarczająco odizolowane poprzez interfejsy tak, że nie znają zawiłych szczegółów siebie nawzajem.

Event Storming – Inna technika do identyfikacji granic usług

Event Storming jest kolejną istotną techniką do identyfikacji agregatów (a więc i mikroserwisów) w systemie. Jest to przydatne narzędzie zarówno do rozbijania monolitów jak i przy projektowaniu złożonych ekosystemów mikroserwisów. Użyliśmy tej techniki do rozbicia jednej z naszych złożonych aplikacji, a nasze doświadczenia z Event Stormingiem zamierzamy opisać w osobnym blogu. Na potrzeby tego bloga, chcemy przedstawić szybki, wysokopoziomowy przegląd. Proszę obejrzeć film Alberto Brandelloni na ten temat, jeśli są Państwo zainteresowani dalszym zgłębianiem tematu.

W skrócie, Event Storming jest ćwiczeniem burzy mózgów pomiędzy zespołami, które pracują nad aplikacją – w naszym przypadku, monolitem – w celu zidentyfikowania różnych zdarzeń i procesów domenowych, które mają miejsce w systemie. Zespoły identyfikują również agregaty lub modele, na które te zdarzenia wpływają oraz wszelkie ich późniejsze skutki. Podczas wykonywania tego ćwiczenia, zespoły identyfikują różne nakładające się koncepcje, niejednoznaczny język domeny i sprzeczne procesy biznesowe. Grupują powiązane modele, definiują na nowo agregaty i identyfikują zduplikowane procesy. W miarę postępów w tym ćwiczeniu, jasne stają się ograniczone konteksty, do których należą te agregaty. Warsztaty Event Storming są przydatne, jeśli wszystkie zespoły znajdują się w jednym pomieszczeniu – fizycznym lub wirtualnym – i zaczynają mapować zdarzenia, polecenia i procesy na tablicy w stylu scrumowym. Na koniec tego ćwiczenia, poniżej znajdują się typowe wyniki:

  1. Zredefiniowana lista Agregatów. Te potencjalnie stają się nowymi mikroserwisami
  2. Zdarzenia domenowe, które muszą przepływać pomiędzy tymi mikroserwisami
  3. Komendy, które są bezpośrednimi wywołaniami od innych aplikacji lub użytkowników

Poniżej pokazaliśmy przykładową tablicę na koniec warsztatu Event Storming. Jest to świetne ćwiczenie współpracy dla zespołów, aby uzgodnić właściwe agregaty i ograniczone konteksty. Oprócz tego, że jest to świetne ćwiczenie integracyjne, zespoły wychodzą z tej sesji ze wspólnym zrozumieniem domeny, wszechobecnym językiem i precyzyjnymi granicami usług.

Fig 6. Tablica Event Storming

Komunikacja pomiędzy mikroserwisami

Szybko podsumowując, monolit hostuje wiele agregatów w ramach pojedynczej granicy procesu. Stąd zarządzanie spójnością agregatów w ramach tej granicy jest możliwe. Na przykład, jeśli klient złoży zamówienie, możemy zmniejszyć stan magazynowy przedmiotów, wysłać wiadomość e-mail do klienta – wszystko w ramach jednej transakcji. Wszystkie operacje zakończyłyby się sukcesem, lub wszystkie zakończyłyby się porażką. Ale, gdy rozbijemy monolit i rozproszymy agregaty na różne konteksty, będziemy mieli dziesiątki, a nawet setki mikroserwisów. Procesy, które do tej pory istniały w ramach pojedynczej granicy monolitu, są teraz rozproszone w wielu systemach rozproszonych. Osiągnięcie transakcyjnej integralności i spójności we wszystkich tych systemach rozproszonych jest bardzo trudne i wiąże się z kosztem – dostępnością systemów.

Mikroserwisy są również systemami rozproszonymi. Stąd, twierdzenie CAP odnosi się również do nich – „system rozproszony może zapewnić tylko dwie z trzech pożądanych cech: spójność, dostępność i tolerancję partycji (’C,’ 'A’ i 'P’ w CAP)”. W systemach świata rzeczywistego tolerancja partycji nie podlega negocjacjom – sieć jest zawodna, maszyny wirtualne mogą ulec awarii, opóźnienia między regionami mogą się pogorszyć, i tak dalej.

Więc to pozostawia nam wybór między Dostępnością a Spójnością. Teraz wiemy, że w każdej nowoczesnej aplikacji poświęcenie dostępności również nie jest dobrym pomysłem.

Fig 7. CAP Theorem

Design applications around eventual consistency

Jeśli spróbujesz zbudować transakcje w kilku systemach rozproszonych, znów wylądujesz w krainie monolitów. Tylko tym razem będzie to najgorszy rodzaj, rozproszony monolit. Jeśli któryś z systemów stanie się niedostępny, cały proces stanie się niedostępny, co często prowadzi do frustrujących doświadczeń klientów, nieudanych obietnic i tak dalej. Poza tym, zmiany w jednej usłudze mogą zazwyczaj pociągać za sobą zmiany w innej usłudze, co prowadzi do skomplikowanych i kosztownych wdrożeń. Dlatego lepiej jest projektować aplikacje dostosowując nasze przypadki użycia do tolerowania odrobiny niespójności na rzecz dostępności. Dla powyższego przykładu, możemy uczynić wszystkie procesy asynchronicznymi i w ten sposób ostatecznie spójnymi. Możemy wysyłać e-maile asynchronicznie, niezależnie od innych procesów; Jeśli obiecany przedmiot nie jest dostępny w magazynie później, może on zostać zamówiony z powrotem lub możemy przestać przyjmować zamówienia na ten przedmiot powyżej pewnego progu.
Od czasu do czasu możesz napotkać scenariusz, który może wymagać silnych transakcji w stylu ACID pomiędzy dwoma agregatami w różnych granicach procesów. Jest to doskonały znak, aby ponownie przeanalizować te agregaty i być może połączyć je w jeden. Event Storming i Mapy Kontekstowe pomogą zidentyfikować te zależności na wczesnym etapie, zanim zaczniemy rozbijać te agregaty w różnych granicach procesowych. Łączenie dwóch mikroserwisów w jeden jest kosztowne i jest to coś, czego powinniśmy starać się unikać.

Fawor event-driven architecture

Mikroserwisy mogą emitować istotne zmiany, które zachodzą w ich agregatach. Są one nazywane zdarzeniami domeny, a wszystkie usługi, które są zainteresowane tymi zmianami mogą nasłuchiwać tych zdarzeń i podejmować odpowiednie działania w swoich domenach. W ten sposób unika się sprzężenia behawioralnego – jedna domena nie narzuca tego, co powinny robić inne domeny, oraz sprzężenia czasowego – pomyślne zakończenie procesu nie zależy od tego, czy wszystkie systemy są dostępne w tym samym czasie. To oczywiście oznacza, że systemy będą ostatecznie spójne.

Fig 8. Architektura sterowana zdarzeniami

W powyższym przykładzie usługa Orders publikuje zdarzenie – Order Cancelled. Pozostałe usługi, które zapisały się do tego zdarzenia, przetwarzają swoje odpowiednie funkcje domenowe: Usługa Payment zwraca pieniądze, Usługa Inventory dostosowuje stan magazynowy przedmiotów itd. Kilka rzeczy, które należy zauważyć, aby zapewnić niezawodność i odporność tej integracji:

  1. Producenci powinni upewnić się, że produkują zdarzenie co najmniej raz. Jeśli nie uda się tego zrobić, powinni zapewnić, że istnieje mechanizm awaryjny obecny do ponownego wyzwalania zdarzeń
  2. Konsumenci powinni zapewnić, że konsumują zdarzenia w sposób idempotentny. Jeśli to samo zdarzenie wystąpi ponownie, nie powinno być żadnego efektu ubocznego na końcu konsumenta. Zdarzenia mogą również przychodzić poza kolejnością. Konsumenci mogą używać pól Timestamp lub numerów wersji, aby zagwarantować unikalność zdarzeń.

Użycie integracji opartej na zdarzeniach może nie zawsze być możliwe ze względu na naturę niektórych przypadków użycia. Proszę spojrzeć na integrację pomiędzy usługą Cart a usługą Payment. Jest to integracja synchroniczna, a co za tym idzie ma kilka rzeczy, na które powinniśmy uważać. Jest to przykład sprzężenia behawioralnego – usługa Cart być może wywołuje REST API z usługi Payment i instruuje ją, aby autoryzowała płatność za zamówienie, oraz sprzężenia czasowego – usługa Payment musi być dostępna, aby usługa Cart mogła zaakceptować zamówienie. Ten rodzaj sprzężenia zmniejsza autonomię tych kontekstów i może powodować niepożądaną zależność. Istnieje kilka sposobów, aby uniknąć tego sprzężenia, ale ze wszystkimi tymi opcjami, stracimy zdolność do zapewnienia natychmiastowej informacji zwrotnej dla klientów.

  1. Konwertuj REST API do integracji opartej na zdarzeniach. Ale ta opcja może nie być dostępna, jeśli usługa płatności eksponuje tylko REST API
  2. Usługa Cart przyjmuje zamówienie natychmiast, a istnieje zadanie wsadowe, które zbiera zamówienia i wywołuje API usługi płatności
  3. Usługa Cart produkuje lokalne zdarzenie, które następnie wywołuje API usługi płatności

Połączenie powyższych z ponawianiem prób w przypadku awarii i niedostępności zależności upstream – usługi płatności – może skutkować znacznie bardziej odpornym projektem. Na przykład, synchroniczna integracja pomiędzy usługami Cart i Payment może być wspierana przez zdarzenia lub retriery oparte na wsadach w przypadku awarii. Takie podejście ma dodatkowy wpływ na doświadczenie klienta – klienci mogli wprowadzić nieprawidłowe dane dotyczące płatności, a my nie będziemy mieli ich online, gdy będziemy przetwarzać płatności offline. Mogą też pojawić się dodatkowe koszty dla firmy związane z odzyskiwaniem nieudanych płatności. Jednak według wszelkiego prawdopodobieństwa, korzyści wynikające z odporności usługi koszyka na niedostępność lub błędy usługi płatności przeważają nad niedociągnięciami. Na przykład, możemy powiadomić klientów, jeśli nie jesteśmy w stanie odebrać płatności offline. W skrócie, istnieją kompromisy pomiędzy doświadczeniem użytkownika, odpornością i kosztami operacyjnymi, i mądrze jest projektować systemy, pamiętając o tych kompromisach.

Unikaj orkiestracji pomiędzy usługami dla specyficznych potrzeb konsumentów

Jednym z anty-wzorów w każdej architekturze zorientowanej na usługi jest to, że usługi zaspokajają specyficzne wzorce dostępu konsumentów. Zwykle dzieje się tak, gdy zespoły konsumenckie ściśle współpracują z zespołami usługowymi. Jeśli zespół pracował nad monolityczną aplikacją, często tworzyliby pojedynczy interfejs API, który przekracza różne granice agregatów, stąd ścisłe sprzężenie tych agregatów. Rozważmy przykład. Powiedzmy, że strona szczegółów zamówienia w aplikacjach internetowych i mobilnych musi pokazywać szczegóły zarówno zamówienia, jak i szczegóły refundacji przetworzonych dla tego zamówienia na jednej stronie. W monolitycznej aplikacji, API GET Zamówienia – zakładając, że jest to API REST – zapyta Zamówienia i Refundacje razem, konsoliduje oba agregaty i wysyła złożoną odpowiedź do rozmówców. Jest to możliwe bez dużego narzutu, ponieważ agregaty należą do tej samej granicy procesu. W ten sposób konsumenci mogą uzyskać wszystkie potrzebne dane w pojedynczym wywołaniu.

Jeśli Zamówienia i Zwroty są częścią różnych kontekstów, dane nie są już obecne w ramach jednej mikroserwisu lub granicy agregatu. Jedną z opcji zachowania tej samej funkcjonalności dla konsumentów jest uczynienie usługi Zamówienia odpowiedzialną za wywołanie usługi Refundacji i utworzenie złożonej odpowiedzi. Takie podejście powoduje kilka problemów:

1. Usługa Zamówienie integruje się teraz z inną usługą wyłącznie w celu wspierania konsumentów, którzy potrzebują danych Refundacji wraz z danymi Zamówienia. Usługa Zamówień jest teraz mniej autonomiczna, ponieważ każda zmiana w agregacie Refundacji spowoduje zmianę w agregacie Zamówień.

2. Usługa Zamówień ma inną integrację, a tym samym inny punkt awarii, który należy wziąć pod uwagę – jeśli usługa Refundacji jest wyłączona, czy Usługa Zamówień może nadal wysyłać częściowe dane i czy konsumenci mogą zawieść z gracją?

3. Jeśli konsumenci potrzebują zmiany, aby pobrać więcej danych z agregatu Refundacji, dwa zespoły są teraz zaangażowane w dokonanie tej zmiany

4. Ten wzorzec, jeśli jest stosowany na całej platformie, może prowadzić do zawiłej sieci zależności między różnymi usługami domeny, wszystko dlatego, że te usługi zaspokajają specyficzne wzorce dostępu dzwoniących.

Backend for Frontends (BFFs)

Podejściem do zmniejszenia tego ryzyka jest pozwolenie zespołom konsumenckim na zarządzanie orkiestracją między różnymi usługami domeny. W końcu dzwoniący znają lepiej wzorce dostępu i mogą mieć pełną kontrolę nad wszelkimi zmianami w tych wzorcach. Takie podejście odłącza usługi domenowe od warstwy prezentacji, pozwalając im skupić się na podstawowych procesach biznesowych. Ale jeśli aplikacje internetowe i mobilne zaczną wywoływać różne usługi bezpośrednio zamiast jednego złożonego API z monolitu, może to spowodować narzut wydajności tych aplikacji – wielokrotne wywołania w sieciach o niższej przepustowości, przetwarzanie i łączenie danych z różnych API, i tak dalej.

Zamiast tego można użyć innego wzorca o nazwie Backend for Front-ends. W tym wzorcu projektowym, usługa backendowa stworzona i zarządzana przez konsumentów – w tym przypadku, zespoły webowe i mobilne – zajmuje się integracją wielu usług domenowych wyłącznie w celu renderowania doświadczeń front-endowych dla klientów. Zespoły webowe i mobilne mogą teraz projektować kontrakty danych w oparciu o przypadki użycia, które obsługują. Mogą nawet używać GraphQL zamiast REST API, aby elastycznie zadawać pytania i otrzymywać z powrotem dokładnie to, czego potrzebują. Ważne jest, aby zauważyć, że ta usługa jest własnością i jest utrzymywana przez zespoły konsumenckie, a nie przez zespoły, które są właścicielami usług domenowych. Zespoły front-endowe mogą teraz optymalizować w oparciu o swoje potrzeby – aplikacja mobilna może zażądać mniejszego payloadu, zmniejszyć liczbę wywołań z aplikacji mobilnej, i tak dalej. Przyjrzyj się zmienionemu widokowi orkiestracji poniżej. Usługa BFF wywołuje teraz zarówno usługi domenowe Orders, jak i Refunds dla swojego przypadku użycia.

Fig 9. Backend for Frontends

Przydatne jest również wczesne zbudowanie usługi BFF, przed wybiciem mnóstwa usług z monolitu. W przeciwnym razie albo usługi domenowe będą musiały wspierać orkiestrację międzydomenową, albo aplikacje webowe i mobilne będą musiały wywoływać wiele usług bezpośrednio z frontendów. Obie te opcje doprowadzą do narzutu wydajności, wyrzucania pracy i braku autonomii pomiędzy zespołami.

Wnioski

W tym blogu poruszyliśmy różne koncepcje, strategie i heurystyki projektowe, które należy rozważyć, gdy zapuszczamy się w świat mikroserwisów, a dokładniej, gdy próbujemy rozbić monolit na wiele mikroserwisów opartych na domenach. Wiele z nich to obszerne tematy same w sobie i nie sądzę, że oddaliśmy wystarczająco dużo sprawiedliwości, aby wyjaśnić je w pełni szczegółowo, ale chcieliśmy przedstawić niektóre z krytycznych tematów i nasze doświadczenie w ich stosowaniu. Sekcja Further Reading (link) ma kilka referencji i kilka przydatnych treści dla każdego, kto chce podążać tą ścieżką.

Uaktualnienie: Kolejne dwa blogi z tej serii są już dostępne. Te dwa blogi omawiają implementację mikroserwisu Cart, z przykładami kodu, wykorzystując zasady Domain-Driven Design oraz wzorce projektowe Ports and Adapters. Głównym celem tych blogów jest pokazanie, w jaki sposób te dwie zasady/wzorce pomagają nam budować modułowe aplikacje, które są zwinne, testowalne i refaktoryzowalne – krótko mówiąc, są w stanie reagować na szybko zmieniające się środowisko, w którym wszyscy działamy.

Implementacja mikroserwisu Cart przy użyciu Domain Driven Design oraz Ports and Adapters Pattern – część 1

Implementacja mikroserwisu Cart przy użyciu Domain Driven Design oraz Ports and Adapters Pattern – część 2

Dalsza lektura

1. Domain Driven Design” Erica Evansa

2. Implementing Domain Driven Design” Vaughna Vernona

3. Artykuł Martina Fowlera o mikroserwisach

4. Building Microservices” Sama Newmana

5. Event storming

7. Backend for Frontends

8. Fallacies of distributed computing

.

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany.