Ewolucja w projektowaniu i wytwarzaniu aplikacji internetowych przeszła przez kilka etapów. W latach dziewięćdziesiątych ubiegłego wieku mieliśmy „Spaghetti-oriented Architecture”, która charakteryzowała się długimi jak makaron metodami, pełnymi ifów i zależności. Można było znaleźć dużo powtórzeń tego samego kodu, a programiści mogli się wtedy szczycić podejściem: trudno było napisać, trudno będzie zrozumieć.
Maciej Rynkiewicz. Software Development Manager w Wakacje.pl. Inżynier z zamiłowaniem do pracy twórczej, programista, architekt systemów i lider. Potrafi zrozumieć potrzeby biznesu i znaleźć rozwiązanie dobre dla wszystkich. Zawsze stawia na zespół, w którym pracuje i transparentną relację. Ma kilkanaście lat doświadczenia w branży IT, w której zaliczył niejedną porażkę, ale odniósł też wiele sukcesów.
Następnie przyszły lata dwutysięczne, które przyniosły rewolucję pt.: „Lasagne-oriented Architecture”. Prawdziwy przewrót: mamy aplikację podzieloną na warstwy, które świetnie wydzielają obszary działania, czasy świetności MVC. Wspaniałe rozwiązanie do czasu gdy aplikacje tak urosły, że utrzymanie oraz ich rozwój jest trudny i czasochłonny. Na szczęście kilka lat później pojawiły się mikroserwisy, które miały być lekiem na całe zło. I faktycznie były, ale żeby dawać wartość biznesową muszą działać razem, czyli przede wszystkim komunikować się.
Komunikacja w sieci jednak niesie za sobą dużo potencjalnych ryzyk i problemów, bo co jeśli Curl nie nawiąże połączenia, albo aplikacja odpowie błędem 500? Jak to dobrze zaplanować, żeby potem nie mieć problemów?
System rozproszony bez względu na złożoność i wartość biznesową jaką przynosi może mieć podobne problemy, z którymi każdy programista musi umieć sobie poradzić. Podstawa to komunikacja, nie jest odkryciem, że serwery, które ze sobą współpracują muszą „widzieć się” i być w stanie przesłać odpowiednią ilość danych, jak i utrzymywać czasem długie połączenia. Następnie aplikacje muszą „dogadać się”, czyli mieć możliwość połączyć się ze sobą za pomocą protokołu sieciowego i rozumieć, co jedna „mówi” a czego druga „oczekuje”.
Nieoceniona staje się w tym miejscu dokumentacja techniczna, a także kluczowy fakt: czy API posiada wersjonowanie i czy możliwe jest zawarcie kontraktu pomiędzy właścicielami technicznymi obu serwisów. Na koniec zostają te najtrudniejsze obszary, czyli niespodziewane błędy aplikacji, które często są błędami w kodzie lub nieprzewidzianymi przypadkami działania. Nie można zapominać także o błędach w działaniu, które wynikają z flow biznesowego lub zależności od zewnętrznego zasobu np. kolejnego API.
Skoro już znamy problemy jakie mogą nas spotkać porozmawiajmy o rozwiązaniach. Chciałbym wyróżnić trzy najpopularniejsze:
1. Połączenie synchroniczne – przeważnie CURL – najbardziej popularne i często wystarczające rozwiązanie. Warto używać do komunikacji w miejscach, gdzie przesyłamy małą ilość danych, a także API, do którego łączymy się odpowiada szybko i jest stabilne.
Plusy:
- Proste rozwiązanie, nie wymaga dodatkowej infrastruktury, nie jest skomplikowane,
- Szybkie w implementacji,
- Łatwe w debugowaniu błędów.
Minusy:
- Ograniczony czas połączenia (zgodny z konfiguracją klienta i serwera),
- Blokuje aplikacje na czas wykonania całej operacji.
2. Komunikacja za pośrednictwem systemu kolejkowania – bardziej skomplikowane rozwiązanie, które wymaga od nas znajomości dodatkowych rozwiązań (np. Kafka, AMQP), do którego odkładamy żądanie obsłużenia dalszej komunikacji identycznej, jak w pkt. 1. Po wykonaniu zadania, skrypt może poinformować aplikację o wykonaniu zadania, jeśli jest taka potrzeba.
Plusy:
- Asynchroniczne rozwiązanie, które nie blokuje aplikacji,
- Umożliwia implementację bardziej skomplikowanych rozwiązań bez wpływu na aplikację, której używa użytkownik końcowy,
- Idealne rozwiązanie dla operacji, które nie wymagają potwierdzenia – można dzięki temu ustawić ponowienia lub odpowiednią reakcję na niepowodzenie,
- Dobre rozwiązanie dla komunikacji z niestabilnymi serwisami.
Minusy:
- Złożone technicznie rozwiązanie, które wymaga dodatkowej infrastruktury,
- Trudniejsze w naprawianiu błędów, debugowaniu i testowaniu.
3. Komunikacja na zasadzie call back’ów – rozwiązanie, które wymaga zaangażowania obu stron, ponieważ cała logika odpowiedzi, na którą nasza aplikacja musi być gotowa, leży po stronie serwera (w komunikacji klient-serwer). Polega to na wysłaniu żądania do serwera, który potwierdza przyjęcie żądania, następnie wykonuje zleconą operację i odpowiada na zdefiniowane API klienta zlecającego.
Dodatkowo konieczne jest ponawianie nieobsłużonych requestów przez serwer, co znacznie komplikuje całość.
Plusy:
- Rozwiązanie angażujące aplikację klienta do wykonania operacji w minimalnym stopniu,
- Działanie w pełni asynchroniczne,
- Proste rozwiązanie techniczne, w stosunku do możliwości jakie daje.
Minusy:
- Wymaga dodatkowej konfiguracji – adres na jaki mają wpadać odpowiedzi musi być przygotowany i stabilny,
- Po stronie serwera wymagana jest implementacja ponowień, jeśli serwis nie przyjmuje odpowiedzi.
Każdą funkcjonalność powinno się projektować indywidualnie, tak aby rozwiązywała problemy, a nie tworzyła nowych. Ważne jest też znalezienie złotego środka i nie popadanie w skrajności. Powyższe propozycje rozwiązań są w stanie obsłużyć prawie wszystkie znane mi przypadki, jednak warto dobrze się zastanowić, czy aby nie wyjeżdżamy z armatą na muchę. Wszystkie mają plusy i minusy, oraz każde z nich wymaga zbierania logów i ustawienia odpowiedniej konfiguracji niezbędnej do prawidłowego działania.
Jestem przekonany, że łatwo jest napisać dużo fajnego kodu, ale trudniej jest tworzyć rozwiązania łatwe w utrzymaniu i dostosowane do naszych potrzeb.
Grafika główna artykułu pochodzi od benorama.