Przejście od monolitu do mikrousług: historia i praktyka

W tym artykule opowiem o tym, jak projekt, nad którym pracuję, przekształcił się z dużego monolitu w zestaw mikrousług.

Projekt rozpoczął swoją historię dość dawno, bo na początku 2000 roku. Pierwsze wersje pisano w Visual Basicu 6. Z biegiem czasu stało się jasne, że rozwój w tym języku będzie trudny w przyszłości do wsparcia, gdyż IDE a sam język jest słabo rozwinięty. Pod koniec 2000 roku zdecydowano się przejść na bardziej obiecujący C#. Nowa wersja była pisana równolegle z rewizją starej, stopniowo pisano coraz więcej kodu w .NET. Backend w języku C# początkowo skupiał się na architekturze usług, jednak w trakcie rozwoju wykorzystano wspólne biblioteki z logiką, a usługi zostały uruchomione w jednym procesie. W rezultacie powstała aplikacja, którą nazwaliśmy „monolitem usług”.

Jedną z niewielu zalet takiego połączenia była możliwość wzajemnego wywoływania się usług poprzez zewnętrzne API. Istniały jasne przesłanki przejścia na bardziej poprawną usługę, a w przyszłości architekturę mikroserwisową.

Prace nad rozkładem rozpoczęliśmy około 2015 roku. Nie osiągnęliśmy jeszcze stanu idealnego – są jeszcze fragmenty dużego projektu, które trudno nazwać monolitami, ale na mikroserwisy też też nie wyglądają. Niemniej postęp jest znaczący.
Opowiem o tym w artykule.

Przejście od monolitu do mikrousług: historia i praktyka

Zawartość

Architektura i problemy istniejącego rozwiązania


Początkowo architektura wyglądała tak: UI to osobna aplikacja, część monolityczna napisana jest w Visual Basicu 6, aplikacja .NET to zbiór powiązanych usług pracujących z dość dużą bazą danych.

Wady poprzedniego rozwiązania

Pojedynczy punkt awarii
Wystąpił pojedynczy punkt awarii: aplikacja .NET działała w jednym procesie. Jeśli jakikolwiek moduł uległ awarii, cała aplikacja uległa awarii i konieczne było ponowne uruchomienie. Ponieważ automatyzujemy dużą liczbę procesów dla różnych użytkowników, z powodu awarii jednego z nich, każdy nie mógł przez jakiś czas pracować. A w przypadku błędu oprogramowania nawet kopia zapasowa nie pomogła.

Kolejka ulepszeń
Ta wada ma charakter raczej organizacyjny. Nasza aplikacja ma wielu klientów i wszyscy chcą ją jak najszybciej ulepszyć. Wcześniej nie było możliwości zrobienia tego równolegle i wszyscy klienci stali w kolejce. Proces ten był negatywny dla przedsiębiorstw, ponieważ musiały udowodnić, że ich zadanie jest wartościowe. Zespół programistów spędził czas na organizowaniu tej kolejki. Zajęło to dużo czasu i wysiłku, a ostatecznie produkt nie mógł się zmienić tak szybko, jak by sobie tego życzyli.

Nieoptymalne wykorzystanie zasobów
Hostując usługi w jednym procesie, zawsze całkowicie kopiowaliśmy konfigurację z serwera na serwer. Chcieliśmy umieścić osobno najbardziej obciążone usługi, aby nie marnować zasobów i zyskać bardziej elastyczną kontrolę nad naszym schematem wdrażania.

Trudność we wdrażaniu nowoczesnych technologii
Problem znany wszystkim programistom: istnieje chęć wprowadzenia do projektu nowoczesnych technologii, ale nie ma takiej możliwości. Przy dużym rozwiązaniu monolitycznym jakakolwiek aktualizacja dotychczasowej biblioteki, nie mówiąc już o przejściu na nową, staje się zadaniem dość nietrywialnym. Dużo czasu zajmuje udowodnienie liderowi zespołu, że przyniesie to więcej bonusów niż zmarnowanych nerwów.

Trudności z wydaniem zmian
To był najpoważniejszy problem – wypuszczaliśmy wydawnictwa co dwa miesiące.
Każde wydanie, pomimo testów i wysiłków programistów, kończyło się dla banku prawdziwą katastrofą. Biznes zrozumiał, że na początku tygodnia część jego funkcjonalności nie będzie działać. Twórcy zrozumieli, że czeka ich tydzień poważnych incydentów.
Wszyscy chcieli zmienić tę sytuację.

