3 Podejścia do background jobów
W trakcie pracy jako programista .NET wiele razy spotkałem się z koniecznością implementacji rozwiązań mających na celu wykonywanie złożonych obliczeniowo zadań lub zdarzeń wywoływanych czasowo (tzw. Background Jobów). Przykładem tych działań może być import złożonych danych z plików wejściowych (takich jak arkusze danych) czy wysyłka powiadomień i newsletterów do użytkownika aplikacji.
W jaki sposób poradzić sobie z zadaniem tego rodzaju? Tworząc takie moduły możemy wykorzystać trzy rozwiązania, które uważam za kluczowe w tworzeniu Background Jobów: BackgroundService, Hangfire oraz Azure Functions.
Co trzeba wiedzieć na samym początku?
Zacznijmy od tego, że BackgroundService jest klasą abstrakcyjną wprowadzoną w .NET Core 2.0. Aby utworzyć usługę działającą w tle należy napisać klasę dziedziczącą po niej oraz zaimplementować metodę ExecuteAsync, w której powinna znaleźć się realizowana przez usługę logika, mamy również możliwość nadpisania takich metod jak StartAsync czy StopAsync, w celu niestandardowej implementacji procesu uruchomienia i zatrzymania naszej usługi.
Hangfire to biblioteka na licencji LGPL 3.0, z pozwoleniem na darmowe użycie w projektach komercyjnych, z możliwością rozszerzenia o płatne funkcjonalności. Darmowa wersja udostępnia wszystkie potrzebne funkcjonalności do implementacji zadań w tle, ich monitorowanie oraz zarządzania przez interfejs administratora.
Natomiast Azure Functions to usługa chmury Azure, umożliwiająca tworzenie aplikacji serverless, które są uruchamiane tylko wtedy, gdy są potrzebne. Mogą być pisane w wielu językach (między innymi C#, JavaScript czy Python) oraz uruchamiane przez na różne sposoby, między innymi poprzez Timer czy wywołania HTTP. Nic nie stoi na przeszkodzie, aby hostować je w chmurze Azure lub na urządzeniach lokalnych, na przykład w kontenerach Dockera.
Różnice między tymi trzema rozwiązaniami są znaczące i trudno jest jednoznacznie wskazać, które z nich sprawdza się najlepiej. Postanowiłem więc porównać ich działanie na przestrzeni kilku kategorii:
Uruchomienie kodu:
- BackgroundService domyślnie jest uruchamiany w momencie startu aplikacji. Mamy możliwość zmiany tego zachowania przez nasz kod (napisanie własnego Timera czy niestandardowa implementacja StartAsync/StopAsync
- Hangfire pozwala uruchomić naszą usługę cyklicznie, o ustalonej godzinie, przez wyrażenie CRON, natychmiastowe uruchomienie w tle, zaplanowanie na uruchomienie w późniejszym czasie.
- Azure Functions, uruchamianie na podstawie Triggera zdefinowanego w kodzie funkcji, mogą to być wiadomości na kolejce, time czy wywołania HTTP.
Zapewnienie działania:
- BackgroundService: zadanie może nie zostać wykonane – w przypadku niespodziewanego błędu lub zamknięcia się instancji naszej aplikacji – lub może wykonać się więcej niż raz przy restarcie aplikacji lub powstania nowej instancji w przypadku skalowania. Programista musi samemu zaimplementować mechanizm nad tym czuwający, na przykład Outbox Pattern.
- Hangfire: zadania są monitorowane z użyciem bazy danych lub Redisa. Możliwa jest też konfiguracja automatycznego powtarzania nieudanych zadań.
- Azure Functions: rozwiązanie wymaga Azure Storage w celu monitorowania wywołań. Implementacja automatycznego powtarzanie nieudanych zadań jest zależna od wybranych triggerów.
Monitorowanie:
- BackgroundService natywnie nie daje narzędzi do monitorowania stanu zadania. Programista musi samemu zaimplementować takie rozwiązanie.
- Hangfire udostępnia Dashboard, który pokazuje aktualny status zadania czy możliwość jego restartu.
- Azure Functions możemy monitorować przez Portal Azure oraz Application Insight.
Hostowanie:
- BackgroundService jest częścią aplikacji, hostujemy go razem z nią.
- Hangfire jest częścią aplikacji, hostujemy go razem z nią lub tworzymy osobną aplikację, w której możemy realizować zadania wywoływane przez inne aplikacje za pomocą wspólnej bazy danych (zadania w Hangfirze są zapisywane w bazie i uruchamiane przez aplikacje zarejestrowane jako Server Hangfire).
- Azure Functions posiada wiele planów hostowania w chmurze Azure, możliwe jest również hostowanie w kontenerze Docker lub w klastrze Kubernetes.
Które rozwiązanie wybrać?
Oczywiście niemożliwe jest wskazanie, które z rozwiązań sprawdza się najlepiej. Mają one swoje mocne strony w określonych sytuacjach i to właśnie pod tym kątem powinniśmy je oceniać. Moje spostrzeżenia przy pracy z każdym z rozwiązań prezentują się więc następująco:
BackgroundService
Nadają się idealnie do zadań, które chcemy uruchamiać przy włączeniu aplikacji, bez spowalniania czasu jej uruchomienia lub gdy są to zadania cyklicznie wykonywane bez potrzeby monitorowania ich statusu czy poprawności wykonania (tzw. “fire and forget”).
Hangfire
Jest to świetne rozwiązanie dla klientów nie chcących uzależniać swojego projektu od konkretnego dostawcy rozwiązań chmurowych. Dostarcza wiele gotowych rozwiązań, dzięki którym programista może szybko zaimplementować właściwą logikę, bez potrzeby konfiguracji monitoringu aplikacji czy jej “durability”. Dodatkowo Hangfire może korzystać z serwisu wcześniej znajdującego się w aplikacji, co przekłada się na szybsze wprowadzanie zmian w background jobs i ogólny niższy próg wejścia dla nowych użytkowników.
Azure Functions
Świetne uzupełnienie aplikacji już hostowanej w Azure. Pozwala szybko napisać nową funkcjonalność dzięki strukturze kodu unikalnej dla Azure Functions. Zapewnia przy tym dobry monitoring działania w integracji z Azurem oraz dużo możliwości na wywołanie takiej funkcji oraz zaoszczędzenie kosztów przez wybranie odpowiedniego modelu hostowania.
Podsumowanie
Podczas tworzenia zadań wykonywanych w tle mamy więc dużo możliwości w doborze rozwiązania dopasowanego do naszych potrzeb. Wymienione przeze mnie narzędzia są tymi z których najczęściej korzystam w mojej pracy – wierzę, że moje doświadczenia w tym temacie pomogą Wam w dokonaniu wyboru.