paint-brush
Niezawodne przesyłanie wiadomości w systemach rozproszonychprzez@fairday
37,190 odczyty
37,190 odczyty

Niezawodne przesyłanie wiadomości w systemach rozproszonych

przez Aleksei8m2024/03/18
Read on Terminal Reader
Read this story w/o Javascript

Za długo; Czytać

Zbudowanie niezawodnego, wysoce dostępnego i skalowalnego systemu rozproszonego wymaga przestrzegania określonych technik, zasad i wzorców.
featured image - Niezawodne przesyłanie wiadomości w systemach rozproszonych
Aleksei HackerNoon profile picture

Problem z podwójnym zapisem

Budowa niezawodnego, wysoce dostępnego, skalowalnego systemu rozproszonego wymaga przestrzegania określonych technik, zasad i wzorców. Projektowanie takich systemów wiąże się z rozwiązywaniem niezliczonych wyzwań. Jednym z najbardziej rozpowszechnionych i podstawowych problemów jest problem podwójnego zapisu .


„Problem podwójnego zapisu” to wyzwanie, które pojawia się w systemach rozproszonych, głównie w przypadku wielu źródeł danych lub baz danych, które muszą być zsynchronizowane. Odnosi się do trudności w zapewnieniu, że zmiany danych są konsekwentnie zapisywane w różnych magazynach danych, takich jak bazy danych lub pamięci podręczne, bez wprowadzania problemów, takich jak niespójności danych, konflikty lub wąskie gardła wydajności.


Architektura mikrousług i baza danych wzorców na usługę przynosi wiele korzyści, takich jak niezależne wdrażanie i skalowanie, izolowane awarie i potencjalny wzrost prędkości rozwoju. Jednak operacje wymagają zmian między wieloma mikrousługami, co zmusza do myślenia o niezawodnym rozwiązaniu tego problemu.

Prawie prawdziwy przykład

Rozważmy scenariusz, w którym nasza domena obejmuje przyjmowanie wniosków pożyczkowych, ich ocenę, a następnie wysyłanie alertów z powiadomieniami do klientów.


W duchu zasady pojedynczej odpowiedzialności, prawa Conwaya i podejścia projektowania zorientowanego na domenę, po kilku sesjach burzy mózgów, cała domena została podzielona na trzy poddomeny z określonymi, ograniczonymi kontekstami o wyraźnych granicach, modelami domen i wszechobecnym językiem.


Pierwszy z nich ma za zadanie wdrażanie i kompilowanie nowych wniosków o pożyczkę. Drugi system ocenia te wnioski i podejmuje decyzje na podstawie dostarczonych danych. Ten proces oceny, obejmujący KYC/KYB, kontrole antyfraudowe i ryzyka kredytowego, może być czasochłonny, wymagając możliwości obsługi tysięcy wniosków jednocześnie. W związku z tym funkcjonalność ta została delegowana do dedykowanej mikrousługi z własną bazą danych, co umożliwia niezależne skalowanie.

Co więcej, podsystemami tymi zarządzają dwa różne zespoły, z których każdy ma własne cykle wydań, umowy o poziomie usług (SLA) i wymagania dotyczące skalowalności.


Na koniec wprowadzono specjalistyczną usługę powiadomień, za pomocą której klienci otrzymują alerty.



Poniżej przedstawiono szczegółowy opis podstawowego przypadku użycia systemu:

  1. Klient składa wniosek o pożyczkę.
  2. Usługa składania wniosków pożyczkowych rejestruje nowy wniosek ze statusem „Oczekujący” i rozpoczyna proces oceny, przekazując wniosek do Usługi oceny.
  3. Dział Oceny dokonuje oceny złożonego wniosku pożyczkowego i następnie informuje Dział Wniosków Pożyczkowych o decyzji.
  4. Po otrzymaniu decyzji Usługa Wniosków Pożyczkowych aktualizuje status wniosku pożyczkowego i uruchamia Usługę Powiadomień, aby poinformować klienta o wyniku.
  5. Usługa powiadomień przetwarza to żądanie i wysyła powiadomienia do klienta za pośrednictwem poczty e-mail, wiadomości SMS lub innej preferowanej metody komunikacji, zgodnie z ustawieniami klienta.


Na pierwszy rzut oka jest to dość prosty i prymitywny system. Przyjrzyjmy się jednak bliżej, w jaki sposób usługa składania wniosków pożyczkowych przetwarza polecenie przesłania wniosku pożyczkowego.