Oczekiwania wobec mikroserwisów


Wydanie gotowych komponentów. Dostawa gotowych komponentów poprzez rozkład rozwiązania i oddzielenie różnych procesów.

Małe zespoły produktowe. Jest to o tyle istotne, że dużym zespołem pracującym nad starym monolitem trudno było kierować. Taki zespół był zmuszony pracować według rygorystycznego procesu, ale zależało mu na większej kreatywności i niezależności. Tylko małe zespoły mogły sobie na to pozwolić.

Izolacja usług w oddzielnych procesach. W idealnym przypadku chciałem to wyizolować w kontenerach, ale duża liczba usług napisanych w .NET Framework działa tylko na Windowsie. Usługi oparte na .NET Core już się pojawiają, ale jest ich jeszcze niewiele.

Elastyczność wdrażania. Chcielibyśmy łączyć usługi tak, jak tego potrzebujemy, a nie tak, jak wymusza to kodeks.

Wykorzystanie nowych technologii. Jest to interesujące dla każdego programisty.

Problemy z przejściem


Oczywiście, gdyby łatwo było rozbić monolit na mikroserwisy, nie byłoby potrzeby rozmawiać o tym na konferencjach i pisać artykułów. Jest wiele pułapek w tym procesie, opiszę te najważniejsze, które nam przeszkodziły.

Pierwszy problem charakterystyczna dla większości monolitów: spójność logiki biznesowej. Kiedy piszemy monolit, chcemy ponownie wykorzystać nasze klasy, aby nie pisać niepotrzebnego kodu. A kiedy przechodzimy na mikrousługi, staje się to problemem: cały kod jest dość ściśle powiązany i trudno jest oddzielić usługi.

W momencie rozpoczęcia pracy repozytorium liczyło ponad 500 projektów i ponad 700 tysięcy linii kodu. To dość poważna decyzja i drugi problem. Nie dało się tego po prostu wziąć i podzielić na mikroserwisy.

Trzeci problem — brak niezbędnej infrastruktury. W rzeczywistości ręcznie kopiowaliśmy kod źródłowy na serwery.

Jak przejść z monolitu do mikroserwisów


Udostępnianie mikrousług

Po pierwsze, od razu sami ustaliliśmy, że separacja mikroserwisów jest procesem iteracyjnym. Zawsze byliśmy zobowiązani do równoległego rozwijania problemów biznesowych. Sposób, w jaki technicznie to zaimplementujemy, jest już naszym problemem. Dlatego przygotowaliśmy się na proces iteracyjny. Inaczej nie zadziała, jeśli masz dużą aplikację i nie jest ona początkowo gotowa do przepisania.

Jakich metod używamy do izolowania mikroserwisów?

Pierwsza metoda — przenieść istniejące moduły jako usługi. Pod tym względem mieliśmy szczęście: istniały już zarejestrowane usługi, które działały przy użyciu protokołu WCF. Podzielono je na osobne zgromadzenia. Przenieśliśmy je osobno, dodając mały program uruchamiający do każdej kompilacji. Został napisany przy użyciu wspaniałej biblioteki Topshelf, która pozwala na uruchomienie aplikacji zarówno jako usługa, jak i jako konsola. Jest to wygodne w przypadku debugowania, ponieważ rozwiązanie nie wymaga żadnych dodatkowych projektów.

Usługi zostały połączone zgodnie z logiką biznesową, gdyż korzystały ze wspólnych zestawów i współpracowały ze wspólną bazą danych. Trudno je nazwać mikrousługami w czystej postaci. Moglibyśmy jednak świadczyć te usługi osobno, w różnych procesach. Samo to umożliwiło zmniejszenie ich wzajemnego wpływu, redukując problem równoległego rozwoju i pojedynczego punktu awarii.

Montaż z hostem to tylko jedna linia kodu w klasie Program. Pracę z Topshelfem ukryliśmy w klasie pomocniczej.

namespace RBA.Services.Accounts.Host
{
   internal class Program
   {
      private static void Main(string[] args)
      {
        HostRunner<Accounts>.Run("RBA.Services.Accounts.Host");

       }
    }
}

