Projektowanie komunikacji międzyusługowej dla mikrousług

Azure DevOps

Komunikacja między mikrousługami musi być wydajna i niezawodna. W przypadku korzystania z wielu małych usług w celu ukończenia jednej działalności biznesowej może to być wyzwanie. W tym artykule przyjrzymy się kompromisom między asynchronicznymi komunikatami a synchronicznymi interfejsami API. Następnie przyjrzymy się niektórym wyzwaniom związanym z projektowaniem odpornej komunikacji międzyusługowej.

Wyzwania

Oto niektóre z głównych wyzwań wynikających z komunikacji między usługami. Siatki usług, opisane w dalszej części tego artykułu, są przeznaczone do obsługi wielu z tych wyzwań.

Odporność. Mogą istnieć dziesiątki lub nawet setki wystąpień dowolnej mikrousługi. Wystąpienie może zakończyć się niepowodzeniem z dowolnej liczby powodów. Może wystąpić awaria na poziomie węzła, taka jak awaria sprzętowa lub ponowny rozruch maszyny wirtualnej. Wystąpienie może ulec awarii lub być przeciążone żądaniami i nie może przetworzyć żadnych nowych żądań. Każde z tych zdarzeń może spowodować niepowodzenie wywołania sieciowego. Istnieją dwa wzorce projektowe, które mogą pomóc zwiększyć odporność wywołań sieciowych typu usługa-usługa:

  • Ponów próbę. Wywołanie sieciowe może zakończyć się niepowodzeniem z powodu błędu przejściowego, który odchodzi samodzielnie. Zamiast awarii wprost, obiekt wywołujący powinien zazwyczaj ponowić próbę wykonania operacji określoną liczbę razy lub do momentu, aż upłynie skonfigurowany okres limitu czasu. Jeśli jednak operacja nie jest idempotentna, ponawianie prób może spowodować niezamierzone skutki uboczne. Oryginalne wywołanie może zakończyć się pomyślnie, ale obiekt wywołujący nigdy nie otrzyma odpowiedzi. Jeśli obiekt wywołujący ponawia próbę, operacja może zostać wywołana dwukrotnie. Ogólnie rzecz biorąc, nie można bezpiecznie ponowić próbę metod POST lub PATCH, ponieważ nie ma gwarancji, że są one idempotentne.

  • Wyłącznik. Zbyt wiele żądań, które zakończyły się niepowodzeniem, może powodować wąskie gardło, ponieważ oczekujące żądania gromadzą się w kolejce. Te zablokowane żądania mogą przechowywać krytyczne zasoby systemu, takie jak pamięć, wątki, połączenia bazy danych itd., co może powodować awarie kaskadowe. Wzorzec wyłącznika może uniemożliwić usłudze wielokrotne wypróbowanie operacji, która może zakończyć się niepowodzeniem.

Równoważenie obciążenia. Gdy usługa "A" wywołuje usługę "B", żądanie musi dotrzeć do uruchomionego wystąpienia usługi "B". W usłudze Kubernetes Service typ zasobu zapewnia stabilny adres IP dla grupy zasobników. Ruch sieciowy do adresu IP usługi jest przekazywany do zasobnika za pomocą reguł iptable. Domyślnie wybierany jest losowy zasobnik. Siatka usług (patrz poniżej) może zapewnić bardziej inteligentne algorytmy równoważenia obciążenia na podstawie zaobserwowanego opóźnienia lub innych metryk.

Śledzenie rozproszone. Pojedyncza transakcja może obejmować wiele usług. Może to utrudnić monitorowanie ogólnej wydajności i kondycji systemu. Nawet jeśli każda usługa generuje dzienniki i metryki, bez konieczności łączenia ich ze sobą, są one ograniczone. Artykuł Rejestrowanie i monitorowanie zawiera więcej informacji na temat śledzenia rozproszonego, ale wspominamy go tutaj jako wyzwanie.

Przechowywanie wersji usługi. Gdy zespół wdraża nową wersję usługi, musi unikać przerywania wszelkich innych usług lub klientów zewnętrznych, które są od niej zależne. Ponadto możesz chcieć uruchamiać wiele wersji usługi obok siebie i kierować żądania do określonej wersji. Aby uzyskać więcej informacji na temat tego problemu, zobacz Przechowywanie wersji interfejsu API .

