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.
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:
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:
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.
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.
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.
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.
Możemy osiągnąć cztery rodzaje gwarancji.
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.
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.
Co najmniej raz konsument otrzyma i przetworzy każdą wiadomość, ale może otrzymać tę samą wiadomość więcej niż jeden raz.
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ść.
Istnieje wiele rozwiązań tego problemu, każde ma swoje zalety i wady.
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:
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.
„ 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:
Śledzenie dziennika transakcji
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.
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.
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ń.
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
Następnym razem przyjrzymy się bardziej praktycznemu przykładowi implementacji Transactional Outbox. Zobacz
Ty!