Programowanie reaktywne to asynchroniczny paradygmat programowania polegający na przetwarzaniu strumieni danych i propagowaniu ich zmian. Podejście to nawiązuje do wzorca projektowego Observer i przyjmuje się koncepcja programowania reaktywnego wywodzi się właśnie z niego.
Dzięki takiemu rozwiązaniu możemy tworzyć wydajne i skalowane systemy – jednak jak to wygląda w praktyce?
Koncepcja i definicja programowania reaktywnego
Programowanie reaktywne (reactive programming) polega na asynchronicznym, nieblokującym przetwarzaniu danych.
Wykorzystując to podejście operujemy na zdarzeniach (events), będące przekazywane pomiędzy:
- Publisher – odpowiada za publikowanie
- Subscriber – nasłuchuje i odczytuje dane
Całość działa na zasadzie subskrypcji, gdzie Subscriber nasłuchuje zdarzeń wybranego Publisher’a. Jeśli porcja danych zostanie wyemitowana, to Subscriber od razu pozyska te dane.
Subscriber z wykorzystaniem zdarzeń, również może przekazywać informacje do Publishera. Interfejs wymiany informacji od Subscriber’a w stronę Publisher’a jest nazywany BackPressure. Dzięki niemu Publisher może otrzymywać feedback między innymi zawierający takie informacje jak:
- ograniczenie informacji otrzymywanych od Puhlisher’a, w przypadku kiedy Subscriber nie nadąża przetwarzać otrzymywanych paczek;
- błędów dotyczących przetwarzania danych, które mogą przerwać proces strumienia.
Należy pamiętać, że komunikacja od Publisher’a do Subscriber’a jest ciągła i trwa tak długo jak aktywna jest subskrypcja. W przypadku BackPressure komunikacja zachodzi na żądanie (on demand), czyli wtedy kiedy jest to niezbędne.
Przykład działania nieblokującego
Przykładowo rozpatrzmy REST API napisane w modelu reaktywnym. Jego zadaniem jest pobranie danych z bazy, a następnie udostępnienie ich. Taka usługa będzie klasycznym przypadkiem Publisher’a, natomiast klient pobierający dane z tego API stanowić będzie Subscriber’a
Jednak, aby aplikacja w pełni spełniała koncepcje programowania reaktywnego to muszą zostać uwzględnione dodatkowe założenia:
- Asynchroniczność
- zdarzenia przetwarzane są asynchronicznie
- inna funkcjonalność odpowiada za wywołanie zdarzenia
- inna funkcjonalność kiedy emitowany jest błąd
- Non Blocking
- jeden wątek przyjmuje żądanie i deleguje je dalej, po czym nasłuchuje kolejnych żądań;
Dla kontrastu w przypadku klasycznego, blokującego podejścia – jeden wątek obsługuje żądanie po czym czeka, aż dostanie odpowiedź (np.) z bazy danych (chodź równie dobrze przykładem może być pobieranie danych z sieci, z dysku). Dopiero jak dostanie on odpowiedź z bazy danych to może wrócić do klienta. Zwróć uwagę – wątek musi (bezczynnie) czekać aż baza danych przetworzy żądanie, dopiero wątek potem wraca do klienta. Na ten czas jest on zamrożony (nieużywalny).
W podejściu nieblokującym jeden wątek odpowiada za obsługę żądania i kiedy żądanie się pojawi wówczas deleguję on obsługę do dedykowanej puli wątków (np odpowiadającą za pobieranie danych z bazy) a sam jest gotowy działać dalej – nie czeka. Dzięki mechanizmowi callbacków (odpowiedziom na zdarzenie) wątek delegujący żądanie dowiaduje się o jego ukończeniu i przekazuje odpowiedź do Subscriber’a.
Więc przewagą programowania reaktywnego jest optymalne wykorzystanie działanie wątków, bez oczekiwania jak jest to w przypadku podejścia klasycznego.
Kiedy stosować programowanie reaktywne?
Rozwiązania reaktywne mają znacznie bardziej większą złożoność od rozwiązań klasycznych. Rozwiązania blokujące mogą nieefektywnie zarządzać pulą wątków, ale problemem występuje dopiero kiedy danemu serwerowi skończy się pula dostępnych wątków.
Dla przykładu Apache Tomcat domyślnie dysponuje 200 wątkami. Po przekroczeniu tej puli zaczyna się problem – ponieważ kolejny requesty będą musiały oczekiwać na obsłużenie.
Jednak przetwarzanie 200 requestów w tym samym czasie jeszcze nie jest powodem do zmiany stacku na reaktywny. W pierwszej kolejności należy optymalizować czas obsługi żądania, tak, aby zwiększyć współczynnik RPS (Requests per second) czyli ilość obsłużonych żądań na sekundę. Im ten współczynnik mamy większy tym bardziej zaoszczędzoną mamy pulę wątków.
Dlatego programowanie reaktywne praktykuje się w dużych lub perspektywiczni dużych projektach, natomiast porównanie wydajności stacków można osiągnąć poprzez realizacje testów i wykonanie benchmark’ów – np. z wykorzystywaniem narzędzia Gatling, które w tym przypadku sprawdza się świetnie i Ci je rekomenduje.
Programistyczny przykład w Java
Zapraszam Cię do obejrzenia mojego materiału wideo w ramach którego pokazuje jak stworzyć reaktywne API z wykorzystaniem Spring WebFlux.
Programowanie reaktywne – podsumowanie
Główną zaletą programowania reaktywnego jest skalowalność usług, oraz optymalne zagospodarowanie wątkami.
Całość wyłożonej wiedzy polecam Ci uzupełnić o The Reactive Manifesto – jest to dokument, który dokładnie opisuje założenia systemów reaktywnych. Wato mieć na uwadze, że systemy reaktywne nie są tym samym czym jest programowanie reaktywne.
Pochwal się w komentarzu, czy pracujesz w stacku reaktywnym i jeśli tak to podziel się jakie są Twoje doświadczenia 😉