Elementy składowe aplikacji rozproszonych. Drugie przybliżenie

Zapowiedź

Koledzy, w połowie lata planuję opublikować kolejną serię artykułów na temat projektowania systemów kolejkowych: „Eksperyment VTrade” - próba napisania frameworka dla systemów transakcyjnych. W ramach cyklu omówiona zostanie teoria i praktyka budowy giełdy, aukcji i sklepu. Na końcu artykułu zapraszam do głosowania na tematy, które najbardziej Cię interesują.

Elementy składowe aplikacji rozproszonych. Drugie przybliżenie

To jest ostatni artykuł z serii o rozproszonych aplikacjach reaktywnych w Erlang/Elixir. W Pierwszy artykuł można znaleźć teoretyczne podstawy architektury reaktywnej. Drugi artykuł ilustruje podstawowe wzorce i mechanizmy konstruowania takich systemów.

Dzisiaj poruszymy kwestie rozwoju bazy kodu i projektów w ogóle.

Organizacja usług

W prawdziwym życiu, tworząc usługę, często trzeba połączyć kilka wzorców interakcji w jednym kontrolerze. Przykładowo usługa użytkowników, która rozwiązuje problem zarządzania profilami użytkowników projektu, musi odpowiadać na żądania req-resp i raportować aktualizacje profili poprzez pub-sub. Sprawa jest dość prosta: za przesyłaniem komunikatów kryje się jeden kontroler, który implementuje logikę usługi i publikuje aktualizacje.

Sytuacja komplikuje się, gdy musimy wdrożyć usługę rozproszoną odporną na awarie. Wyobraźmy sobie, że zmieniły się wymagania wobec użytkowników:

  1. teraz usługa powinna przetwarzać żądania na 5 węzłach klastra,
  2. móc wykonywać zadania przetwarzania w tle,
  3. a także móc dynamicznie zarządzać listami subskrypcji w celu aktualizacji profili.

Uwaga: Nie rozważamy kwestii spójnego przechowywania i replikacji danych. Załóżmy, że problemy te zostały już wcześniej rozwiązane i system ma już niezawodną i skalowalną warstwę pamięci masowej, a procedury obsługi mają mechanizmy umożliwiające interakcję z nią.

Formalny opis obsługi użytkowników stał się bardziej skomplikowany. Z punktu widzenia programisty zmiany są minimalne ze względu na użycie komunikatorów. Aby spełnić pierwszy wymóg, musimy skonfigurować równoważenie w punkcie wymiany req-resp.

Często występuje konieczność przetwarzania zadań w tle. W przypadku użytkowników może to być sprawdzanie dokumentów użytkownika, przetwarzanie pobranych multimediów lub synchronizowanie danych z mediami społecznościowymi. sieci. Zadania te należy w jakiś sposób rozdzielić w obrębie klastra i monitorować postęp ich realizacji. Dlatego mamy dwie możliwości rozwiązania: albo skorzystać z szablonu dystrybucji zadań z poprzedniego artykułu, albo, jeśli nie pasuje, napisać niestandardowy harmonogram zadań, który będzie zarządzał pulą procesorów w taki sposób, w jaki potrzebujemy.

Punkt 3 wymaga rozszerzenia szablonu pub-sub. A do wdrożenia, po utworzeniu punktu wymiany pub-sub, musimy dodatkowo uruchomić w naszym serwisie kontroler tego punktu. To tak, jakbyśmy przenieśli logikę obsługi subskrypcji i wypisów z warstwy przesyłania wiadomości do implementacji użytkowników.

W rezultacie dekompozycja problemu pokazała, że ​​aby spełnić wymagania musimy uruchomić 5 instancji usługi na różnych węzłach i stworzyć dodatkowy podmiot – pub-sub kontroler, odpowiedzialny za subskrypcję.
Aby uruchomić 5 handlerów nie trzeba zmieniać kodu serwisowego. Jedyną dodatkową czynnością jest ustawienie zasad bilansowania w punkcie wymiany, o czym porozmawiamy nieco później.
Istnieje również dodatkowa złożoność: kontroler pub-sub i niestandardowy harmonogram zadań muszą działać w jednej kopii. Ponownie usługa przesyłania wiadomości, jako podstawowa, musi zapewniać mechanizm wyboru lidera.

Wybór Lidera

