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.
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:
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.
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.
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:
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 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ć.
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:
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.
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:
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.
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:
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.
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.