Saga Pattern

Kiedyś, kiedy byłem jeszcze dzieckiem i nie wiedziałem co to Saga Pattern, otworzyłem szafkę w kuchni i wtem ujrzałem to:

Saga na kawce

Tak, to była pierwsza Saga na Kawce, jaką widziałem w życiu. Wtedy zrobiła to moja mama, ale od tamtej pory wiedziałem, że ja też chcę robić w życiu Sagę na Kafce – Udało się! Dobra, żarciki na bok, przejdźmy do wzorca Sagi!

  1. Wstęp
  2. Transakcje rozproszone vs Saga Pattern
  3. Czym jest Saga Pattern?
  4. Saga Pattern orkiestracja vs choreografia
  5. Saga Pattern na przykładzie aplikacja do zamawiania jedzenia
  6. Co jeśli proces kompensacji się nie powiedzie!?
  7. Sprostowanie — terminologia
  8. Linki
Jeśli wolisz wideo to zapraszam na YT 🙂

1. Wstęp

Wzorzec Saga powstał w roku 1987 i pierwotnie odnosił się do jednej bazy danych, w skrócie do tego, żeby jedną wielką operację na bazie danych wykonać w kilku mniejszych transakcjach i w razie faila, móc cofnąć zmiany (white paper). W dzisiejszych czasach, ze względu na popularność architektury mikroserwisowej, Saga Pattern w zdecydowanej większości przypadków będzie odnosiła się do architektury rozproszonej i do wykonywania operacji za pośrednictwem mikroserwisów na kilku bazach danych. Na tym też przypadku skupię się w poniższym wpisie.

2. Transakcje rozproszone vs Saga Pattern

Jak dobrze wiem, transakcje bazodanowe gwarantują nam spójność, czyli w ramach jednej transakcji, gdy modyfikujemy dwie tabele, zmiany zostaną wykonane na obu tabelach lub na żadnej z nich. Jednak nie jest to już tak proste, jeśli te dwie tabele znajdują się w osobnych bazach danych, co często się zdarza w architekturze mikroserwisów. W takich przypadkach nie możemy już skorzystać ze zwykłej transakcji bazodanowej. Do tego celu czasami można użyć transakcji rozproszonych, na przykład 2PC (Two-phase commit protocol).

Transakcje rozproszone (2PC)Saga Pattern
DziałaniePozwala na zatwierdzenie całej transakcji w jednym żądaniu, obejmując to żądanie przez różne systemy i sieci. Zakładając, że każdy uczestniczący system i sieć przestrzega protokołu.Pozwala na podzielenie transakcji na wiele kroków, które mogą trwać długo. Każda operacja jest zatwierdzana przed kolejną, co sprawia, że dane stają się ostatecznie spójne.
Trwałość
Transakcja jest atomowa i musi być zatwierdzona lub cofnięta jako całość.Transakcja może być rozłożona na wiele kroków, a każdy krok może mieć własną logikę zatwierdzania lub cofania.
SkalowalnośćSkalowalność jest ograniczona przez potrzebę komunikacji i synchronizacji między uczestnikami.Bardziej elastyczny i skalowalny, ponieważ każdy krok może być obsługiwany przez oddzielne komponenty.
Wymagania systemoweWymaga wsparcia 2PC przez platformy i systemy uczestniczące.Jest bardziej uniwersalny i niezależny od konkretnych platform.
ZastosowanieNadaję się tylko dla transakcji natychmiastowych.Zazwyczaj używany do długotrwałych transakcji.

Wzorzec Saga jest ogólnie uważany za zdolny do osiągnięcia tego, czego można dokonać za pomocą 2PC, ale nie odwrotnie.
Z powodu wad protokołu 2PC, takich jak ryzyko pojedynczego punktu awarii, zależność od najwolniejszej usługi, problemy ze skalowalnością i brak wsparcia w bazach NoSQL, rzadko kiedy jest on wybierany w architekturze mikroserwisów.
Oczywiście istnieją także inne algorytmy do zarządzania transakcjami rozproszonymi, ale jako branża doszliśmy do wniosku, że transakcje rozproszone nie działają 🙂

3. Czym jest Saga Pattern?

W kontekście mikrousług, gdzie różne usługi mogą mieć swoje własne, odseparowane bazy danych, Saga to sekwencja operacji, które są wykonywane po kolei, aby osiągnąć określony cel. Każda z tych operacji jest lokalną transakcją w ramach jednej usługi. Na razie brzmi to jak zwykłe zlecanie wykonania operacji dla danego mikroserwisu, więc gdzie ta moc Sagi?