Możemy rozważyć dwa podejścia do interakcji usług:

  1. Najpierw lokalne zatwierdzenie, potem publikacja: w tym podejściu usługa aktualizuje swoją lokalną bazę danych (zatwierdza), a następnie publikuje zdarzenie lub wiadomość w innych usługach.

  2. Najpierw opublikuj, a potem zatwierdź lokalnie: Metoda ta polega na opublikowaniu zdarzenia lub komunikatu przed zatwierdzeniem zmian w lokalnej bazie danych.


Obie metody mają swoje wady i są tylko częściowo niezawodne w komunikacji w systemach rozproszonych.


Oto diagram sekwencyjny pokazujący zastosowanie pierwszego podejścia.


Najpierw-lokalne-zatwierdzenie-potem-publikacja


W tym scenariuszu usługa Loan Application Service stosuje podejście First-Local-Commit-Then-Publish , w którym najpierw zatwierdza transakcję, a następnie próbuje wysłać powiadomienie do innego systemu. Jednak proces ten jest podatny na awarię, jeśli na przykład występują problemy z siecią, usługa Assessment Service jest niedostępna lub usługa Loan Application Service napotka błąd Out of Memory (OOM) i ulegnie awarii. W takich przypadkach wiadomość zostanie utracona, pozostawiając usługę Assessment bez powiadomienia o nowym wniosku pożyczkowym, chyba że zostaną wdrożone dodatkowe środki.


I drugi.

Najpierw opublikuj, a następnie zatwierdź lokalnie
W scenariuszu First-Publish-Then-Local-Commit usługa Loan Application Service staje w obliczu poważniejszych ryzyk. Może poinformować usługę Assessment Service o nowej aplikacji, ale nie zapisać tej aktualizacji lokalnie z powodu problemów, takich jak problemy z bazą danych, błędy pamięci lub błędy kodu. Takie podejście może prowadzić do znacznych niespójności danych, co może powodować poważne problemy, w zależności od tego, jak usługa Loan Review Service obsługuje przychodzące aplikacje.


Dlatego musimy zidentyfikować rozwiązanie, które oferuje solidny mechanizm publikowania zdarzeń dla zewnętrznych konsumentów. Jednak zanim zagłębimy się w potencjalne rozwiązania, powinniśmy najpierw wyjaśnić rodzaje gwarancji dostarczania wiadomości możliwych do osiągnięcia w systemach rozproszonych.

Gwarancje dostarczenia wiadomości

Możemy osiągnąć cztery rodzaje gwarancji.

  1. Brak gwarancji
    Nie ma gwarancji, że wiadomość zostanie dostarczona do miejsca docelowego. Podejście First-Local-Commit-Then-Publish dotyczy właśnie tego. Konsumenci mogą otrzymać wiadomości raz, wiele razy lub wcale.

  2. Najpóźniej jednorazowa dostawa
    Dostawa maksymalnie raz oznacza, że wiadomość zostanie dostarczona do miejsca docelowego maksymalnie 1 raz. Podejście First-Local-Commit-Then-Publish można wdrożyć w ten sposób również z polityką ponawiania prób z wartością jeden.

  3. Co najmniej raz konsument otrzyma i przetworzy każdą wiadomość, ale może otrzymać tę samą wiadomość więcej niż jeden raz.

  4. Dostarczenie dokładnie raz\Dostarczenie dokładnie raz oznacza, że konsument otrzyma wiadomość efektywnie tylko raz.
    Technicznie rzecz biorąc, jest to możliwe dzięki transakcjom Kafki i specyficznej idempotentnej implementacji producenta i konsumenta.


W większości przypadków gwarancje dostawy „przynajmniej raz” rozwiązują wiele problemów, zapewniając dostarczenie wiadomości przynajmniej raz, ale konsumenci muszą być idempotentni. Jednak biorąc pod uwagę nieuniknione awarie sieci, cała logika konsumenta musi być idempotentna, aby uniknąć przetwarzania zduplikowanych wiadomości, niezależnie od gwarancji producenta. Dlatego też wymóg ten nie jest wadą, lecz odzwierciedla rzeczywistość.

Rozwiązania

Istnieje wiele rozwiązań tego problemu, każde ma swoje zalety i wady.

Zatwierdzenie dwufazowe