Drugi sposób alokacji mikrousług to: twórz je, aby rozwiązywać nowe problemy. Jeśli jednocześnie monolit nie rośnie, to już jest znakomicie, co oznacza, że ​​zmierzamy we właściwym kierunku. Aby rozwiązać nowe problemy, próbowaliśmy stworzyć osobne usługi. Jeśli była taka możliwość, to stworzyliśmy bardziej „kanoniczne” usługi, które w pełni zarządzają własnym modelem danych, osobną bazą danych.

Podobnie jak wielu zaczynaliśmy od usług uwierzytelniania i autoryzacji. Są do tego idealne. Są niezależne, z reguły mają odrębny model danych. Oni sami nie wchodzą w interakcję z monolitem, jedynie zwraca się do nich o rozwiązanie niektórych problemów. Korzystając z tych usług, możesz rozpocząć przejście na nową architekturę, debugować na nich infrastrukturę, wypróbować pewne podejścia związane z bibliotekami sieciowymi itp. W naszej organizacji nie ma zespołów, które nie mogłyby stworzyć usługi uwierzytelniania.

Trzeci sposób alokacji mikrousługTen, którego używamy, jest dla nas trochę specyficzny. To usunięcie logiki biznesowej z warstwy UI. Naszą główną aplikacją UI jest aplikacja desktopowa, która podobnie jak backend jest napisana w języku C#. Programiści okresowo popełniali błędy i przenosili do interfejsu użytkownika fragmenty logiki, które powinny znajdować się w backendzie i zostać ponownie wykorzystane.

Jeśli spojrzysz na prawdziwy przykład z kodu części UI, zobaczysz, że większość tego rozwiązania zawiera prawdziwą logikę biznesową, która jest przydatna w innych procesach, a nie tylko przy budowaniu formularza UI.

Przejście od monolitu do mikrousług: historia i praktyka

Prawdziwa logika interfejsu użytkownika znajduje się tylko w kilku ostatnich wierszach. Przenieśliśmy go na serwer, aby można było go ponownie wykorzystać, zmniejszając tym samym UI i uzyskując poprawną architekturę.

Czwarty i najważniejszy sposób izolowania mikrousług, która pozwala na redukcję monolitu, jest usunięcie istniejących usług wraz z przetwarzaniem. Kiedy usuwamy istniejące moduły w niezmienionej postaci, wynik nie zawsze odpowiada programistom, a proces biznesowy mógł stać się przestarzały od czasu utworzenia funkcjonalności. Dzięki refaktoryzacji możemy wesprzeć nowy proces biznesowy, ponieważ wymagania biznesowe stale się zmieniają. Możemy ulepszyć kod źródłowy, usunąć znane defekty i stworzyć lepszy model danych. Narasta wiele korzyści.

Oddzielenie usług od przetwarzania jest nierozerwalnie związane z koncepcją ograniczonego kontekstu. Jest to koncepcja z Domain Driven Design. Oznacza sekcję modelu domeny, w której wszystkie terminy jednego języka są jednoznacznie zdefiniowane. Spójrzmy na przykład na kontekst ubezpieczeń i rachunków. Mamy aplikację monolityczną i musimy pracować z kontem w ubezpieczeniu. Oczekujemy, że programista znajdzie istniejącą klasę Konto w innym zestawie, odniesie się do niej z klasy Ubezpieczenie i otrzymamy działający kod. Zasada DRY zostanie zachowana, zadanie zostanie wykonane szybciej przy użyciu istniejącego kodu.

W rezultacie okazuje się, że konteksty rachunków i ubezpieczeń są ze sobą powiązane. W miarę pojawiania się nowych wymagań połączenie to będzie zakłócać rozwój, zwiększając złożoność i tak już złożonej logiki biznesowej. Aby rozwiązać ten problem, należy znaleźć granice pomiędzy kontekstami w kodzie i usunąć ich naruszenia. Na przykład w kontekście ubezpieczeń całkiem możliwe, że 20-cyfrowy numer rachunku Banku Centralnego i data jego otwarcia będą wystarczające.

Aby oddzielić od siebie te ograniczone konteksty i rozpocząć proces oddzielania mikrousług od rozwiązania monolitycznego, zastosowaliśmy takie podejście, jak tworzenie zewnętrznych API w obrębie aplikacji. Jeśli wiedzieliśmy, że jakiś moduł powinien stać się mikroserwisem, w jakiś sposób zmodyfikowanym w procesie, to od razu poprzez wywołania zewnętrzne wykonaliśmy wywołania do logiki należącej do innego ograniczonego kontekstu. Na przykład za pośrednictwem REST lub WCF.