Szyfrowanie TLS i wzajemne uwierzytelnianie TLS. Ze względów bezpieczeństwa możesz chcieć zaszyfrować ruch między usługami przy użyciu protokołu TLS i użyć wzajemnego uwierzytelniania TLS do uwierzytelniania wywołujących.

Synchroniczne a asynchroniczne komunikaty

Istnieją dwa podstawowe wzorce obsługi komunikatów, których mikrousługi mogą używać do komunikowania się z innymi mikrousługami.

  1. Komunikacja synchroniczna. W tym wzorcu usługa wywołuje interfejs API uwidacznianą przez inną usługę przy użyciu protokołu, takiego jak HTTP lub gRPC. Ta opcja jest wzorcem synchronicznej obsługi komunikatów, ponieważ obiekt wywołujący czeka na odpowiedź z odbiornika.

  2. Przekazywanie komunikatów asynchronicznych. W tym wzorcu usługa wysyła komunikat bez oczekiwania na odpowiedź, a co najmniej jedna usługa przetwarza komunikat asynchronicznie.

Ważne jest rozróżnienie między asynchronicznym we/wy i protokołem asynchronicznym. Asynchroniczne we/wy oznacza, że wątek wywołujący nie jest blokowany podczas wykonywania operacji we/wy. Jest to ważne dla wydajności, ale jest szczegółem implementacji pod względem architektury. Protokół asynchroniczny oznacza, że nadawca nie czeka na odpowiedź. Protokół HTTP jest protokołem synchronicznym, mimo że klient HTTP może używać asynchronicznego we/wy podczas wysyłania żądania.

Istnieją kompromisy w każdym wzorcu. Żądanie/odpowiedź to dobrze rozumiany paradygmat, więc projektowanie interfejsu API może wydawać się bardziej naturalne niż projektowanie systemu obsługi komunikatów. Jednak asynchroniczna obsługa komunikatów ma pewne zalety, które mogą być przydatne w architekturze mikrousług:

  • Zmniejszone sprzężenie. Nadawca komunikatu nie musi wiedzieć o użytkowniku.

  • Wielu subskrybentów. Korzystając z modelu pub/sub, wielu odbiorców może subskrybować odbieranie zdarzeń. Zobacz Styl architektury sterowanej zdarzeniami.

  • Izolacja awarii. Jeśli odbiorca ulegnie awarii, nadawca nadal może wysyłać komunikaty. Komunikaty zostaną odebrane po odzyskaniu przez użytkownika. Ta możliwość jest szczególnie przydatna w architekturze mikrousług, ponieważ każda usługa ma własny cykl życia. Usługa może stać się niedostępna lub zostać zastąpiona nowszą wersją w danym momencie. Asynchroniczne komunikaty mogą obsługiwać sporadyczne przestoje. Z drugiej strony synchroniczne interfejsy API wymagają dostępności usługi podrzędnej lub operacja kończy się niepowodzeniem.

  • Czas odpowiedzi. Usługa nadrzędna może odpowiedzieć szybciej, jeśli nie czeka na usługi podrzędne. Jest to szczególnie przydatne w architekturze mikrousług. Jeśli istnieje łańcuch zależności usług (usługa A wywołuje usługę B, która wywołuje język C itd.), oczekiwanie na wywołania synchroniczne może dodać niedopuszczalne ilości opóźnień.

  • Bilansowanie obciążenia. Kolejka może pełnić rolę buforu w celu wyrównania obciążenia, dzięki czemu odbiorcy mogą przetwarzać komunikaty we własnym tempie.

  • Przepływy pracy. Kolejki mogą służyć do zarządzania przepływem pracy przez sprawdzenie komunikatu po każdym kroku przepływu pracy.

