Przejdź do treści

DevStyle - Strona Główna
Życie w chmurze, czyli jazda bez transakcji

Życie w chmurze, czyli jazda bez transakcji

Szymon Kulec

16 kwietnia 2018

Chmura

Tagi:

Jak żyć bez transakcji? To pytanie stawia sobie każdy adept chmury, który postanawia użyć kilku z jej usług.

Czasami, nawet w obrębie jednego serwisu, możemy napotkać na ograniczenia, które nie pozwolą na stary, dobry “transakcyjny zapis”. Co robić jeśli coś się nie uda? Co zrobić aby się udało? Zapraszam do artykułu.

There can be only one

Jeżeli znasz Nieśmiertelnego to pamiętasz, że może być tylko jeden. Niestety, jeżeli kiedykolwiek użyłeś jakiejś usługi kolejkowej, to wiesz, że otrzymanie i utrzymanie gwarancji dostarczenia każdej wiadomości dokładnie raz (ang. exactly-once delivery) jest po prostu niemożliwe.

O co chodzi z tymi gwarancjami? Spójrzmy na następujący scenariusz. Używamy serwisu kolejek, pozwalającego na pobranie wiadomości (odpytanie się o nowe) oraz o potwierdzenie tych już przetworzonych. Zastanówmy się co się stanie, jeżeli nasz kod napisalibyśmy w następujący sposób:

var msg = queue.GetMessage();
ExecuteOperation(msg);
queue.Acknowledge(msg.Data);

Operacje wykonywane kolejno: pobranie wiadomości, przetworzenie wiadomości, potwierdzenie wiadomości. Zastanówmy się, co się stanie, jeżeli z jakiegoś powodu aplikacja przestanie działać po wykonaniu operacji, a przed potwierdzeniem wiadomości? W większości systemów wiadomość wróci do kolejki po jakimś czasie – usługa nie wie, czy ktoś ją przetworzył, czy nie. Oznacza to, że ta sama wiadomość trafi do nas po raz kolejny i przetworzymy ją po raz drugi. Jak widać powyższe ustrukturyzowanie kodu pozwala na pojawienie się tej samej wiadomości więcej niż raz (ang. at-least-once delivery).

Co stanie się, jeśli potwierdzenie przesuniemy linijkę wyżej, zmieniając kod na następujący?

var msg = queue.GetMessage();
queue.Acknowledge(msg);
ExecuteOperation(msg.Data);

Stosując dokładnie takie samo podejście, możemy zapytać o przypadek, w którym to zaraz po potwierdzeniu operacji, następuje jakiś krytyczny błąd i aplikacja zostaje zatrzymana. Co wówczas się dzieje z operacją biznesową, która miała być wykonana? Nie zostaje wykonana. Wiadomość, potwierdzona wcześniej, została usunięta z kolejki. Oznacza to, że istnieją sytuacje, w których wiadomość może być przetworzona najwyżej raz (ang. at-most-once).

To tylko kolejki

Biorąc powyższe przykłady, można by powiedzieć, że dotyczą one tylko serwisów kolejkowych, wiadomości. Jest to nieprawda. Ponieważ żadne dwie linijki naszego kodu nie są spięte transakcją, efektywnie pomiędzy każdym wywołaniem serwisów może nastąpić wyjątek, przerywając operację biznesową w połowie. To nie wina kolejek. Po prostu: przy braku wsparcia dla transakcji, należy świadomie projektować systemy tak, aby odporne były na (potencjalnie) kilkukrotne wykonanie tej samej operacji. Jak to zrobić?

Czy jesteś idempotentny?

Idempotencja to nie choroba, a cecha, która pozwala zaaplikować tę samą operację kilka razy, bez zmiany jej wyniku. Jeżeli chcielibyśmy opisać to w sposób matematyczny, to wyglądałoby to następująco:

f(f(x)) = f(x)

W jaki sposób zastosować to podejście w naszym kodzie? Zacznijmy od przykładu jak mogłoby to wyglądać, który będzie zmodyfikowanym przykładem at-least-once

var msg = queue.GetMessage();
ExecuteOperation(msg.Data, msg.Id);
queue.Acknowledge(msg);

Dodatkowym argumentem przekazywanym do funkcji ExecuteOperation jest identyfikator wiadomości (większość systemów kolejkowych dostarcza taki identyfikator; jeżeli konkretna usługa by go nie podawała, wówczas nadawca może to zrobić, dodając takie pole). Identyfikator przekazany zostaje do wywołania operacji biznesowej. Na jej poziomie może zostać użyty do sprawdzenia, czy dane związane z tą konkretną operacją nie zostały już zapisane/przetworzone.

Ostatecznie, aby umożliwić takie sprawdzenie, razem z danymi biznesowymi należy zapisać identyfikator operacji. W ten sposób, kolejne wykonania, o ile nastąpią, będą mogły w prosty sposób sprawdzić, czy coś się udało.

REST i inne serwisy

Nic nie stoi na przeszkodzie, aby podczas aplikowania tego wzorca w Waszych rozwiązaniach nie pozwolić mu wyciec do warstwy API jako, np. nagłówek. Wówczas wołający Wasze serwisy, mogliby zapewnić idempotentność wywołań podając swoje identifkatory.

Jak widać, wchodząc raz do świata idempotencji warto opublikować go na zewnątrz, tak, aby i inni mogli zapewnić wykonanie operacji biznesowych dokładnie raz.

Azure Functions i Durable Task

Zbliżone podejście do idempotentnego odbiorcy zostało zaimplementowane w Durable Task: frameworku pozwalającym opisywać procesy wykonywane jako Azure Functions. Pod kodem bazującym na składni async/await umieszczono mechanizm nagrywania zmian wykonanych przez proces. W ten sposób nawet w przypadku wystąpienia wyjątku w połowie, Durable Task automatycznie będzie w stanie wznowić działanie od ostatnio zapisanej operacji, nie wykonując już tych wcześniejszych.

Kawałek po kawałku

Stwierdzenie, że “po prostu przeniosę się do chmury”, o ile jest popularne, to uznałbym je za nie zawsze prawdziwe.

Jeżeli Twój system działał dotychczas z jedną bazą zapewniającą transakcje, a teraz chcesz przenieść go do chmury, zwiększając zestaw jego fundamentów, to musisz rozważyć brak transakcji. Jak zaadresujesz potencjalne błędy pojawiające się w trakcie wykonania?

Jak najbardziej można i należy robić to kawałek po kawałku. Ważne elementy systemu przepisane w sposób zapewniający 100% gwarancji poprawnego wykonania będą mogły być wołane nawet kilka razy, ostatecznie pozwalając na wytworzenie rozwiązania prawdziwie gotowego na jazdę bez transakcji.

Witamy w świecie chmury!

Zobacz również