Stanowczo zdecydowaliśmy, że nie będziemy unikać kodu, który wymagałby transakcji rozproszonych. W naszym przypadku okazało się, że przestrzeganie tej zasady jest dość łatwe. Nie spotkaliśmy się jeszcze z sytuacjami, w których rzeczywiście potrzebne byłyby ściśle rozproszone transakcje - ostateczna spójność pomiędzy modułami jest w zupełności wystarczająca.

Spójrzmy na konkretny przykład. Mamy koncepcję orkiestratora – potoku przetwarzającego jednostkę „aplikacji”. Tworzy kolejno klienta, konto i kartę bankową. Jeżeli klient i konto zostaną utworzone pomyślnie, ale utworzenie karty nie powiedzie się, aplikacja nie przejdzie do statusu „pomyślnie” i pozostanie w stanie „karta nie została utworzona”. W przyszłości działanie w tle wychwyci to i zakończy. System od jakiegoś czasu jest w stanie niespójności, ale generalnie jesteśmy z tego zadowoleni.

Jeśli zaistnieje sytuacja, w której konieczne będzie spójne zapisanie części danych, najprawdopodobniej zdecydujemy się na konsolidację usługi, aby przetworzyć ją w jednym procesie.

Spójrzmy na przykład alokacji mikrousługi. Jak stosunkowo bezpiecznie wprowadzić go do produkcji? W tym przykładzie mamy wydzieloną część systemu - moduł obsługi płac, jedną z sekcji kodu, z której chcielibyśmy wykonać mikroserwis.

Przejście od monolitu do mikrousług: historia i praktyka

W pierwszej kolejności tworzymy mikroserwis przepisując kod. Poprawiamy pewne aspekty, z których nie byliśmy zadowoleni. Wdrażamy nowe wymagania biznesowe od Klienta. Do połączenia UI z backendem dodajemy API Gateway, które zapewni przekierowanie połączeń.

Przejście od monolitu do mikrousług: historia i praktyka

Następnie uruchamiamy tę konfigurację, ale w stanie pilotażowym. Większość naszych użytkowników nadal pracuje ze starymi procesami biznesowymi. Dla nowych użytkowników opracowujemy nową wersję aplikacji monolitycznej, która nie zawiera już tego procesu. Zasadniczo mamy połączenie monolitu i mikrousługi działającej jako pilotaż.

Przejście od monolitu do mikrousług: historia i praktyka

Po udanym pilotażu rozumiemy, że nowa konfiguracja rzeczywiście jest wykonalna, możemy usunąć z równania stary monolit i pozostawić nową konfigurację w miejscu starego rozwiązania.

Przejście od monolitu do mikrousług: historia i praktyka

W sumie wykorzystujemy prawie wszystkie istniejące metody dzielenia kodu źródłowego monolitu. Wszystkie pozwalają nam zmniejszyć rozmiar części aplikacji i przetłumaczyć je na nowe biblioteki, tworząc lepszy kod źródłowy.

Praca z bazą danych


Bazę danych można podzielić gorzej niż kod źródłowy, ponieważ zawiera nie tylko bieżący schemat, ale także zakumulowane dane historyczne.

Nasza baza danych, jak wiele innych, miała jeszcze jedną istotną wadę – jej ogromny rozmiar. Ta baza danych została zaprojektowana zgodnie ze skomplikowaną logiką biznesową monolitu i relacjami zgromadzonymi pomiędzy tabelami o różnych ograniczonych kontekstach.

W naszym przypadku na domiar złego (duża baza danych, wiele połączeń, czasem niejasne granice między tabelami) pojawił się problem, który pojawia się w wielu dużych projektach: wykorzystanie udostępnionego szablonu bazy danych. Dane zostały pobrane z tabel poprzez ich przeglądanie, replikację i wysłane do innych systemów, w których taka replikacja była potrzebna. W rezultacie nie mogliśmy przenieść tabel do osobnego schematu, ponieważ były aktywnie używane.

W separacji pomaga nam ten sam podział na ograniczone konteksty w kodzie. Zwykle daje nam to całkiem niezłe pojęcie o tym, jak rozkładamy dane na poziomie bazy danych. Rozumiemy, które tabele należą do jednego ograniczonego kontekstu, a które do innego.

Zastosowaliśmy dwie globalne metody partycjonowania bazy danych: partycjonowanie istniejących tabel i partycjonowanie z przetwarzaniem.