Istnieją jednak również pewne wyzwania związane z efektywnym używaniem asynchronicznej obsługi komunikatów.

  • Sprzęganie z infrastrukturą obsługi komunikatów. Użycie konkretnej infrastruktury obsługi komunikatów może spowodować ścisłe sprzężenie z daną infrastrukturą. Później trudno będzie przełączyć się do innej infrastruktury obsługi komunikatów.

  • Opóźnienie. Całkowite opóźnienie operacji może stać się wysokie, jeśli kolejki komunikatów zapełniają się.

  • Koszt. W przypadku wysokiej przepływności koszt pieniężny infrastruktury obsługi komunikatów może być znaczący.

  • Złożoność. Obsługa asynchronicznej obsługi komunikatów nie jest banalnym zadaniem. Na przykład należy obsługiwać zduplikowane komunikaty przez deduplikację lub tworzenie idempotentnych operacji. Trudno jest również zaimplementować semantykę żądań-odpowiedzi przy użyciu asynchronicznej obsługi komunikatów. Aby wysłać odpowiedź, potrzebujesz innej kolejki oraz sposobu korelowania komunikatów żądań i odpowiedzi.

  • Przepływność. Jeśli komunikaty wymagają semantyki kolejki, kolejka może stać się wąskim gardłem w systemie. Każdy komunikat wymaga co najmniej jednej operacji kolejki i jednej operacji dequeue. Ponadto semantyka kolejek zwykle wymaga pewnego rodzaju blokowania wewnątrz infrastruktury obsługi komunikatów. Jeśli kolejka jest usługą zarządzaną, może wystąpić dodatkowe opóźnienie, ponieważ kolejka jest zewnętrzna dla sieci wirtualnej klastra. Możesz rozwiązać te problemy, tworząc partie komunikatów, ale to komplikuje kod. Jeśli komunikaty nie wymagają semantyki kolejki, może być możliwe użycie strumienia zdarzeń zamiast kolejki. Aby uzyskać więcej informacji, zobacz Styl architektury opartej na zdarzeniach.

Dostarczanie dronów: wybieranie wzorców obsługi komunikatów

To rozwiązanie korzysta z przykładu Drone Delivery. Jest idealnym rozwiązaniem dla przemysłu lotniczego i lotniczego.

Mając na uwadze te zagadnienia, zespół programistyczny dokonał następujących wyborów projektowych dla aplikacji Drone Delivery:

  • Usługa pozyskiwania uwidacznia publiczny interfejs API REST używany przez aplikacje klienckie do planowania, aktualizowania lub anulowania dostaw.

  • Usługa pozyskiwania używa usługi Event Hubs do wysyłania asynchronicznych komunikatów do usługi Scheduler. Komunikaty asynchroniczne są niezbędne do zaimplementowania bilansowania obciążenia wymaganego do pozyskiwania.

  • Usługi account, Delivery, Package, Drone i Third-party Transport services uwidaczniają wewnętrzne interfejsy API REST. Usługa Scheduler wywołuje te interfejsy API w celu wykonania żądania użytkownika. Jednym z powodów używania synchronicznych interfejsów API jest to, że usługa Scheduler musi uzyskać odpowiedź z każdej z usług podrzędnych. Awaria w dowolnej z usług podrzędnych oznacza, że cała operacja nie powiodła się. Jednak potencjalny problem polega na tym, że opóźnienie jest wprowadzane przez wywołanie usług zaplecza.

  • Jeśli jakakolwiek usługa podrzędna ma nieprzejrzałą awarię, cała transakcja powinna zostać oznaczona jako nieudana. Aby obsłużyć ten przypadek, usługa Scheduler wysyła asynchroniczny komunikat do nadzorcy, aby nadzorca mógł zaplanować transakcje wyrównujące.

  • Usługa dostarczania uwidacznia publiczny interfejs API, którego klienci mogą używać do uzyskiwania stanu dostawy. W artykule Brama interfejsu API omówiono sposób, w jaki brama interfejsu API może ukryć podstawowe usługi od klienta, więc klient nie musi wiedzieć, które usługi uwidaczniają, które interfejsy API.

  • Podczas gdy dron jest w locie, usługa Drone wysyła zdarzenia, które zawierają bieżącą lokalizację i stan drona. Usługa dostarczania nasłuchuje tych zdarzeń w celu śledzenia stanu dostawy.

  • Gdy stan dostawy zmieni się, usługa dostarczania wysyła zdarzenie stanu dostawy, takie jak DeliveryCreated lub DeliveryCompleted. Każda usługa może subskrybować te zdarzenia. W bieżącym projekcie usługa Historia dostarczania jest jedynym subskrybentem, ale później mogą istnieć inni subskrybenci. Na przykład zdarzenia mogą przejść do usługi analizy w czasie rzeczywistym. Ponieważ harmonogram nie musi czekać na odpowiedź, dodanie kolejnych subskrybentów nie ma wpływu na główną ścieżkę przepływu pracy.

