Sytuacja kobiet w IT w 2024 roku
15.06.20219 min
Fernando Doglio

Fernando DoglioData Engineering Manager

Node.js - najlepsze wzorce projektowe do tworzenia mikroserwisów

Poznaj najlepsze wzorce projektowe z Node.js przydatne do tworzenia mikroserwisów.

Node.js - najlepsze wzorce projektowe do tworzenia mikroserwisów

Mając już jakieś doświadczenie z Node.js, doszedłem do wniosku, że to naprawdę świetne narzędzie do pisania mikroserwisów. Zapewnia on bardzo krótki czas developmentu, wiele gotowych frameworków, a asynchroniczne I/O zapewnia fantastyczną wydajność w przypadku wielu jednoczesnych żądań.

I pomimo że sam Node.js jest świetnym narzędziem, to wszystko i tak będzie zależeć od tego, jak go używasz. Mając to na uwadze, przedstawię Ci kilka przydatnych i interesujących wzorców projektowych dla mikroserwisów. Możesz w końcu zrobić kilka serwisów w projekcie, a sposób wykonania zależy od korzyści, jakie chcesz uzyskać. Przyjrzyjmy się im.

Wzorzec agregatora

Chodzi tutaj o to, aby stworzyć serwisy w oparciu o inne, drobne i indywidualne usługi. Agregator zapewni wspólne publiczne API używane przez klientów. Miałby tylko wymaganą logikę potrzebną do korzystania ze wszystkich innych serwisów, które z kolei zawierają właściwą logikę biznesową.

Spójrz na powyższy schemat - przedstawia teoretyczny mikroserwis ubezpieczenia samochodu. Klient dba tylko o główne API - jest to jedyny dostępny mikroserwis i do niego będą się odnosić wszystkie aplikacje klienckie. Pod maską jednak widać, że istnieje wiele indywidualnych API, które ze sobą współpracują, będąc dopasowanymi przez agregator.

Takie podejście zapewnia kilka korzyści:

  1. Rozwój architektury jest łatwiejszy i bardziej przejrzysty dla klienta końcowego. W końcu API nigdy się nie zmieni, a nawet jeśli tak się stanie, to będzie trzeba dostosować tylko jeden URL. Jednak w tle możesz dodawać, usuwać albo robić to i to, ile tylko chcesz. Tak naprawdę, to jeśli nie zmienisz publicznego API, możesz zmieniać wewnętrzną architekturę jak tylko chcesz i nie będzie to miało wpływu na klientów aplikacji.
  2. Bezpieczeństwo wydaje się prostsze. Głównym celem wszystkich zadań związanych z bezpieczeństwem API powinno być publiczne API. Reszta może znajdować się w VPN, do którego, do której ma dostęp tylko agregator. To znacznie upraszcza komunikację między różnymi API.
  3. Mikroserwisy te można skalować indywidualnie. Mierząc zużycie zasobów, możesz wykryć, które z nich są najbardziej obciążone i utworzyć więcej ich kopii. Następnie umieść je wszystkie za load balancerem i upewnij się, że wszyscy wchodzący w interakcję z tą usługą trafią do load balancera. Tak długo, jak Twoje usługu są bezstanowe (tak jak w przypadku korzystania z REST), mechanika komunikacji się nie zmieni.


Takie małe mikroserwisy można tworzyć wokół usługi Restify, która daje Ci wszystko, czego potrzebujesz do zbudowania mikroserwisu REST. Działa to świetnie, a API jest bardzo podobne do interfejsu Express, który jest znanym i łatwym w użyciu frameworkiem - da się go też wykorzystać do tworzenia takich serwisów.

Łańcuch zobowiązań

Podejście to jest bardzo podobne do poprzedniego. Właściwie to ukrywasz złożoną architekturę opartą na mikroserwisach za pojedynczą usługą, która orkiestruje innymi.

Główna różnica polega na tym, że w tym wypadku logika wymaga połączenia kilku serwisów szeregowo. Oznacza to, że aby nastąpiła interakcja między klientem a głównym API, to żądanie musi przejść z głównego API do Serwisu 1, a następnie z Serwisu 1 do Serwisu 2, z Serwisu 2 do Serwisu 3. Następnie wszystko musi rozpocząć się od Serwisu 3, aby Serwis 4 dał nam pożądaną odpowiedź, która z kolei powróci service by service aż do głównego API.