Podział istniejących tabel jest dobrą metodą, jeśli struktura danych jest dobra, spełnia wymagania biznesowe i wszyscy są z niej zadowoleni. W takim przypadku możemy rozdzielić istniejące tabele na osobny schemat.

Dział z obróbką jest potrzebny, gdy model biznesowy bardzo się zmienił, a tabele w ogóle nas już nie zadowalają.

Dzielenie istniejących tabel. Musimy ustalić, co będziemy rozdzielać. Bez tej wiedzy nic się nie uda i tutaj z pomocą przyjdzie nam wydzielenie ograniczonych kontekstów w kodzie. Z reguły, jeśli rozumiesz granice kontekstów w kodzie źródłowym, staje się jasne, które tabele powinny znaleźć się na liście dla działu.

Wyobraźmy sobie, że mamy rozwiązanie, w którym dwa moduły monolitu współdziałają z jedną bazą danych. Musimy zadbać o to, aby tylko jeden moduł współdziałał z sekcją wydzielonych tabel, a drugi zaczął z nią współdziałać poprzez API. Na początek wystarczy, że poprzez API odbywa się samo nagrywanie. Jest to warunek konieczny, abyśmy mogli mówić o niezależności mikroserwisów. Odczyt połączeń może pozostać o ile nie ma większego problemu.

Przejście od monolitu do mikrousług: historia i praktyka

Następnym krokiem jest wydzielenie sekcji kodu obsługującej oddzielne tabele, z przetwarzaniem lub bez, na osobną mikrousługę i uruchomienie jej w osobnym procesie, czyli kontenerze. Będzie to osobna usługa z połączeniem do bazy monolitu i tych tabel, które nie są z nią bezpośrednio powiązane. Monolit nadal współpracuje z odłączaną częścią do czytania.

Przejście od monolitu do mikrousług: historia i praktyka

Później usuniemy to połączenie, czyli odczyt danych z aplikacji monolitycznej z wydzielonych tabel również będzie przekazywany do API.

Przejście od monolitu do mikrousług: historia i praktyka

Następnie z ogólnej bazy danych wybierzemy tabele, z którymi współpracuje tylko nowy mikroserwis. Tabele możemy przenieść na osobny schemat lub nawet do osobnej fizycznej bazy danych. Nadal istnieje połączenie odczytowe pomiędzy mikroserwisem a bazą danych monolitu, ale nie ma się czym martwić, w tej konfiguracji może ono działać dość długo.

Przejście od monolitu do mikrousług: historia i praktyka

Ostatnim krokiem jest całkowite usunięcie wszystkich połączeń. W takim przypadku może zaistnieć konieczność migracji danych z głównej bazy danych. Czasami chcemy ponownie wykorzystać w kilku bazach niektóre dane lub katalogi zreplikowane z systemów zewnętrznych. Zdarza się to nam okresowo.

Przejście od monolitu do mikrousług: historia i praktyka

Dział przetwarzania. Metoda ta jest bardzo podobna do pierwszej, tylko w odwrotnej kolejności. Natychmiast przydzielamy nową bazę danych i nowy mikroserwis, który współdziała z monolitem poprzez API. Ale jednocześnie pozostaje zestaw tabel bazy danych, które chcemy w przyszłości usunąć. Nie jest nam już potrzebny, zastąpiliśmy go w nowym modelu.

Przejście od monolitu do mikrousług: historia i praktyka

Aby ten system zadziałał, prawdopodobnie będziemy potrzebować okresu przejściowego.

Istnieją wówczas dwa możliwe podejścia.

pierwszy: duplikujemy wszystkie dane w nowej i starej bazie danych. W takim przypadku mamy do czynienia z redundancją danych i mogą pojawić się problemy z synchronizacją. Ale możemy przyjąć dwóch różnych klientów. Jeden będzie działał z nową wersją, drugi ze starą.

drugi: dzielimy dane według pewnych kryteriów biznesowych. Przykładowo w systemie mieliśmy 5 produktów, które były zapisane w starej bazie danych. Szóste umieszczamy w ramach nowego zadania biznesowego w nowej bazie danych. Będziemy jednak potrzebować bramy API, która zsynchronizuje te dane i pokaże klientowi, gdzie i co może uzyskać.

Obydwa podejścia działają, wybierz w zależności od sytuacji.

Gdy mamy pewność, że wszystko działa, część monolitu współpracującą ze starymi strukturami baz danych można wyłączyć.