W systemach rozproszonych wybór lidera to procedura polegająca na wyznaczeniu jednego procesu odpowiedzialnego za planowanie rozproszonego przetwarzania pewnego obciążenia.

W systemach, które nie są podatne na centralizację, stosuje się algorytmy uniwersalne i oparte na konsensusie, takie jak paxos czy tratwa.
Ponieważ Messaging jest brokerem i elementem centralnym, wie o wszystkich kontrolerach usług – kandydatach na liderów. Messaging może wyznaczyć lidera bez głosowania.

Po uruchomieniu i połączeniu z punktem wymiany wszystkie usługi otrzymują komunikat systemowy #'$leader'{exchange = ?EXCHANGE, pid = LeaderPid, servers = Servers}. Jeśli LeaderPid pokrywa się z pid bieżącym procesie, wyznacza się go na lidera i tworzy listę Servers zawiera wszystkie węzły i ich parametry.
W momencie pojawienia się nowego i odłączenia działającego węzła klastra, odbierają je wszystkie kontrolery usług #'$slave_up'{exchange = ?EXCHANGE, pid = SlavePid, options = SlaveOpts} и #'$slave_down'{exchange = ?EXCHANGE, pid = SlavePid, options = SlaveOpts} odpowiednio.

W ten sposób wszystkie komponenty są świadome wszystkich zmian, a klaster ma gwarancję, że w danym momencie będzie miał jednego lidera.

Mediatorzy

Do realizacji złożonych procesów przetwarzania rozproszonego, a także przy problemach optymalizacji istniejącej architektury wygodnie jest korzystać z pośredników.
Aby nie zmieniać kodu usługi i rozwiązać np. problemów z dodatkowym przetwarzaniem, routingiem czy rejestrowaniem wiadomości, można przed usługą włączyć obsługę proxy, która wykona całą dodatkową pracę.

Klasycznym przykładem optymalizacji pub-sub jest aplikacja rozproszona z rdzeniem biznesowym generującym zdarzenia aktualizacyjne, takie jak zmiany cen na rynku, oraz warstwą dostępu – N serwerami, które udostępniają websocket API dla klientów webowych.
Jeśli podejmiesz decyzję od razu, obsługa klienta wygląda następująco:

  • klient nawiązuje połączenie z platformą. Po stronie serwera, który kończy ruch, uruchamiany jest proces obsługujący to połączenie.
  • W kontekście procesu serwisowego następuje autoryzacja i subskrypcja aktualizacji. Proces wywołuje metodę subskrybowania tematów.
  • Po wygenerowaniu zdarzenia w jądrze jest ono dostarczane do procesów obsługujących połączenia.

Wyobraźmy sobie, że mamy 50000 5 subskrybentów tematu „aktualności”. Abonenci są rozdzieleni równomiernie na 50000 serwerów. W rezultacie każda aktualizacja, która dotrze do punktu wymiany, zostanie zreplikowana 10000 XNUMX razy: XNUMX XNUMX razy na każdym serwerze, w zależności od liczby abonentów na nim. Niezbyt skuteczny schemat, prawda?
Aby poprawić sytuację, wprowadźmy proxy, które ma taką samą nazwę jak punkt wymiany. Globalny rejestrator nazw musi być w stanie zwrócić najbliższy proces według nazwy, jest to ważne.

Uruchommy ten serwer proxy na serwerach warstwy dostępowej, a wszystkie nasze procesy obsługujące API websocket będą subskrybować go, a nie oryginalny punkt wymiany pub-sub w jądrze. Proxy subskrybuje rdzeń tylko w przypadku unikalnej subskrypcji i replikuje wiadomość przychodzącą do wszystkich swoich abonentów.
W rezultacie pomiędzy jądrem a serwerami dostępowymi zostanie wysłanych 5 komunikatów zamiast 50000 XNUMX.

Routing i równoważenie

Req-Resp