Długo by wyjaśniać - ale nie tak długo, jak to się wykonuje. Taki typ interakcji nie jest zalecany, chyba że zmienisz kanał komunikacji na bardziej zwinny. Możesz użyć REST zamiast HTTP do interakcji klient-główne API, a następnie przełączyć się na gniazda w celu komunikacji między serwisami. W ten sposób dodatkowe opóźnienie z HTTP nie sumowałoby się przy każdym żądaniu. Gniazda będą już miały otwarte i aktywne kanały komunikacyjne między wewnętrznymi mikroserwisami.

Takie podejście daje nam podobne korzyści, jak w poprzednim przypadku, jednak ma jedną ogromną wadę: im dłuższy łańcuch, tym większe opóźnienie w odpowiedzi. Dlatego upewnienie się, że używasz właściwego protokołu komunikacyjnego, ma kluczowe znaczenie dla powodzenia tego wzorca.

Spójrz na Socket.io, jeśli zastanawiasz się, jak obsługiwać komunikację między mikroserwisami przez gniazda. Jest to biblioteka do obsługi gniazd w Node.js.

Asynchroniczne wysyłanie wiadomości

Ciekawym sposobem na ulepszenie mechaniki wzorca łańcucha zobowiązań jest uczynienie go asynchronicznym.

Osobiście uwielbiam ten wzorzec. Użycie architektury opartej na mikroserwisach asynchronicznie może zapewnić dużą elastyczność i poprawę wydajności. Nie jest to łatwe, ponieważ komunikacja może stać się nieco skomplikowana, a problemy związane z debugowaniem mogą być jeszcze większe. Od teraz nie ma wyraźnego przepływu danych z Serwisu 1 do Serwisu 2.

Świetnym rozwiązaniem jest tworzenie identyfikatora zdarzenia, gdy klient wysyła swoje początkowe żądanie, a następnie propagowanie go do każdego zdarzenia, które z niego wynika. W ten sposób możesz filtrować logi za pomocą takiego identyfikatora i lepiej zrozumieć każdą wiadomość wygenerowaną z pierwotnego żądania.

Zauważ też, że powyższy diagram pokazuje klienta, który wchodzi w bezpośrednią interakcję z kolejką wiadomości. To świetne rozwiązanie, jeśli masz prosty interfejs i masz pełną kontrolę nad klientem. Niemniej jednak jeśli mamy do czynienia z publicznym klientem, w którego kodzie każdy może wprowadzać zmiany, to warto dać im coś na kształt SDK, żeby mogli się z Tobą komunikować. 

Abstrakcja i uproszczenie komunikacji z bezpieczeństwem ułatwi korzystanie z Twojej usługi w kodzie.

Mamy jednak alternatywę - byłoby nią dostarczenie serwisu przypominającego gateway, który wchodzi w interakcję z klientem. W taki właśnie sposób klient rozmawia tylko z gatewayem, a reszta jest dla niego transparentna. Nie neguje to jednak wymagań klienta co do bycia świadomym asynchronicznej natury komunikacji. Nadal trzeba wymyślić sposób na subskrypcję dla zdarzeń i do wysłania nowych do kolejki. Nawet jeśli będzie to przez gateway, to nadal musi się to wydarzyć. 

Kilka zalet takiego podejścia:

  • Dużo szybszy czas odpowiedzi. Komunikacja otwiera się początkową subskrypcją zdarzeń, a potem każde następne żądanie jest wysyłane na zasadzie fire and forget. Oznacza to, że nie trzeba czekać na całą logikę biznesową, aby ta zakończyła połączenie i dała użytkownikowi odpowiedź. Dane pojawią się, gdy będą gotowe i w taki właśnie sposób UI musi sobie z nim poradzić. 
  • Trudniej jest zgubić informacje ze względu na problem ze stabilnością. W dwóch powyższych podejściach awaria jednego serwisu sprawi, że aktywne żądania nie powiodą się. Tutaj, o ile tylko będzie działać kolejka wiadomości, dane zostaną przechowane. Jednak kiedy wadliwe mikroserwisy zaczną działać, to oczekujące żądania będą mogły zostać zrealizowane.
  • O wiele łatwiej jest wtedy skalować. Posiadanie kilku kopii tego samego mikroserwisu nie wymaga już load balancera. Teraz każdy serwis staje się oddzielnym konsumentem/producentem, a zdarzenia traktuje się niezależnie od siebie. 