W kontekście pojedynczej transakcyjnej bazy danych, jeśli coś pójdzie nie tak, zastosowany zostanie rollback, który automatycznie cofnie wszystkie zmiany. Jednakże, gdy mamy do czynienia z długotrwałym procesem biznesowym, który jest rozproszony pomiędzy różne mikroserwisy, nie możemy po prostu zastosować rollbacka, który automatycznie cofnie wszystkie zmiany w każdej z baz danych.

Aby cofnąć zmiany w różnych bazach danych, musimy wykonać dodatkowe operacje, których celem jest przywrócenie poprzedniego stanu. Jeżeli to nie jest możliwe, musimy wykonać jakąś operację, która poinformuje o niepowodzeniu którejś z faz procesu. W kontekście Sagi, ten proces cofania nazywamy kompensacją. Oczywiście każda kompensacja musi zostać przez nas zaimplementowana, ale na tym polega ten wzorzec.

Podsumowując, wzorzec Sagi pozwala nam utrzymanie ostatecznej spójności w rozproszonym środowisku z wieloma bazami danych.

Aby w pełni zrozumieć dalszą część wpisu, musisz być świadomy różnicy między Command a Event.
Command (komenda) – wiadomość wydająca rozkaz, kierowana tylko do jednego odbiorcy.
Event (zdarzenie) – wiadomość mówiąca, co się wydarzyło. Może mieć od 0 do n odbiorców.
Aby dowiedzieć się więcej na ten temat, przejdź do wpisu: Command vs Event.

4. Saga Pattern orkiestracja vs choreografia

W kontekście architektury rozproszonej najczęściej spotkamy się z sytuacją, gdzie mikroserwisy „komunikują się” ze sobą za pośrednictwem brokera wiadomości (np. Apache Kafka lub RabbitMQ). W konsekwencji przepływ procesów realizowanych przez Sage odbywa się również z wykorzystaniem message brokera.

Istnieją dwa typowe podejścia implementacji sagi, orkiestracja i choreografia:

Orkiestracja — to sposób, w którym istnieje jeden centralny serwis, zwany orkiestratorem, zarządza i kontroluje wykonanie całego procesu. Orkiestrator jest odpowiedzialny za decydowanie, które serwisy powinny być wywołane i kiedy. Wywołuje on konkretne operacje w odpowiednich serwisach i oczekuje na ich odpowiedzi. W przypadku wystąpienia błędu orkiestrator jest odpowiedzialny za wywołanie odpowiednich operacji kompensacyjnych. Może wykorzystać komendy i zdarzenia.

Choreografia — to sposób, w którym nie ma scentralizowanego punktu kontrolnego. W tym przypadku każdy serwis zna swój krok w procesie i wie, kiedy powinien działać. Serwisy komunikują się ze sobą poprzez wysyłanie i odbieranie zdarzeń tzn. każdy mikroserwis może publikować zdarzenia, które będą odczytywane przez kilka innych usług. Korzysta tylko ze zdarzeń.

Orkiestracja Choreografia
Zalety• Dobre dla złożonych przepływów.
• Łatwość kontrolowania każdego podprocesu.Nie wprowadza cyklicznych zależności, ponieważ orkiestrator wydaje rozkazy.
• Usługi nie muszą znać poleceń dedykowanych dla innych usług, co upraszcza logikę biznesową.
• Łatwiejsza wizualizacja i monitorowanie procesu.
• Łatwiejsza kompensacja.
• Dobry dla prostych przepływów niepotrzebujących logiki koordynacji.
• Nie wymaga dodatkowej usługi orkiestratora.
• Nie wprowadza jednego punktu awarii, ponieważ obowiązki są rozłożone na uczestników sagi.
• Lepsza wydajność.
Wady• Dodatkowa złożoność projektowania wymaga implementacji orkiestratora.
• Single point of failure, jeśli orkiestrator padnie saga przestaje działać.
• Trudne w utrzymaniu i rozwijaniu, dodawanie nowych kroku znacznie zwiększa złożoność procesu.
• Brak jednego źródła prawdy.
• Istnieje ryzyko cyklicznej zależności między uczestnikami sagi, ponieważ muszą oni korzystać ze wzajemnych poleceń.
• Testowanie integracji jest trudne, ponieważ jedno zdarzenie może być czytane przez kilka usług.
• Trudy proces kompensacji.

5. Saga Pattern na przykładzie aplikacja do zamawiania jedzenia

Za przykład posłuży nam prosta aplikacja do zamawiania jedzenia. Na końcu tego wpisu znajduje się link do repozytorium GitHub, gdzie zaimplementowałem Saga Pattern dla tej aplikacji. Zanim jednak przejdziesz do analizy kodu, przeczytaj poniższe sekcje, aby wiedzieć, które podejście zostało wybrane i dlaczego. Architektura omawianej aplikacji wygląda następująco:

Architektura aplikacji do zamawiania jedzenia