W obecnej implementacji przesyłania wiadomości istnieje 7 strategii dystrybucji żądań:

  • default. Żądanie zostaje wysłane do wszystkich kontrolerów.
  • round-robin. Żądania są wyliczane i cyklicznie rozdzielane pomiędzy kontrolerami.
  • consensus. Kontrolerzy obsługujący usługę dzielą się na liderów i niewolników. Prośby wysyłane są wyłącznie do lidera.
  • consensus & round-robin. Grupa ma lidera, ale prośby są rozdzielane pomiędzy wszystkich członków.
  • sticky. Funkcja skrótu jest obliczana i przypisana do określonej procedury obsługi. Kolejne żądania z tym podpisem trafiają do tego samego modułu obsługi.
  • sticky-fun. Podczas inicjalizacji punktu wymiany funkcja obliczania skrótu dla sticky balansowy.
  • fun. Podobnie jak w przypadku sticky-fun, tylko Ty możesz go dodatkowo przekierować, odrzucić lub wstępnie przetworzyć.

Strategia dystrybucji jest ustawiana podczas inicjalizacji punktu wymiany.

Oprócz równoważenia, przesyłanie wiadomości umożliwia oznaczanie encji. Przyjrzyjmy się rodzajom tagów w systemie:

  • Etykieta połączenia. Pozwala zrozumieć, przez jakie połączenie doszło do wydarzeń. Używane, gdy proces kontrolera łączy się z tym samym punktem wymiany, ale z różnymi kluczami routingu.
  • Etykieta serwisowa. Umożliwia łączenie procedur obsługi w grupy dla jednej usługi oraz rozszerzanie możliwości routingu i równoważenia. W przypadku wzorca req-resp routing jest liniowy. Wysyłamy zapytanie do punktu wymiany, który następnie przekazuje je do serwisu. Jeśli jednak musimy podzielić procedury obsługi na grupy logiczne, podział odbywa się za pomocą tagów. W przypadku podania tagu żądanie zostanie wysłane do określonej grupy kontrolerów.
  • Poproś o tag. Umożliwia rozróżnienie odpowiedzi. Ponieważ nasz system jest asynchroniczny, aby przetworzyć odpowiedzi serwisowe, musimy mieć możliwość określenia RequestTag podczas wysyłania żądania. Z tego będziemy mogli zrozumieć odpowiedź, na jaką prośbę do nas przyszło.

Pub-sub

W przypadku pub-sub wszystko jest trochę prostsze. Posiadamy punkt wymiany, w którym publikowane są wiadomości. Punkt wymiany dystrybuuje wiadomości pomiędzy abonentów, którzy zapisali się na potrzebne im klucze routingu (można powiedzieć, że jest to analogiczne do tematów).

Skalowalność i odporność na błędy

Skalowalność systemu jako całości zależy od stopnia skalowalności warstw i komponentów systemu:

  • Usługi są skalowane poprzez dodanie do klastra dodatkowych węzłów z procedurami obsługi tej usługi. Podczas pracy próbnej możesz wybrać optymalną politykę bilansowania.
  • Sama usługa przesyłania komunikatów w oddzielnym klastrze jest zazwyczaj skalowana poprzez przenoszenie szczególnie obciążonych punktów wymiany do oddzielnych węzłów klastra lub przez dodanie procesów proxy do szczególnie obciążonych obszarów klastra.
  • Skalowalność całego systemu jako cecha zależy od elastyczności architektury i możliwości łączenia poszczególnych klastrów we wspólną logiczną całość.

Sukces projektu często zależy od prostoty i szybkości skalowania. Wiadomości w obecnej wersji rozwijają się wraz z aplikacją. Nawet jeśli brakuje nam klastra 50-60 maszyn, możemy sięgnąć po federację. Niestety temat federacji wykracza poza zakres tego artykułu.

Rezerwacja

Analizując równoważenie obciążenia, omawialiśmy już redundancję kontrolerów usług. Jednak przesyłanie wiadomości również musi być zarezerwowane. W przypadku awarii węzła lub maszyny przesyłanie wiadomości powinno zostać automatycznie przywrócone w możliwie najkrótszym czasie.

W moich projektach wykorzystuję dodatkowe węzły, które przejmują obciążenie w przypadku upadku. Erlang ma standardową implementację trybu rozproszonego dla aplikacji OTP. Tryb rozproszony wykonuje odzyskiwanie w przypadku awarii poprzez uruchomienie uszkodzonej aplikacji na innym wcześniej uruchomionym węźle. Proces jest transparentny, po awarii aplikacja automatycznie przechodzi do węzła awaryjnego. Możesz przeczytać więcej o tej funkcjonalności tutaj.

produktywność

