Życie w chmurze, czyli jazda bez transakcji

3

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!

Share.

About Author

Microsoft MVP, współzałożyciel DotNetos, architekt, speaker, inżynier oprogramowania oraz lider Warszawskiej Grupy .NET. Z zamiłowaniem łączy architekturę i oprogramowanie wysokiej wydajności. Chętnie dzieli się szeroką wiedzą na temat współczesnych architektur, systemów rozproszonych oraz zasad rządzących niskopoziomowym światem niezwykle wydajnych aplikacji. Nigdy nie alokuje przed 12.

3 Comments

  1. Piotr Czech on

    Jak to wygląda ze skalowalnością takiego rozwiązania w Durable Task ?

    Ja osobiście zauważyłem, że jak implementowałem rozwiązanie dla urządzeń IoT, Azure Functions był w stanie przetworzyć dane z IoT Hub, jednak system od Event Hub podczas kolejkowania od Azure Functions już nie.

    Event Hub’y posiadają możliwość partycjonowania wiadomości na wiele kanałów, więc razem z Azure SB Lite byłem w stanie przetworzyć te wszystkie dane bez zapychania jednej kolejki.

    Jestem ciekaw czy Durable Task jest w stanie sam się wyskalować w zależności od ilości dany, które do niego przypłyną czy trzeba bawić się i tworzyć np. maszyny wirtualne ? Z innej strony “nagrywanie zmian” ma jakieś ukryte wady, np. duży narzut na pamięć?

Leave A Reply