Dobrą opcją dla kolejek wiadomości, która świetnie się sprawdza z Node.js, są Redis i Kafka. Redis mam takie opcje jak Pub/Sub, powiadomienia Key-space, a nawet potoki - wszystko to sprawia, że mamy do czynienia ze świetną kolejką wiadomości. Oto moja sugestia:

  • Jeśli mierzysz się z dużym ruchem i spodziewasz się wygenerowania tysięcy zdarzeń na minutę, to Kafka będzie tutaj naprawdę świetną opcją.
  • W innych przypadkach wykorzystaj Redis - szybciej go skonfigurować i utrzymywać.


Jeśli pracujesz na ekosystemie opartym na chmurze, to sprawdź natywne rozwiązania, takie jak AWS SQS, które również nieźle działają i da się je automatycznie skalować.

Wzorzec Circuit Breaker

Czy Twój serwis rzucił kiedykolwiek błędem przez niestabilną usługę zewnętrzną, z której korzystasz? A co, jeśli mógłbyś w jakiś sposób wykryć, kiedy to się zdarza i zaktualizować swoją logikę wewnętrzną w sposób dynamiczny? 

Do takich rzeczy mamy wzorzec Circuit Breaker, który zapewnia sposób na wykrycie popsutej zależności i zatrzymuje przepływ danych i pozwala uniknąć opóźnień i okropnego UX.

Możesz też wykorzystać ten wzorzec do komunikacji z serwisami wewnętrznymi - jeśli nie one działać, to będzie trzeba je naprawić. Niemniej jednak, nie można dużo zrobić z zewnętrznymi usługami, które się psują, prawda?

Potrzebujemy tutaj proxy dla naszego zewnętrznego API. Zrobi ono dwie rzeczy:

  1. Przekieruje komunikację z Twoich serwisów do zewnętrznego API.
  2. Track failing request - jeśli dana liczba przekroczy próg w zasięgu wstępnie zdefiniowanego okna czasu, to przestań na chwilę wysyłać żądania.


Kiedy pojawi się błąd, trzeba sobie z nim jakoś poradzić, ale rozwiązanie będzie zależne od potrzeb wynikających z wewnętrznej logiki. Dodałem w moim diagramie “default response”, zakładając że można dostarczyć jakiś rodzaj domyślnych danych, które pozwoliłby na kontynuację przepływu, nawet przy pomocy podstawowych informacji. Można też wybrać wyłączenie tej funkcji lub ustalenie jej jako serwisu drugorzędnego, który daje nam te same informacje. 

Alternatyw jest wiele i trzeba zdecydować, co jest najlepszym rozwiązaniem. Wzorzec daje Ci tylko lepsze zrozumienie takiego przypadku, a to co się dzieje potem, zależy już od Ciebie.

Lepsza wewnętrzna architektura dla mikroserisów 

Pomimo że powyższe wzorce są świetne przy tworzeniu elastycznych i wydajnych architektur na poziomie makro, to trzeba rozważyć również wewnętrzną strukturę mikroserwisów. 

To właśnie dlatego chciałem na szybko omówić kwestię najpowszechniejszych komponentów dla mikroserwisów.

Wszystkie powyższe wzorce pokazują Ci, jak wyrwać się ze wzorca monolitycznego i podzielić go na kilka serwisów. Niby korzyści są oczywiste: upraszcza pracę z wieloma zespołami, a rozszerzanie i aktualizowanie pojedynczego serwisu nie musi wpływać na resztę (tak samo działoby się w serwisie monolitycznym) - skalowanie indywidualnych serwisów jest też szybsze i prostsze. 

