Architektury nowoczesnych aplikacji opanowały trendy i marketing do tego stopnia, że konferencje czy meetupy techniczne często zdają się przypominać pokazy mody.
Niezależnie od tego, czym i w jakiej skali się zajmujemy, inspirują nas „wielcy projektanci” – ogromne firmy o międzynarodowym zasięgu, które każdego dnia musza się mierzyć z milionami żądnych ich usług użytkowników. Takie rozwiązania z góry skazane są na architektury rozproszone (jak np. mikrousługi), ze względu przede wszystkim na możliwość elastycznego skalowania. Ich prezentacje pełne są potknięć i porażek, stopniowo przekuwanych w spektakularne sukcesy.
W naszej branży, specjaliści chcą wykorzystywać na co dzień najnowsze technologie, oferować swoim klientom nowoczesne rozwiązania, a klienci zamawiać systemy, które podążają z duchem czasu – to zrozumiałe. Na nasze wspólne nieszczęście, nie zawsze pamiętamy, że wybór architektury niesie za sobą konsekwencje, a tworzenie systemów rozproszonych, nawet w 2019 roku, wciąż jest trudne. Dlatego, na tegorocznej konferencji Pozitive technologies postanowiłem opowiedzieć o tej drugiej, nieco przemilczanej stronie takich architektur. Zapominając na chwilę o ich popularności i niewątpliwych zaletach, wyłania się bowiem rzeczywistość bardziej przypominająca pełną zagrożeń dżunglę, niż rajski ogród. Ten wpis będzie nie tyle kompletnym kursem przetrwania, co ostrzeżeniem o tym, co czyha na nas w rozproszonym świecie – zanim na YouTube pojawi się nagranie z mojego wystąpienia.
Przepraszam, coś mnie rozproszyło…
Z praktycznego punktu widzenia, definicja systemu rozproszonego jest prosta – to zbiór niezależnych komponentów (usług, modułów, funkcji, itd.), które mogą znajdować się na zupełnie innych maszynach, komunikujących się ze sobą poprzez sieć. W teorii, awaria jednego z nich nie wpływa dzięki temu na pozostałe, a każdy z komponentów może być skalowany niezależnie (poprzez zwiększanie liczby jego instancji). Po drugiej stronie, mamy „klasyczne” podejście monolityczne – aplikację, która stanowi jedną całość, która w przypadku awarii jest całkowicie niedostępna i uruchamiana jest na pojedynczej maszynie. A skaluje się, delikatnie mówiąc, dość niekorzystnie. W takim zestawieniu, może się wydawać, że monolity nie mają żadnych szans w starciu z nowoczesnością…
Problemy rozproszonego świata to jednak często kwestie, które przy pracy nad monolitami nie przyszłyby nam nawet do głowy. Przykładowo, jeśli jedna część monolitycznej aplikacji musi skorzystać z funkcji oferowanej przez drugą, to takie wywołanie może się zero-jedynkowo udać w całości lub nie, a czas potrzebny na samą komunikację jest właściwie nieistotny z wydajnościowego punktu widzenia. W świecie rozproszonym, gdy usługa/funkcja (zależnie od implementacji) wywołuje inną, sytuacja drastycznie się komplikuje, a czas wywołania odgrywa istotną rolę. Wszystko to przez jeden, bagatelizowany szczegół – komunikacja odbywa się poprzez sieć, a ta jest zaskakująco zawodna. W przypadku wywołań sieciowych właściwie możemy zapomnieć o zero-jedynkowości, a prawo Murphy’ego staje się naszą codziennością.
Rozważmy następujący przykład komunikacji synchronicznej:
Nawet w tak prostej sytuacji:
- B może przyjąć żądanie, przetworzyć je, ale nie uda się odesłać odpowiedzi (A wie jedynie, że „coś poszło nie tak”)
- A może czekać na przetworzenie żądania tak długo, aż uzna je za zakończone niepowodzeniem (gdy B chwilę później zacznie je jednak przetwarzać)
- A tuż po wysłaniu żądania (ale przed otrzymaniem odpowiedzi) straci komunikację z siecią (nigdy nie dowie się, czy B odpowiedział).
Skoro tego rodzaju problemy występują tak często, pojawia się pytanie: czy w takim razie wszystkie systemy tego typu nie powinny być na to przygotowane? Odpowiedź jest prosta – tak, powinny. Niestety doświadczenie pokazuje, że zawodność sieci jest najczęściej ignorowanym problemem w tego typu rozwiązaniach. Przynajmniej do czasu, gdy takie systemy zostają wreszcie uruchomione produkcyjnie i udostępnione prawdziwym użytkownikom…
A co w sytuacji, gdy wywołanie B zmienia stan systemu – np. poprzez aktualizację jakiejś bazy danych?
Spójność danych
Lata pracy z aplikacjami monolitycznymi, w których korzystamy na dodatek głównie tylko z jednego źródła (bazy) danych, przyzwyczaiły nas do tego, że semantykę transakcyjności – rozumianą jako wykonanie „wszystko albo nic” – dostajemy niemal za darmo. Jeśli więc nasz proces biznesowy składa się z wielu kroków zmieniających stan (np. zaksięgowanie płatności, stworzenie zamówienia, wystawienie faktury, itp.) to w przypadku błędu na którymkolwiek etapie oczekujemy, że anulowane zostaną wszystkie z nich. W ten sposób osiągamy spójność danych rozumianą z perspektywy biznesowej poprawności.
Własności te, z jakiegoś powodu, mylnie przyjęło się łączyć jedynie z relacyjnymi bazami danych (ACID), a wiele osób do dziś zawzięcie wierzy, że ich transakcyjność jest swego rodzaju „panaceum na błędy”, bez którego nie może się obyć żaden „poważny” system klasy enterprise. Niestety, nawet ona stała się, moim zdaniem, ofiarą swojego sukcesu i marketingu – notorycznie jest nadużywana i stosowana dość lekkomyślnie. Zainteresowanych tematem (lub linczem na mojej osobie, za tak nonszalanckie zdeptanie bazodanowych świętości ?) odsyłam do świetnej prezentacji Martina Kleppmanna.
W świecie rozproszonym, problem semantyki transakcyjności jesteśmy zmuszeni rozwiązać na nowo. Jednym z najpopularniejszych na to sposobów jest zastosowanie wzorca Saga. W zależności od wybranego wariantu:
- implementujemy „zarządcę” procesu, który jest odpowiedzialny za nadzorowanie jego przebiegu i wykonywanie akcji kompensacyjnych lub ponawiania w przypadku błędu (orchestration)
- implementujemy proces jako sekwencję asynchronicznych zdarzeń (events), gdzie każda akcja może zakończyć się sukcesem lub niepowodzeniem, a elementy naszego systemu nasłuchują na informacje o błędach wykonując w reakcji na nie działania kompensacyjne (choreography).
Poprawne i efektywne implementowanie transakcyjności w rozproszonym świecie to jednak dość obszerny temat, który wykracza poza zakres tego wpisu.
Jak znikają pieniądze?
Systemy rozproszone są też nieporównywalnie bardziej skomplikowane pod względem śledzenia procesów przez nie wykonywanych. Nawet jeśli poszczególne elementy systemu (usługi/funkcje) są szczegółowo monitorowane i skrupulatnie dokumentują wszystkie zdarzenia w swoich logach, to nie gwarantuje to w żaden sposób przejrzystości wykonań na poziomie procesów biznesowych, które przechodzą teraz przez wiele modułów. Jak to możliwe? Okazuje się, że przestępcy tę przypadłość zrozumieli i wykorzystali już jakiś czas temu. Uzyskując nieautoryzowany dostęp do czyjejś bankowości internetowej muszą w jakiś sposób przelać środki na własne rachunki – i to najlepiej tak, by nie dać się złapać. Pojedynczy przelew (do jednego banku) byłby zbyt prosty do namierzenia, dlatego całość kwoty dzielona jest na wiele małych kaskadowych transakcji przechodzących przez różne banki i konta (najczęściej również przejęte lub założone na tzw. „słupów”). Tak więc choć systemy bankowe zobligowane są do dokładnego monitorowania wszystkich transakcji, to analiza działań obejmujących wiele z nich jednocześnie jest czasochłonna i trudna. W rozproszonych aplikacjach, bez odpowiedniego monitoringu systemu, szukanie błędów przypomina taką pracę śledczych – zarówno ze względu na poziom skomplikowania jak i presję czasową.
Podsumowanie
Świat systemów rozproszonych oferuje nam ogromne możliwości jednocześnie zmuszając do radzenia sobie z wieloma, często trudnymi do rozwiązania problemami. Czy to znaczy, że nie powinniśmy budować tego rodzaju aplikacji? Zdecydowanie nie! Ważne jest jednak, by mieć świadomość z jakimi zagrożeniami przyjdzie się mierzyć oraz posiadać w swoim arsenale narzędzia i sposoby na ich neutralizowanie. Tak jak w przypadku większości wzorców czy technologii, dokładne ich zrozumienie jest kluczowe dla efektywności oprogramowania, które wytwarzamy. I to nawet wtedy, gdy słyszmy od innych głównie o korzyściach płynących z ich wykorzystywania.
Zostaw komentarz