Przejście od monolitu do mikrousług: historia i praktyka

Ostatnim krokiem jest usunięcie starych struktur danych.

Przejście od monolitu do mikrousług: historia i praktyka

Podsumowując możemy powiedzieć, że mamy problemy z bazą danych: ciężko się z nią pracuje w porównaniu z kodem źródłowym, trudniej jest ją udostępniać, ale można i należy to robić. Znaleźliśmy kilka sposobów, które pozwalają nam to zrobić całkiem bezpiecznie, ale nadal łatwiej jest popełniać błędy w przypadku danych niż w kodzie źródłowym.

Praca z kodem źródłowym


Tak wyglądał diagram kodu źródłowego, gdy zaczęliśmy analizować projekt monolityczny.

Przejście od monolitu do mikrousług: historia i praktyka

Można go z grubsza podzielić na trzy warstwy. Jest to warstwa uruchomionych modułów, wtyczek, usług i poszczególnych działań. W rzeczywistości były to punkty wejścia w ramach rozwiązania monolitycznego. Wszystkie zostały szczelnie zamknięte warstwą Common. Miała logikę biznesową wspólną dla usług i wiele połączeń. Każda usługa i wtyczka wykorzystywała do 10 lub więcej wspólnych zestawów, w zależności od ich wielkości i sumienia programistów.

Mieliśmy szczęście, że mieliśmy biblioteki infrastruktury, z których można było korzystać osobno.

Czasami dochodziło do sytuacji, gdy niektóre wspólne obiekty w rzeczywistości nie należały do ​​tej warstwy, ale były bibliotekami infrastruktury. Zostało to rozwiązane poprzez zmianę nazwy.

Największym problemem były ograniczone konteksty. Zdarzało się, że w jednym wspólnym zgromadzeniu zmieszano 3-4 konteksty i wykorzystywano je wzajemnie w ramach tych samych funkcji biznesowych. Należało zrozumieć, gdzie można to podzielić i wzdłuż jakich granic oraz co dalej zrobić z odwzorowaniem tego podziału na zespoły kodu źródłowego.

Sformułowaliśmy kilka zasad procesu dzielenia kodu.

Pierwszy: Nie chcieliśmy już dzielić logiki biznesowej pomiędzy usługami, działaniami i wtyczkami. Chcieliśmy uniezależnić logikę biznesową w ramach mikroserwisów. Z drugiej strony mikroserwisy najlepiej postrzegać jako usługi, które istnieją całkowicie niezależnie. Uważam, że takie podejście jest nieco marnotrawne i trudne do osiągnięcia, ponieważ np. usługi w C# i tak będą połączone standardową biblioteką. Nasz system jest napisany w języku C#, nie korzystaliśmy jeszcze z innych technologii. Dlatego uznaliśmy, że stać nas na zastosowanie powszechnych podzespołów technicznych. Najważniejsze, że nie zawierają żadnych fragmentów logiki biznesowej. Jeśli masz wygodne opakowanie na używany ORM, kopiowanie go z usługi do usługi jest bardzo kosztowne.

Nasz zespół jest fanem projektowania opartego na domenach, więc architektura cebulowa była dla nas idealna. Podstawą naszych usług nie jest warstwa dostępu do danych, ale zespół z logiką domenową, który zawiera wyłącznie logikę biznesową i nie ma powiązań z infrastrukturą. Jednocześnie możemy samodzielnie modyfikować zestaw domeny, aby rozwiązać problemy związane z frameworkami.

Na tym etapie napotkaliśmy pierwszy poważny problem. Usługa musiała odnosić się do jednego zestawu domeny, chcieliśmy uniezależnić logikę, a zasada DRY bardzo nam tu przeszkadzała. Twórcy chcieli ponownie wykorzystać klasy z sąsiednich zespołów, aby uniknąć duplikacji, w wyniku czego domeny ponownie zaczęły się ze sobą łączyć. Przeanalizowaliśmy wyniki i zdecydowaliśmy, że być może problem leży również w obszarze urządzenia przechowującego kod źródłowy. Mieliśmy duże repozytorium zawierające cały kod źródłowy. Rozwiązanie dla całego projektu było bardzo trudne do zmontowania na lokalnej maszynie. Dlatego dla części projektu stworzono osobne, małe rozwiązania i nikt nie zabronił dodawania do nich jakiegoś wspólnego lub domenowego zestawu i ponownego ich wykorzystania. Jedynym narzędziem, które nam na to nie pozwoliło, był przegląd kodu. Ale czasami to też się nie udało.