Niemniej jednak praca wielu zespołów równocześnie z kilkoma mikroserwisami może być prawdziwym logistycznym wyzwaniem, jeśli nie będzie się ostrożnym. Co najważniejsze:

  • Nie powtarzaj kodu. Postaraj się, aby pisany kod był jak najbardziej do siebie podobny we wszystkich zespołach. Nie zmuszaj nikogo do niewiadomo jakich wysiłków, kiedy reszta już ma swój styl kodowania. 
  • Upewnij się, że wszystkie zespoły pracują tak samo. Warto, aby wszystkie teamy miały takie same standardy, co zapewni łatwiejszą wymianę poszczególnych członków teamów. 
  • Warto mieć prosty mechanizm, aby móc używać kodu napisanego przez kogoś innego. Zgodnie z tym, co napisaliśmy w pierwszym punkcie, korzystanie z kodu innych teamów powinno być bardzo proste, co ułatwia development i redukuje czas. 
  • Upewnij się, że łatwo jest odnaleźć się w tym, nad czym pracują inni. Kiedy kilka zespołów pracuje jednocześnie, to dzielenie się pracą tak, aby można było jej użyć ponownie jest bardzo ważne - czasami teamy implementują na nowo jakąś bibliotekę, ponieważ nie wiedzą, co reszta tak naprawdę robi. 


Jeśli nie trzymasz się powyższego, to może się zdarzyć i tak, że development będzie trwał o wiele dłużej przez ponowną implementację (np. bibliotek do logowania czy do powtarzających się walidacji itd.), utrudnione dzielenie się wiedzą i dłuższy onboarding przy rekrutacjach wewnętrznych. 

Jak można to wszystko zaplanować?

Moja sugestia jest taka, żeby pamiętać o wszystkim, o czym tutaj mówię - każde działanie powinno być skierowane na zapewnienie developerom narzędzi, które pomogą im się uporać z tymi problemami. 

Osobiście, bardzo lubię Bit (GitHub), aby to wszystko scentralizować. Pozwala mi to na: 

  • Definiowanie pojedyńczego środowiska developerskiego, aby dzielić się wszystkim pomiędzy zespołami
  • Udostępnianie wspólnych modułów za pomocą standardowych instalacji oraz cały kod źródłowy, który je importuje. W obu przypadkach uzyskasz dostęp do modułu, tak jak npm.
    Drugi przypadek również daje Ci dostęp do kodu źródłowego na wypadek, gdybyś chciał go rozszerzyć i wyeksportować z powrotem do centralnego repozytorium. Jest to duża przewaga nad innymi menedżerami pakietów Node.js, które pozwalają tylko instalować moduły.
  • Dowiedz się, nad czym pracują inne zespoły i czy możesz ponownie wykorzystać ich moduły za pośrednictwem centralnego rynku, który może być zarówno publiczny (jeśli masz nadzieję na udostępnienie pracy swojej firmy), jak i prywatny, zapewniając dokładnie tę samą listę funkcji. 
  • Twórz i dokonuj deploymentu mikroserwisów. Korzystając z Bit, decydujesz, które komponenty są udostępniane i używane w Twoich mikroserwisach, a które są pełnymi mikroserwisami, których deployment odbywa się oddzielnie.


Dzięki Bitowi możesz dokonywać abstrakcji takich pojęć, jak „linter”, „menedżer pakietów”, „bundler” itd. i skupiać się tylko na tym, co jest do zrobienia. Zamiast upewniać się, czy używasz npm, yarn czy pnpm, pomartw się o użycie bit install. To wypełnia lukę między członkami zespołu, którzy znają jedno lub drugie narzędzie, i ujednolica sposób pracy wszystkich teamów.

Podsumowanie

Oczywiście możesz też zrobić to samo, korzystając z zestawu dobrze zdefiniowanych standardów i indywidualnych narzędzi. Jest to całkowicie możliwe i wiem, że działa. Po co miałbyś jednak samodzielnie podejmować dodatkowy wysiłek pisania i wyznaczania tych wszystkich standardów, jeśli jedno narzędzie zrobi to za Ciebie?

Jakie są Twoje ulubione wzorce dla mikroserwisów? Czy omówiłem je w tym artykule? A co z wewnętrzną architekturą mikroserwisów? Jak radzisz sobie z pracą wielu zespołów? Jak upewniasz się, że nie tworzą one wielokrotnie tych samych wspólnych bibliotek? Napisz w komentarzu pod artykułem! ??????



Oryginał tekstu w języku angielskim przeczytasz tutaj.

<p>Loading...</p>