Diagram komunikacji dronów

Zwróć uwagę, że zdarzenia stanu dostawy pochodzą z zdarzeń lokalizacji dronów. Na przykład gdy dron osiągnie lokalizację dostawy i wysunie pakiet, usługa dostarczania przełoży to na zdarzenie DeliveryCompleted. Jest to przykład myślenia pod względem modeli domen. Zgodnie z wcześniejszym opisem zarządzanie dronami należy do oddzielnego powiązanego kontekstu. Zdarzenia dronów przekazują fizyczną lokalizację drona. Zdarzenia dostarczania, z drugiej strony, reprezentują zmiany w stanie dostarczania, który jest inną jednostką biznesową.

Korzystanie z siatki usług

Siatka usług to warstwa oprogramowania, która obsługuje komunikację między usługami. Siatki usług zostały zaprojektowane tak, aby rozwiązać wiele problemów wymienionych w poprzedniej sekcji i przenieść odpowiedzialność za te problemy z dala od samych mikrousług i do warstwy udostępnionej. Siatka usług działa jako serwer proxy, który przechwytuje komunikację sieciową między mikrousługami w klastrze. Obecnie koncepcja siatki usług dotyczy głównie orkiestratorów kontenerów, a nie architektur bezserwerowych.

Uwaga

Siatka usługi to przykład wzorca ambasadora — usługa pomocnika, która wysyła żądania sieciowe w imieniu aplikacji.

W tej chwili główne opcje siatki usługi na platformie Kubernetes to Linkerd i Istio. Obie te technologie szybko ewoluują. Jednak niektóre funkcje, które zarówno Linkerd, jak i Istio mają wspólne cechy:

  • Równoważenie obciążenia na poziomie sesji na podstawie obserwowanych opóźnień lub liczby zaległych żądań. Może to zwiększyć wydajność w przypadku równoważenia obciążenia warstwy 4 udostępnianego przez platformę Kubernetes.

  • Routing w warstwie 7 na podstawie ścieżki adresu URL, nagłówka hosta, wersji interfejsu API lub innych reguł na poziomie aplikacji.

  • Ponów próbę żądań, które zakończyły się niepowodzeniem. Siatka usługi rozumie kody błędów HTTP i może automatycznie ponowić nieudane żądania. Można skonfigurować maksymalną liczbę ponownych prób wraz z okresem przekroczenia limitu czasu w celu ograniczenia maksymalnego opóźnienia.

  • Przerwanie obwodu. Jeśli wystąpienie stale kończy się niepowodzeniem, siatka usług tymczasowo oznaczy ją jako niedostępną. Po upływie okresu wycofywania spróbuje ponownie wystąpienie. Wyłącznik można skonfigurować na podstawie różnych kryteriów, takich jak liczba kolejnych awarii,

  • Siatka usługi przechwytuje metryki dotyczące wywołań międzyusługowych, takich jak wolumin żądania, opóźnienie, współczynniki błędów i powodzenia oraz rozmiary odpowiedzi. Siatka usług umożliwia również śledzenie rozproszone przez dodanie informacji korelacji dla każdego przeskoku w żądaniu.

  • Wzajemne uwierzytelnianie TLS dla wywołań typu service-to-service.

Czy potrzebujesz siatki usług? To zależy. Bez siatki usług należy wziąć pod uwagę każde z wyzwań wymienionych na początku tego artykułu. Możesz rozwiązać problemy, takie jak ponawianie, wyłącznik i śledzenie rozproszone bez siatki usługi, ale siatka usług przenosi te obawy z poszczególnych usług i do dedykowanej warstwy. Z drugiej strony siatka usługi zwiększa złożoność konfiguracji i konfiguracji klastra. Mogą wystąpić konsekwencje dotyczące wydajności, ponieważ żądania są teraz kierowane przez serwer proxy siatki usług i dlatego, że dodatkowe usługi są teraz uruchomione w każdym węźle w klastrze. Przed wdrożeniem siatki usług w środowisku produkcyjnym należy wykonać dokładną wydajność i testowanie obciążenia.

