Problem z którym się ostatnio spotkałem jest prozaiczny. Aby lepiej go zobrazować, posłużę się analogicznym przykładem do systemu nad którym pracuję.

  • System pobiera pieniądze od klientów za wykonane usługi.
  • Informacja na temat zlecenia pobrania opłaty przychodzi z innej części platformy.
  • Komunikacja w całym procesie przebiega asynchroniczne. Wiadomości publikowane są na szynę komunikacyjną.

Gdy wszystko przebiega pomyślnie, cały proces wygląda mniej więcej tak.

async-communication-happy-path

Awarie zdarzają się rzadko, dlatego przez większość czasu wszystko będzie odbywa się zgodnie z planem. Aż pewnego razu coś wybuchnie i będzie to akurat piątkowe popołudnie.

Które elementy procesu mogą powodować problemy?

Niestety wszystkie.

Zacznijmy od zapoznania się z gwarancjami, które oferuje infrastruktura. Wyróżniamy dwa tryby działania:

  • Gwarancja dostarczenia najwyżej raz - broker wysyła wiadomość do odbiorcy raz i nie czeka na potwierdzenie odebrania wiadomości
  • Gwarancja dostarczenia przynajmniej raz - broker wysyła wiadomość i czeka od potwierdzenie odbioru przez określony czas. Gdy ACK nie przychodzi, wysyła wiadomość ponownie do momentu otrzymania potwierdzenia.

A gwarancja dostarczenia dokładnie raz? Otóż nikt takiej gwarancji nie daje. Nie jest to bynajmniej zła wola producentów oprogramowania. Zapewnienie takiej gwarancji jest po prostu trudne o ile w ogóle możliwe. Wiele rzeczy może się po drodze wysypać. Zaczynając od oczywistych problemów jak bugi, crashe aplikacji klienckich, po mniej oczywiste problemy z warstwą fizyczną (warto pamiętać, że sieć jest zawodna, niezależnie od liczby abstrakcji po drodze) czy awarie hardware’u.

Przyjrzyjmy się ścieżkom negatywnym:

Przypadek 1. “At-most once delivery”.

Wykorzystując tę gwarancję, narażamy się na sytuację w której wiadomość wysłana przez brokera nigdy nie trafi do odbiorcy. Pakiet zgubił się po drodze (przypomnę jeszcze raz, że sieć jest zawodna), broker padł albo usługa. Pewnie znalazłoby się coś jeszcze. Nie wiemy jednak, że sytuacja ta wydarzyła się po drodze bo nie czekamy na potwierdzenie otrzymania wiadomości.

failing-at-most-once

Przypadek 2. “At-least once delivery”

W odróżnieniu do poprzedniego trybu działania, kluczowe jest tu otrzymanie potwierdzenia. Potwierdzenia, które może się zgubić podczas gdy wiadomość została przetworzona.

failing-at-least-once

Opcje są dwie. Albo akceptujemy możliwość, że coś po drodze zgubimy albo zgadzamy się na duplikaty. Tylko, że biznes raczej nie pójdzie na żaden z tych kompromisów. Tym bardziej gdy w gre wchodzą pieniądze. Jest jeszcze trzecia opcja. Nie jest dostępna z pudełka i wymaga dobrze przemyślanego procesu lub dodatkowego kawałka kodu, ale pozwala rozwiązać ten problem.

Gwarancja jednokrotnego przetworzenia wiadomości.

Patrząc z dalszej perspektywy łatwo zauważyć, że w gruncie rzeczy nie chodzi o to aby wiadomość została dostarczona dokładnie raz. Istotne jest natomiast, aby wiadomość została raz przetworzona, lub by wielokrotne przetworzenie wiadomości nie naruszyło integralności danych. Przeniesienie problemu w inne miejsce otwiera drzwi na nowe możliwości.

Jak do tego podejść?

Pierwszym rozwiązaniem są idempotentni konsumerzy. Dopóki wynik operacji się nie zmieni, obserwatora nie interesuje ile razy wiadomość została przetworzona. Raz czy dziesięć, nie ma to zupełnie znaczenia. Jedyny problem jest taki, że nie wszędzie da się to zaimplementować. Bądź da się, ale jest to niesamowicie kosztowne. Każdy przypadek będzie wymagał innego podejścia i nie ma tu złotej rady. Najlepiej pogadać z biznesem i poszukać rozwiązania z ekspertami domenowymi.

Drugim, bardziej uniwersalnym sposobem, który niestety wymaga dodatkowego kawałka kodu, będzie idempotentny procesor wiadomości. Jego odpowiedzialnością będzie rejestrowanie wszystkich wiadomości, które trafiają do niego i pomijanie tych, które są duplikatami. Quick win? I tak i nie. Chociaż sam schemat działania jest prosty, implementacja może okazać się trochę bardziej skomplikowana. Istotne są szczegóły.

Transakcyjność procesu nadawania i przetwarzania.

Spójrzmy na sytuację, która może się zdarzyć przy nadawaniu:

published-uncommited-msg-outbox

Występuje tu problemem opublikowania wiadomości, która nigdy nie powinna zostać wysłana. Producent po opublikowaniu wiadomości zakończył pracę z błędem. Stan nie został zapisany a mimo tego, inne aplikacje zostały poinformowane o zdarzeniu, które de facto się nie wydarzyło. Klient został obciążony za zakup, którego nie “dokonał”. Aby uchronić się przed tego typu sytuacją, proces publikacji powinien odbyć się dopiero po zapisaniu stanu producenta.

Spróbujmy więc inaczej.

lost-msg-outbox

Kolejne problemy. Tym razem klient dokonał zakupu, za który nie zapłacił. Broker odebrał wiadomość ale nie przekazał jej dalej.

Proces odbiorczy

Uznajmy jednak, udało się nadać wiadomość bez błędów. Jakie problemy czekają po stronie odbiorczej?

lost-msg-inbox

Potwierdzając odbiór wiadomości jeszcze przed jej przetworzeniem, narażamy się na sytuację w której odbiorca nie przetworzy wiadomości. Klient nie płaci za zamówienie. Przenieśmy więc wysłanie potwierdzenia na koniec.

duplicated-msg-inbox

Tym razem broker wiadomości nie otrzymuje potwierdzenia i robi retransmisję. Klient płaci podwójnie.

Nie jest kolorowo. Ewidentnie potrzeba tu transakcyjności pomiędzy systemami rozproszonymi. Transackcyjności, której nie mamy i raczej mieć nie będziemy. Można natomiast zaimplementować coś na kształt. Coś co będzie się zachowywać bardzo podobnie.

Outbox + Inbox

A co gdyby rozdzielić nadawanie/obieranie wiadomości od jej produkowania/konsumpcji i wykonać te operacje w dwóch oddzielnych transakcjach. Jeżeli coś się wysypie to tylko w jednej części, którą będzie można ponowić.

Problem ten adresują dwa wzorce - Transactional Outbox/Inbox.

Jak to zaimplementować? O tym w następnych postach.

Bibliografia:
[1] Chris Richardson, [artykuł] Pattern: Transactional Outbox. Źródło: https://microservices.io/patterns/data/transactional-outbox.html
[2] Oskar Dudycz, [artykuł] Outbox, Inbox patterns and delivery guarantees explained. Źródło: https://event-driven.io/en/outbox_inbox_patterns_and_delivery_guarantees_explained/

Zostaw komentarz