Następnie zaczęliśmy przechodzić na model z oddzielnymi repozytoriami. Logika biznesowa nie przepływa już od usługi do usługi, domeny naprawdę stały się niezależne. Konteksty ograniczone są obsługiwane w bardziej przejrzysty sposób. Jak ponownie wykorzystujemy biblioteki infrastruktury? Rozdzieliliśmy je do osobnego repozytorium, następnie umieściliśmy w pakietach Nuget, które umieściliśmy w Artifactory. Przy każdej zmianie montaż i publikacja następuje automatycznie.

Przejście od monolitu do mikrousług: historia i praktyka

Nasze usługi zaczęły odnosić się do pakietów infrastruktury wewnętrznej w taki sam sposób, jak pakiety zewnętrzne. Pobieramy zewnętrzne biblioteki z Nuget. Do pracy z Artifactory, gdzie umieściliśmy te pakiety, użyliśmy dwóch menedżerów pakietów. W małych repozytoriach korzystaliśmy również z Nuget. W repozytoriach z wieloma usługami zastosowaliśmy Paket, który zapewnia większą spójność wersji pomiędzy modułami.

Przejście od monolitu do mikrousług: historia i praktyka

Tym samym pracując nad kodem źródłowym, nieznacznie zmieniając architekturę i oddzielając repozytoria, uniezależniamy nasze usługi.

Problemy z infrastrukturą


Większość wad przejścia na mikrousługi ma związek z infrastrukturą. Będziesz potrzebować zautomatyzowanego wdrożenia, będziesz potrzebować nowych bibliotek do uruchomienia infrastruktury.

Ręczna instalacja w środowiskach

Początkowo instalowaliśmy rozwiązanie dla środowisk ręcznie. Aby zautomatyzować ten proces, stworzyliśmy potok CI/CD. Wybraliśmy proces ciągłego dostarczania, ponieważ ciągłe wdrażanie nie jest jeszcze dla nas akceptowalne z punktu widzenia procesów biznesowych. Dlatego wysłanie do pracy odbywa się za pomocą przycisku, a do testów - automatycznie.

Przejście od monolitu do mikrousług: historia i praktyka

Używamy Atlassian, Bitbucket do przechowywania kodu źródłowego i Bamboo do budowania. Lubimy pisać skrypty kompilacji w Cake, ponieważ jest taki sam jak C#. Gotowe pakiety trafiają do Artifactory, a Ansible automatycznie trafia na serwery testowe, po czym można je od razu przetestować.

Przejście od monolitu do mikrousług: historia i praktyka

Oddzielne rejestrowanie


Kiedyś jednym z pomysłów monolitu było zapewnienie wspólnego logowania. Musieliśmy także zrozumieć, co zrobić z poszczególnymi dziennikami znajdującymi się na dyskach. Nasze logi zapisywane są do plików tekstowych. Zdecydowaliśmy się zastosować standardowy stos ELK. Nie pisaliśmy do ELK bezpośrednio przez dostawców, ale zdecydowaliśmy, że sfinalizujemy logi tekstowe i zapiszemy w nich identyfikator śledzenia jako identyfikator, dodając nazwę usługi, aby można było te logi później przeanalizować.

Przejście od monolitu do mikrousług: historia i praktyka

Korzystając z Filebeat, otrzymujemy możliwość zbierania naszych logów z serwerów, następnie je przekształcamy, wykorzystujemy Kibanę do budowania zapytań w interfejsie użytkownika i sprawdzamy, jak przebiegało połączenie pomiędzy usługami. Trace ID bardzo w tym pomaga.

Testowanie i debugowanie powiązanych usług


Początkowo nie do końca rozumieliśmy, jak debugować opracowywane usługi. W przypadku monolitu wszystko było proste; uruchomiliśmy go na lokalnej maszynie. Początkowo próbowali zrobić to samo z mikrousługami, ale czasami, aby w pełni uruchomić jedną mikrousługę, trzeba uruchomić kilka innych, a to jest niewygodne. Zdaliśmy sobie sprawę, że musimy przejść do modelu, w którym na lokalnej maszynie zostawiamy tylko tę usługę lub usługi, które chcemy debugować. Pozostałe usługi wykorzystywane są z serwerów pasujących do konfiguracji z prod. Po debugowaniu, podczas testowania, dla każdego zadania do serwera testowego wydawane są tylko zmienione usługi. Tym samym rozwiązanie jest testowane w takiej formie, w jakiej w przyszłości pojawi się w produkcji.