Według Wikipedii, Two-Phase Commit (2PC) to protokół rozproszonych transakcji używany w informatyce i systemach zarządzania bazami danych w celu zapewnienia spójności i niezawodności rozproszonych transakcji. Jest przeznaczony do sytuacji, w których wiele zasobów (np. baz danych) musi uczestniczyć w jednej transakcji i zapewnia, że wszystkie z nich zatwierdzą transakcję lub wszystkie ją anulują, zachowując w ten sposób spójność danych. Brzmi to dokładnie tak, jak potrzebujemy, ale Two-Phase Commit ma kilka wad:

  • Jeśli jeden uczestniczący zasób przestanie odpowiadać lub wystąpi awaria, cały proces może zostać zablokowany do czasu rozwiązania problemu. Może to prowadzić do potencjalnych problemów z wydajnością i dostępnością.
  • Two-Phase Commit nie zapewnia wbudowanych mechanizmów tolerancji błędów. Polega na mechanizmach zewnętrznych lub ręcznej interwencji w celu obsługi awarii.
  • Nie wszystkie nowoczesne bazy danych obsługują zatwierdzanie dwufazowe.

Wspólna baza danych

Najbardziej oczywistym rozwiązaniem dla architektury mikrousług jest zastosowanie wzorca (lub nawet czasami antywzorca) — współdzielonej bazy danych. To podejście jest bardzo intuicyjne, jeśli potrzebujesz spójności transakcyjnej w wielu tabelach w różnych bazach danych, po prostu użyj jednej współdzielonej bazy danych dla tych mikrousług.


Wady tego podejścia obejmują wprowadzenie pojedynczego punktu awarii, hamowanie niezależnego skalowania bazy danych i ograniczenie możliwości korzystania z różnych rozwiązań baz danych najlepiej dostosowanych do konkretnych wymagań i przypadków użycia. Ponadto modyfikacje baz kodu mikrousług byłyby konieczne w celu obsługi takiej formy rozproszonej transakcji.

Skrzynka nadawcza transakcyjna

Transakcyjny outbox ” to wzorzec projektowy używany w systemach rozproszonych w celu zapewnienia niezawodnej propagacji wiadomości, nawet w obliczu zawodnych systemów przesyłania wiadomości. Polega on na przechowywaniu zdarzeń w wyznaczonej tabeli „OutboxEvents” w ramach tej samej transakcji, co sama operacja. To podejście dobrze wpisuje się w właściwości ACID baz danych relacyjnych. Z kolei wiele baz danych No-SQL nie obsługuje w pełni właściwości ACID, decydując się zamiast tego na zasady twierdzenia CAP i filozofii BASE, które stawiają dostępność i ostateczną spójność ponad ścisłą spójność.


Skrzynka nadawcza transakcyjna zapewnia co najmniej jednorazową gwarancję i może być wdrożona na kilka sposobów:

  1. Śledzenie dziennika transakcji

  2. Wydawca ankiet


Podejście śledzenia dziennika transakcji oznacza korzystanie z rozwiązań specyficznych dla bazy danych, takich jak CDC (Change Data Capture). Główne wady tego podejścia to:

  • Rozwiązania specyficzne dla baz danych

  • Zwiększone opóźnienie ze względu na specyfikę implementacji CDC


Inną metodą jest Polling Publisher , który ułatwia outbox offloading poprzez sondowanie tabeli outbox. Podstawową wadą tego podejścia jest potencjalne zwiększone obciążenie bazy danych, co może prowadzić do wyższych kosztów. Ponadto nie wszystkie bazy danych No-SQL obsługują wydajne zapytania dotyczące określonych segmentów dokumentów. Wyodrębnianie całych dokumentów może zatem skutkować pogorszeniem wydajności.


Poniżej znajduje się krótki diagram sekwencji wyjaśniający jak to działa.


Słuchaj siebie

Podstawowym wyzwaniem związanym ze wzorcem Transactional Outbox jest jego zależność od właściwości ACID bazy danych. Może być prosty w typowych bazach danych OLTP, ale stwarza wyzwania w obszarze NoSQL. Aby temu zaradzić, potencjalnym rozwiązaniem jest wykorzystanie dziennika dołączania (na przykład Kafka) bezpośrednio od zainicjowania przetwarzania żądania.


Zamiast bezpośrednio przetwarzać polecenie „złóż wniosek o pożyczkę”, natychmiast wysyłamy je do wewnętrznego tematu Kafki, a następnie zwracamy klientowi wynik „zaakceptowany”. Jednak ponieważ jest wysoce prawdopodobne, że polecenie nadal musi zostać przetworzone, nie możemy natychmiast poinformować klienta o wyniku. Aby zarządzać tą ostateczną spójnością, możemy zastosować techniki takie jak długie sondowanie, sondowanie inicjowane przez klienta, optymistyczne aktualizacje interfejsu użytkownika lub używanie WebSockets lub zdarzeń wysyłanych przez serwer do powiadomień. Jest to jednak zupełnie odrębny temat, więc wróćmy do naszego początkowego tematu.