Spróbujmy przynajmniej z grubsza porównać wydajność królikmq i naszego niestandardowego przesyłania wiadomości.
znalazłem oficjalne wyniki testy królika mq od zespołu openstack.

W pkt 6.14.1.2.1.2.2. Oryginalny dokument przedstawia wynik RPC CAST:
Elementy składowe aplikacji rozproszonych. Drugie przybliżenie

Nie będziemy wcześniej wprowadzać żadnych dodatkowych ustawień jądra systemu operacyjnego ani maszyny wirtualnej Erlang. Warunki testowania:

  • erl optuje: +A1 +sbtu.
  • Test w ramach pojedynczego węzła erlang jest uruchamiany na laptopie ze starym i7 w wersji mobilnej.
  • Testy klastrów przeprowadzane są na serwerach z siecią 10G.
  • Kod działa w kontenerach dokowanych. Sieć w trybie NAT.

Kod testowy:

req_resp_bench(_) ->
  W = perftest:comprehensive(10000,
    fun() ->
      messaging:request(?EXCHANGE, default, ping, self()),
      receive
        #'$msg'{message = pong} -> ok
      after 5000 ->
        throw(timeout)
      end
    end
  ),
  true = lists:any(fun(E) -> E >= 30000 end, W),
  ok.

Scenariusz 1: Test przeprowadzany jest na laptopie ze starą mobilną wersją i7. Test, przesyłanie wiadomości i usługa są wykonywane w jednym węźle w jednym kontenerze Docker:

Sequential 10000 cycles in ~0 seconds (26987 cycles/s)
Sequential 20000 cycles in ~1 seconds (26915 cycles/s)
Sequential 100000 cycles in ~4 seconds (26957 cycles/s)
Parallel 2 100000 cycles in ~2 seconds (44240 cycles/s)
Parallel 4 100000 cycles in ~2 seconds (53459 cycles/s)
Parallel 10 100000 cycles in ~2 seconds (52283 cycles/s)
Parallel 100 100000 cycles in ~3 seconds (49317 cycles/s)

Scenariusz 2: 3 węzły działające na różnych komputerach w oknie dokowanym (NAT).

Sequential 10000 cycles in ~1 seconds (8684 cycles/s)
Sequential 20000 cycles in ~2 seconds (8424 cycles/s)
Sequential 100000 cycles in ~12 seconds (8655 cycles/s)
Parallel 2 100000 cycles in ~7 seconds (15160 cycles/s)
Parallel 4 100000 cycles in ~5 seconds (19133 cycles/s)
Parallel 10 100000 cycles in ~4 seconds (24399 cycles/s)
Parallel 100 100000 cycles in ~3 seconds (34517 cycles/s)

We wszystkich przypadkach wykorzystanie procesora nie przekroczyło 250%

Wyniki

Mam nadzieję, że ten cykl nie będzie przypominał wysypiska myśli i moje doświadczenie będzie realną korzyścią zarówno dla badaczy systemów rozproszonych, jak i praktyków, którzy są na samym początku budowania architektur rozproszonych dla swoich systemów biznesowych i z zainteresowaniem patrzą na Erlang/Elixir , ale mam wątpliwości, czy warto...

Strzał Fotek @chuttersnap

W ankiecie mogą brać udział tylko zarejestrowani użytkownicy. Zaloguj się, Proszę.

Jakie tematy powinienem poruszyć bardziej szczegółowo w ramach serii Eksperyment VTrade?

  • Teoria: Rynki, zlecenia i ich czas: DAY, GTD, GTC, IOC, FOK, MOO, MOC, LOO, LOC

  • Księga zamówień. Teoria i praktyka stosowania książki z grupowaniami

  • Wizualizacja handlu: Tiki, słupki, uchwały. Jak przechowywać i jak kleić

  • Zaplecze biurowe. Planowanie i rozwój. Monitorowanie pracowników i badanie incydentów

  • API. Zastanówmy się, jakie interfejsy są potrzebne i jak je wdrożyć

  • Przechowywanie informacji: PostgreSQL, Timescale, Tarantool w systemach handlowych

  • Reaktywność w systemach transakcyjnych

  • Inny. Napiszę w komentarzach

Głosowało 6 użytkowników. 4 użytkowników wstrzymało się od głosu.

Źródło: www.habr.com

Dodaj komentarz