Category Archives: Programowanie rozproszone

Systemy oparte na kolejkach komunikatów na przykładzie nServiceBus, część I

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:

  1. Serwer tymczasowo będzie niedostępny.
  2. Sieć zostanie odłączona.
  3. 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:

  1. Klient wysyła zapytanie ale serwer w międzyczasie ulega awarii stąd nie jest w stanie obsłużyć zapytania.
  2. 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:

image

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:

  1. kolejki wiadomości wychodzących
  2. 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:

image

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:

image

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?