Istnieją serwery, na których działają tylko produkcyjne wersje usług. Serwery te są potrzebne na wypadek incydentów, do sprawdzenia dostawy przed wdrożeniem oraz do szkoleń wewnętrznych.

Dodaliśmy zautomatyzowany proces testowania z wykorzystaniem popularnej biblioteki Specflow. Testy uruchamiają się automatycznie przy użyciu NUnit natychmiast po wdrożeniu z Ansible. Jeśli pokrycie zadań jest w pełni automatyczne, nie ma potrzeby ręcznego testowania. Chociaż czasami nadal wymagane są dodatkowe testy ręczne. W Jira używamy tagów, aby określić, które testy uruchomić w przypadku konkretnego problemu.

Dodatkowo wzrosło zapotrzebowanie na badania obciążeniowe, które wcześniej były przeprowadzane jedynie w nielicznych przypadkach. Do uruchamiania testów używamy JMeter, do ich przechowywania InfluxDB, a do tworzenia wykresów procesów Grafana.

Co osiągnęliśmy?


Po pierwsze pozbyliśmy się pojęcia „wypuszczenia”. Dawno minęły dwumiesięczne monstrualne premiery, kiedy ten kolos został wdrożony w środowisku produkcyjnym, tymczasowo zakłócając procesy biznesowe. Teraz usługi wdrażamy średnio co 1,5 dnia, grupując je, bo idą do użytku po zatwierdzeniu.

W naszym systemie nie ma awarii krytycznych. Jeśli wypuścimy mikroserwis z błędem, wówczas związana z nim funkcjonalność zostanie uszkodzona i nie będzie to miało wpływu na pozostałe funkcjonalności. To znacznie poprawia komfort użytkowania.

Możemy kontrolować wzorzec wdrażania. W razie potrzeby możesz wybrać grupy usług oddzielnie od reszty rozwiązania.

Dodatkowo znacząco zmniejszyliśmy problem dzięki dużej kolejce ulepszeń. Mamy teraz osobne zespoły ds. produktów, które niezależnie pracują nad niektórymi usługami. Proces Scrum już tutaj dobrze pasuje. Konkretny zespół może mieć osobnego Właściciela Produktu, który przydziela mu zadania.

Streszczenie

  • Mikrousługi doskonale nadają się do rozkładania złożonych systemów. W trakcie tego procesu zaczynamy rozumieć, co znajduje się w naszym systemie, jakie istnieją ograniczone konteksty, gdzie leżą ich granice. Pozwala to na prawidłowe rozłożenie ulepszeń pomiędzy modułami i zapobiega pomyłkom w kodzie.
  • Mikrousługi zapewniają korzyści organizacyjne. Często mówi się o nich wyłącznie jako o architekturze, ale każda architektura jest potrzebna do rozwiązywania potrzeb biznesowych, a nie sama. Można zatem powiedzieć, że mikroserwisy świetnie nadają się do rozwiązywania problemów w małych zespołach, biorąc pod uwagę, że Scrum jest obecnie bardzo popularny.
  • Separacja jest procesem iteracyjnym. Nie można wziąć aplikacji i po prostu podzielić jej na mikrousługi. Powstały produkt prawdopodobnie nie będzie funkcjonalny. Dedykując mikrousługi warto napisać na nowo istniejące dziedzictwo, czyli zamienić je w kod, który nam się podoba i lepiej odpowiada potrzebom biznesowym pod względem funkcjonalności i szybkości.

    Małe zastrzeżenie: Koszty przejścia na mikroserwisy są dość znaczne. Samo rozwiązanie problemu infrastruktury zajęło dużo czasu. Jeśli więc masz małą aplikację, która nie wymaga specjalnego skalowania, chyba że masz dużą liczbę klientów konkurujących o uwagę i czas Twojego zespołu, to mikrousługi mogą nie być tym, czego dzisiaj potrzebujesz. To dość drogie. Jeśli rozpoczniesz proces od mikrousług, koszty będą początkowo wyższe niż w przypadku rozpoczęcia tego samego projektu od opracowania monolitu.

    PS Bardziej emocjonalna historia (i jakby dla Was osobiście) – wg powiązanie.
    Oto pełna wersja raportu.

Źródło: www.habr.com

Dodaj komentarz