Transakcje rozproszone

Typowym wyzwaniem w mikrousługach jest prawidłowa obsługa transakcji obejmujących wiele usług. Często w tym scenariuszu powodzenie transakcji jest wszystkie lub nic — jeśli jedna z uczestniczących usług zakończy się niepowodzeniem, cała transakcja musi zakończyć się niepowodzeniem.

Należy wziąć pod uwagę dwa przypadki:

  • Usługa może napotkać przejściowy błąd, taki jak przekroczenie limitu czasu sieci. Te błędy często można rozwiązać po prostu, ponawiając próbę wywołania. Jeśli operacja nadal kończy się niepowodzeniem po określonej liczbie prób, jest ona uznawana za nietransientną awarię.

  • Nietransientna awaria to każda awaria, która jest mało prawdopodobna, aby uciec samodzielnie. Nieprzejściowe błędy obejmują normalne warunki błędu, takie jak nieprawidłowe dane wejściowe. Obejmują one również nieobsługiwane wyjątki w kodzie aplikacji lub awarii procesu. Jeśli wystąpi ten typ błędu, cała transakcja biznesowa musi zostać oznaczona jako niepowodzenie. Może być konieczne cofnięcie innych kroków w tej samej transakcji, która już się powiodła.

Po nieprzejaśnianym niepowodzeniu bieżąca transakcja może być w stanie częściowo zakończonym niepowodzeniem , w którym co najmniej jeden krok zakończył się pomyślnie. Jeśli na przykład usługa Drone już zaplanowała drona, dron musi zostać anulowany. W takim przypadku aplikacja musi cofnąć kroki, które zakończyły się pomyślnie, przy użyciu transakcji wyrównywałej. W niektórych przypadkach należy to zrobić przez system zewnętrzny, a nawet przez proces ręczny.

Jeśli logika transakcji wyrównywujących jest złożona, rozważ utworzenie oddzielnej usługi odpowiedzialnej za ten proces. W aplikacji Drone Delivery usługa Scheduler umieszcza nieudane operacje w dedykowanej kolejce. Oddzielna mikrousługa o nazwie Nadzorca odczytuje z tej kolejki i wywołuje interfejs API anulowania w usługach, które muszą zrekompensować. Jest to odmiana wzorca nadzorcy agenta harmonogramu. Usługa Nadzorca może również wykonywać inne działania, takie jak powiadamianie użytkownika za pomocą tekstu lub wiadomości e-mail lub wysyłanie alertu do pulpitu nawigacyjnego operacji.

Diagram przedstawiający mikrousługę nadzorcy

Sama usługa Scheduler może zakończyć się niepowodzeniem (na przykład z powodu awarii węzła). W takim przypadku nowe wystąpienie może uruchamiać się i przejmować. Jednak wszelkie transakcje, które były już w toku, muszą zostać wznowione.

Jednym z podejść jest zapisanie punktu kontrolnego w trwałym magazynie po zakończeniu każdego kroku przepływu pracy. Jeśli wystąpienie usługi Scheduler ulegnie awarii w środku transakcji, nowe wystąpienie może użyć punktu kontrolnego, aby wznowić działanie poprzedniego wystąpienia. Jednak pisanie punktów kontrolnych może spowodować obciążenie wydajności.

Inną opcją jest zaprojektowanie wszystkich operacji, które mają być idempotentne. Operacja jest idempotentna, jeśli może być wywoływana wiele razy bez produkcji dodatkowych skutków ubocznych po pierwszym wywołaniu. Zasadniczo usługa podrzędna powinna ignorować zduplikowane wywołania, co oznacza, że usługa musi być w stanie wykryć zduplikowane wywołania. Implementacja metod idempotentnych nie zawsze jest prosta. Aby uzyskać więcej informacji, zobacz Operacje idempotentne.

Następne kroki

W przypadku mikrousług, które komunikują się bezpośrednio ze sobą, ważne jest, aby tworzyć dobrze zaprojektowane interfejsy API.