Logika biznesowa zamawiania posiłku (happy path):

  1. Złożenie zamówienia,
  2. Dokonanie płatności,
  3. Akceptacja zamówienia przez restauracje.

5.1 Przykładowa choreografia (happy path)

  1. Order service dostaje żądanie utworzenia zamówienia.
  2. Order service tworzy zamówienie i publikuje zdarzenie orderCreated.
  3. Payment service czytany eventorderCreated wykonuje operację płatności i publikuje zdarzenie paymentCompleted.
  4. Event paymentCompleted jest równolegle czytany przez:
    4.1 Order service, który zmienia statu zamówienia na PAID.
    4.2 Restaurant service, który akceptuje zamówienie i publikuje event orderAccepted.
  5. Order service czyta event orderAccepted i zmienia status zamówienia na APPROVED – Koniec Sagi.

5.2 Przykładowa orkiestracja (happy path)

  1. Order service dostaje żądanie utworzenia zamówienia.
  2. Order service tworzy zamówienie i publikuje zdarzenie orderCreated.
  3. Orchestrator service czyta orderCreated i publikuje komendę executePayment.
  4. Payment service czyta komendę executePayment, wykonuje operację płatności i publikuje zdarzenie paymentCompleted.
  5. Orchestrator service czyta zdarzenie paymentCompleted i publikuje komendę setOrderStatusToPaid.
  6. Order service czyta komendę setOrderStatusToPaid, ustawia status zamówienia na PAID i publikuje zdarzenie orderPaid.
  7. Orchestrator service czyta zdarzenie orderPaid i publikuje komendę acceptOrder.
  8. Restaurant service czyta komendę acceptOrder, akceptuje zamówienie i publikuje zdarzenie orderAccepted.
  9. Orchestrator service czyta zdarzenie orderAccepted i publikuje komendę setOrderStatusToApproved.
  10. Order service czyta komendę setOrderStatusToApproved, zmienia status zamówienia na APPROVED i publikuje zdarzenie orderApproved.
  11. Orchestrator service czyta zdarzenie orderApproved i kończy Sage.

5.3 Mix orkiestracja i choreografia (happy path)

Plusy choreografii to mniej przepływu wiadomości oraz korzystanie tylko z eventów. Dzięki temu, gdy pojawi się kolejny podproces, np. wysłanie emaila przez Notification Service po zaakceptowaniu zamówienia przez restaurację, wystarczy dokonać zmian w jednym miejscu – dodać Notification Service.

Orkiestracja jest bardziej złożona i każdy nowy podproces wymaga implementacji co najmniej w dwóch miejscach, czyli wysłania komendy sendEmail w Orchestrator service i jej obsługi w Notification service.
Jednakże orkiestracja jest o wiele prostsza w utrzymaniu i monitorowaniu, a przede wszystkim, w przypadku gdy trzeba wykonać jakąś kompensację.

Jak to bywa w IT, wzorzec dostosowujemy do problemu. Dlatego też nic nie stoi na przeszkodzie, aby łączyć podejścia choreografii i orkiestracji.

W poniższym przykładzie, do którego kodzik znajdziecie na Git, wykorzystuję tylko eventy (tak jak w choreografii) i jednocześnie przenoszę koordynację procesu składania zamówienia do serwisu Order.
W efekcie pozostaje pełną kontrolę nad każdym z podprocesów, ale jednocześnie cały proces się skraca i upraszcza.

  1. Order service dostaje żądanie utworzenia zamówienia.
  2. Order service tworzy zamówienie i publikuje zdarzenie orderCreated.
  3. Payment service czytany eventorderCreated wykonuje operację płatności i publikuje zdarzenie paymentCompleted.
  4. Order service czyta zdarzenie paymentCompleted, zmienia statu zamówienia na PAID i publikuje event orderPaid.
  5. Restaurant service czyta zdarzenie orderPaid, akceptuje zamówienie i publikuje zdarzenie orderAccepted.
  6. Order service czyta event orderAccepted, ustawia status zamówienia na APPROVED i kończy Sage.

5.4 Mix orkiestracja i choreografia (kompensacja)

Jak wspomniałem na początku, moc wzorca sagi tkwi w możliwości kompensacji procesu.
Nie każdy podproces można cofnąć zmieniając dane w bazie. Na przykład, jeśli wyślemy email, to już tego nie cofniemy.
Możemy jednak w zamian za to podjąć jakąś korekcyjną akcję biznesową, np. wysłać kolejny email: „Niestety musieliśmy anulować zamówienie, ale za to oferujemy Ci 10% rabatu na następne..”.
W większości przypadków jednak będziemy mogli podjąć akcję kompensacyjną, wywołując kolejne transakcje na bazach danych, których zadaniem będzie przywrócenie poprzedniego stanu.