Wysłaliśmy wiadomość w wewnętrznym temacie Kafki. Następnie usługa Loan Application Service konsumuje tę wiadomość — to samo polecenie, które otrzymała od klienta — i rozpoczyna przetwarzanie. Najpierw wykonuje pewną logikę biznesową; dopiero po pomyślnym wykonaniu tej logiki i utrwaleniu wyników publikuje nowe wiadomości w publicznym temacie Kafki.


Przyjrzyjmy się fragmentowi pseudokodu.


 public async Task HandleAsync(SubmitLoanApplicationCommand command, ...) { //First, process business logic var loanApplication = await _loanApplicationService.HandleCommandAsync(command, ...); //Then, send new events to public Kafka topic producer.Send(new LoanApplicationSubmittedEvent(loanApplication.Id)); //Then, commit offset consumer.Commit(); }


Co się stanie, jeśli przetwarzanie logiki biznesowej się nie powiedzie? Nie martw się, ponieważ offset nie został jeszcze zatwierdzony, wiadomość zostanie ponowiona.


Co się stanie, jeśli wysłanie nowych zdarzeń do Kafki się nie powiedzie? Nie martw się, ponieważ logika biznesowa jest idempotentna, nie utworzy ona duplikatu wniosku o pożyczkę. Zamiast tego spróbuje ponownie wysłać wiadomości do publicznego tematu Kafki.


Co się stanie, jeśli wiadomości zostaną wysłane do Kafki, ale offset commit się nie powiedzie? Nie martw się, ponieważ logika biznesowa jest idempotentna, nie utworzy ona duplikatu wniosku o pożyczkę. Zamiast tego ponownie wyśle wiadomości do publicznego tematu Kafki i będzie mieć nadzieję, że offset commit tym razem się powiedzie.


Do głównych wad tego podejścia zalicza się dodatkową złożoność związaną z nowym stylem programowania, ostateczną spójność (ponieważ klient nie zna od razu wyniku) i wymóg idempotentności całej logiki biznesowej.

Źródło zdarzeń

Czym jest event sourcing i jak można go tutaj zastosować? Event sourcing to wzorzec architektoniczny oprogramowania używany do modelowania stanu systemu poprzez przechwytywanie wszystkich zmian jego danych jako serii niezmiennych zdarzeń. Te zdarzenia reprezentują fakty lub przejścia stanu i służą jako jedyne źródło prawdy dla bieżącego stanu systemu. Tak więc technicznie rzecz biorąc, wdrażając system event-sourcing, mamy już wszystkie zdarzenia w EventStore, a ten EventStore może być używany przez konsumentów jako pojedyncze źródło prawdy o tym, co się wydarzyło. Nie ma potrzeby specjalnego rozwiązania bazy danych do śledzenia wszystkich zmian ani obaw dotyczących kolejności, jedynym problemem jest siedzenie po stronie odczytu, ponieważ aby móc uzyskać rzeczywisty stan bytu, wymagane jest odtworzenie wszystkich zdarzeń.

Wniosek

W tym artykule omówiliśmy kilka podejść do budowania niezawodnego przesyłania wiadomości w systemach rozproszonych. Istnieje kilka rekomendacji, które możemy wziąć pod uwagę podczas budowania systemów o tych cechach

  1. Zawsze rozwijaj idempotentnych konsumentów, ponieważ awaria sieci jest nieunikniona.
  2. Ostrożnie stosuj zasadę „Najpierw zatwierdź lokalnie, a potem opublikuj”, dokładnie rozumiejąc wymagania dotyczące gwarancji.
  3. Nigdy nie stosuj podejścia „Najpierw opublikuj, a potem zatwierdź lokalnie”, ponieważ może to doprowadzić do poważnej niespójności danych w systemie.
  4. Jeśli istnieje duże prawdopodobieństwo, że decyzja dotycząca wyboru istniejącej bazy danych ulegnie zmianie lub strategia techniczna wymaga wybrania najlepszego rozwiązania do przechowywania danych dla danego problemu — nie należy tworzyć bibliotek współdzielonych poprzez wiązanie się z rozwiązaniami baz danych, takimi jak CDC .
  5. Stosuj podejście Transactional Outbox jako standardowe rozwiązanie w celu uzyskania gwarancji co najmniej jednokrotnego wysłania wiadomości.
  6. Warto rozważyć zastosowanie podejścia „Słuchaj siebie” w przypadku korzystania z baz danych No-SQL.


Następnym razem przyjrzymy się bardziej praktycznemu przykładowi implementacji Transactional Outbox. Zobacz

Ty!