W kilku postach mam zamiar opisać bibliotekę nServiceBus. Nie chodzi mi jednak o opis samego API, a zastanowienie się, kiedy warto z takiej architektury skorzystać. Większość programistów wciąż projektuje systemy na zasadzie klient-serwer. W wielu przypadkach jest to wystarczające rozwiązanie. Nie zawsze musimy tworzyć skalowalne rozwiązania. NoSQL, hadoop, chmura mają zastosowanie ale w wielu przypadkach jest to po prostu niepotrzebne. Nie każdy tworzy oprogramowanie, wykorzystywane przez miliony użytkowników. Wracając jednak do tematu. W przypadku nServiceBus chciałbym skupić się bardziej na scenariuszach w których klasyczne przetwarzanie zapytań może po prostu doprowadzić do zawieszenia systemu.
Klasyczny model to RPC – remote procedure call. Polega on na wywołaniu metody (web service, kontroler w MVC) i zwróceniu natychmiast wyniku. Jest to bardzo proste i analogiczne do wywoływania zwykłej metody in-memory. Kod można pisać sekwencyjnie czyli natychmiast po wywołaniu metody będziemy posiadać dostęp do tego co wróci – bez potrzebny implementacji callback’ow. Ta zaleta (łatwość obsługi) bezpośrednio stanowi największą wadę, a mianowicie brak skalowalności. Zastanówmy się co może przytrafić nam się w momencie wysłania zapytania przez klienta:
- Serwer tymczasowo będzie niedostępny.
- Sieć zostanie odłączona.
- Sieć będzie bardzo obciążona, stąd na rezultat przyjdzie czekać dłużej niż zwykle.
W przypadku RPC, gdy serwer nie działa, oczywiście zapytanie zakończy się błędem lub blokowaniem klienta. Co jeśli kilka tysięcy klientów wyśle zapytanie w tej samej chwili? Prymitywna implementacja RPC stworzy kilka tysięcy wątków, co oczywiście będzie miało katastrofalne skutki. RPC przede wszystkim nie jest odporne na awarie zarówno po stronie klienta jak i serwera. Możliwe scenariusze to:
- Klient wysyła zapytanie ale serwer w międzyczasie ulega awarii stąd nie jest w stanie obsłużyć zapytania.
- Klient wysyła zapytanie, serwer obsługuje je ale w miedzy czasie klient ulega awarii. Z tego względu nie będzie możliwe otrzymanie odpowiedzi.
Do tego dochodzi czas wykonania powyższych operacji. RPC przyjmuje, że jest on stały i nie powinien blokować użytkownika na zbyt duży czas.
Kolejny problem to skalowalność. RPC to prosta architektura klient-serwer. Nie ma możliwości dystrybucji zapytań między różne węzły. Oczywiście to uproszczenie bo możemy mieć w końcu load balancer i kilka serwerów obsługujących zapytania. Im bardziej jednak będziemy zapytania “rozpraszać”, tym szybciej dojdziemy do architektury odmiennej od RPC.
Klasyczny model RPC możemy przedstawić zatem następująco:
Prosta sprawa, wysyłamy zapytanie i dostajemy od razu odpowiedź. Jeśli wywołamy kolejno metody A i B to najpierw uzyskamy wynik A a potem B – kolejność zachowana.
Przejdźmy teraz do wyjaśnienia jak działa nServiceBus i podobne technologie. Przede wszystkim mamy do dyspozycji kolejki. Serwer zamiast próbować obsłużyć wszystkie zadania jednocześnie, przechowuje dane na kolejce. W przypadku nServiceBus jest to MSMQ (Microsoft’owa implementacja). MSMQ to jest temat sam w sobie na długi artykuł albo książkę więc nie będę tutaj wyjaśniał API. Każdy węzeł (klient, serwer) posiada dwie grupy kolejek:
- kolejki wiadomości wychodzących
- kolejki wiadomości przychodzących
Dobrym porównaniem systemów opartych o komunikaty (wiadomości) jest poczta email. Jeśli osoba A wysyła email do osoby B, to odbierze ona go, gdy będzie miała czas. Osoba A nie spodziewa się natychmiastowej odpowiedzi. Wysyła po prostu swoje pytanie (wiadomość). Następnie osoba B w wolnym czasie odbiera wiadomość, czyta i odpowiada. Analogicznie osoba A odczyta odpowiedź, gdy będzie miała czas.
Powróćmy znów do nServiceBus. Jeśli klient wysyła wiadomość do serwera będzie wyglądać to następująco:
Najpierw klient umieszcza wiadomość na wewnętrznej kolejce wiadomości wychodzących. Nawet jak połączenie między klientem a serwerem zostanie zerwane, wiadomość będzie przechowywana w kolejce i gotowa do wysłania, gdy tylko połączenie znów będzie działać. Następnie, po wysłaniu, zostanie ona umieszczona w kolejce wiadomości przychodzących drugiego węzła. Jeśli serwer jest bardzo zajęty, zostanie ona po prostu tam przechowywana aż do momentu, kiedy może zostać obsłużona. W pewnym momencie serwer zdejmie wiadomość, wykona dane zadanie i wygeneruje odpowiedź. Odpowiedz zwracana jest również w analogiczny sposób:
Serwer po wygenerowaniu odpowiedzi umieszcza ją w kolejce wiadomości wychodzących. Następnie, jak tylko będzie taka możliwość, jest ona przesyłana do klienta i umieszczana w kolejce komunikatów przychodzących. Klient potem zdejmuje ją z kolejki i obsługuje odpowiedź w odpowiedni sposób.
Dzięki dwóm typom kolejek, jesteśmy odporni na awarie sieci. Zmienia natomiast to sposób jak komunikujemy się z serwerem. Nie jest to model zapytanie-odpowiedź! Należy mieć to na uwadze, że nie ma gwarancji kiedy dostaniemy odpowiedź. Nie zawsze taki mechanizm nadaje się w danej aplikacji. Pojęcia klient oraz serwer również nie są prawidłowe i będę dalej posługiwać się słowem “węzeł”. Bardzo często w systematach opartych o kolejki mamy np. 5 węzłów i każdy z nich może przetwarzać te same zapytanie.
To dopiero początek. W przyszłym wpisie pokażę jakiś prosty system oparty o nServiceBus. Więcej czasu w kolejnych wpisach również chcę przeznaczyć, jak nServiceBus dba o to, aby wiadomość dotarła i została przetworzona. Póki, co wiemy, że w przypadku awarii sieci, wiadomość pozostaje w kolejce. Co jednak w przypadku, gdy obsługa wiadomości zakończyła się wyjątkiem?
Bardzo ciekawy post! Czekam na kolejne 😉