W naszym przypadku może się zdarzyć, że płatność nie dojdzie do skutku. Wtedy będzie trzeba wywołać jakiś event, np. paymentRejected, przechwycić go w Order Service i zmienić status zamówienia. Ten scenariusz jest dość prosty. Weźmy więc na tapet coś trudniejszego. Czyli sytuację, w której zamówienie zostaje utworzone, płatność przechodzi, ale restauracja odrzuca zamówienie, bo np. chłop zamówił kaszankę, a okazało się, że kaszanka już się skończyła, bo ktoś kupił ostatnią kiche lokalnie (łakomczuszek). W takim przypadku proces kompensacji może wyglądać następująco:

  1. Order service dostaje żądanie utworzenia zamówienia.
  2. Order service tworzy zamówienie i publikuje zdarzenie orderCreated.
  3. Payment service czytany eventorderCreated wykonuje operację płatności i publikuje zdarzenie paymentCompleted.
  4. Order service czyta zdarzenie paymentCompleted, zmienia statu zamówienia na PAID i publikuje event orderPaid.
  5. Restaurant service czyta zdarzenie orderPaid, ale okazuje się, że nie ma tej kaszanki, dlatego odrzuca zamówienie i publikuje zdarzenie orderRejected.
  6. Order service czyta event orderRejected i rozpoczyna proces kompensacji. Ustawia status zamówienia na CANCELLINGi publikuje zdarzenie orderCancelling.
  7. Payment service czyta event orderCancelling, cofa płatność i publikuje event paymentCancelled.
  8. Order service czyta event paymentCancelled, ustawia status zamówienia na CANCELLED i kończy Sage.

6. Co jeśli proces kompensacji się nie powiedzie!?

Oczywiście, ktoś mógłby zapytać: „A co, jeśli proces kompensacji się nie powiedzie?! Co wtedy!?”. Wtedy jak to w programowaniu „to zależy”. Nie ma jednej złotej zasady, wszystko zależy od skali takich przypadków. Może warto zastosować scheduler, który co jakiś czas sprawdzi Sagi, które nie są w statusie ostatecznym po upływie określonego czasu. Albo stworzyć osobny topic na Kafce dla takich sytuacji.

A co zrobić z takimi Sagami? Znowu, to zależy od ilości takich przypadków. Może nie ma sensu przepalać man days na tworzenie procesu kompensacji do kompensacji. Zamiast tego, na koniec dnia skuteczniejszy może okazać się mechanizm białkowy. Czyli np. Pani Halinka zadzwoni do tych trzech klientów, dla których wystąpił taki przypadek, i zrobi kompensację „z palca” za pomocą swojego pracowniczego portalu.

7. Sprostowanie — terminologia

Saga Pattern, jak wiele innych kwestii w świecie IT, jest często przedmiotem debat. Chciałbym podkreślić, że nie staję po żadnej ze stron i unikam dyskusji na temat tego, czy choreografię możemy nazwać Sagą, czy tylko orkiestrację. A kiedy orkiestrację modeluje się jako maszynę stanów, to czy nadal jest to Saga, bo przecież Saga nie ma stanu, więc to musi być wzorzec Menedżera Procesów (Process Manager Pattern) – i tak dalej…

Uważam, że nie ma sensu spierać się o to, jak nazwiemy daną technikę — czy to będzie Saga Pattern, Process Manager Pattern, czy choreografia lub jeszcze inaczej. Twórca wzorca Saga stworzył jego podstawy w 1987 roku. Zapewne nie przewidywał, jak będziemy z niego korzystać dzisiaj. Na koniec dnia najważniejsze jest, aby wiedzieć, jak radzić sobie w przypadku rozproszonego procesu biznesowego, realizowanego przez wiele usług z różnymi bazami danych, a nie jak to nazwać 🙂

8. Linki

Poniżej znajduje się link do projektu, w którym między innymi zaimplementowałem omawiany wzorzec Sagi.
A jeśli chcesz sprawdzić swoje kompetencje jako Senior Java Developer, zapraszam Cię do mojego quizu — spoiler: będą tam pytania dotyczące wzorca Sagi 🫖. Jeśli wpis Ci się podobał, daj mi znać w komentarzu i zostaw swój email, aby być na bieżąco. Twój feedback będzie dla mnie motywacją do dalszego blogowania 💪

Bądź na bieżąco!

Zostaw swój e-mail, a raz na jakiś czas wyśle Ci info o nowych postach na blogu. Tylko wartościowe e-mail!

Logo github


Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *

Zacznij wpisywać wyszukiwane hasło powyżej i naciśnij Enter, aby wyszukać. Naciśnij ESC, aby anulować.

Do góry