Jak zaprojektować ciężką w czasie rzeczywistym aplikację internetową opartą na gniazdach sieciowych?

17

W trakcie opracowywania aplikacji jednostronicowej w czasie rzeczywistym stopniowo wprowadzałem websockets, aby umożliwić moim użytkownikom dostęp do aktualnych danych. Podczas tej fazy ze smutkiem zauważyłem, że niszczę zbyt wiele struktury mojej aplikacji i nie udało mi się znaleźć rozwiązania tego problemu.

Zanim przejdę do szczegółów, wystarczy trochę kontekstu:

  • Aplikacja internetowa to SPA w czasie rzeczywistym;
  • Backend jest w Ruby on Rails. Zdarzenia w czasie rzeczywistym są wypychane przez Ruby do klucza Redis, a następnie serwer mikro-węzłów wyciąga to i wypycha do Socket.Io;
  • Frontend jest w AngularJS i łączy się bezpośrednio z serwerem socket.io w Node.

Po stronie serwera, w czasie rzeczywistym miałem wyraźny rozdział zasobów oparty na kontrolerze / modelu, z przetwarzaniem dołączonym do każdego z nich. Ten klasyczny projekt MVC został całkowicie zniszczony, a przynajmniej ominięty, w momencie, gdy zacząłem przesyłać rzeczy za pośrednictwem gniazd sieciowych do moich użytkowników. Mam teraz pojedynczy potok, w którym cała moja aplikacja płynie mniej lub bardziej ustrukturyzowanymi danymi . I uważam to za stresujące.

W interfejsie głównym problemem jest powielanie logiki biznesowej. Gdy użytkownik ładuje stronę, muszę ładować moje modele przez klasyczne wywołania AJAX. Ale muszę też radzić sobie z zalewaniem danych w czasie rzeczywistym i duplikuję większość logiki biznesowej po stronie klienta, aby zachować spójność modeli po stronie klienta.

Po kilku badaniach nie mogę znaleźć żadnych dobrych postów, artykułów, książek ani czegokolwiek, co dałoby porady na temat tego, jak można i należy zaprojektować architekturę nowoczesnej aplikacji internetowej z uwzględnieniem kilku konkretnych tematów:

  • Jak uporządkować dane wysyłane z serwera do użytkownika?
    • Czy powinienem wysyłać tylko zdarzenia typu „ten zasób został zaktualizowany i należy go ponownie załadować za pośrednictwem wywołania AJAX”, czy też przesłać zaktualizowane dane i zastąpić poprzednie dane załadowane za pomocą początkowych wywołań AJAX?
    • Jak zdefiniować spójny i skalowalny szkielet wysyłanych danych? jest to komunikat o aktualizacji modelu lub komunikat „wystąpił błąd związany z blahblahblah”
  • Jak nie wysyłać danych o wszystkim z dowolnego miejsca w wewnętrznej bazie danych?
  • Jak ograniczyć powielanie logiki biznesowej zarówno po stronie serwera, jak i klienta?
Philippe Durix
źródło
Czy na pewno Rails to najlepsza opcja dla Twojego SPA? Railsy są świetne, ale są przeznaczone do aplikacji monolitycznej ... możesz chcieć modułowego backendu z separacją problemów ... W zależności od twoich potrzeb, rozważę alternatywne ramy Ruby dla SPA w czasie rzeczywistym.
Myst
1
Nie jestem pewien, czy Rails jest najlepszą opcją, ale jestem bardzo zadowolony ze stosu na miejscu przynajmniej na backendie (prawdopodobnie dlatego, że jestem dobry z tym konkretnym frameworkiem). Moje obawy dotyczą bardziej tego, jak zaprojektować SPA z technologicznego punktu widzenia. Nie chcę też zwielokrotniać liczby języków, ram i bibliotek w jednym projekcie.
Philippe Durix,
Połączony test porównawczy może mieć problemy, ale ujawnia słabość obecnej implementacji ActiveRecord. Być może jestem stronniczy w kwestii plezi.io , ale jak wskazano w późniejszych wynikach testu porównawczego , zapewnia on znaczną poprawę wydajności, nawet przed klastrowaniem i Redis (które nie były testowane). Myślę, że działał lepiej niż node.js ... Aż do zmiany, używałbym plezi.io.
Myst

Odpowiedzi:

10

Jak uporządkować dane wysyłane z serwera do użytkownika?

Użyj wzorca wiadomości . Cóż, już używasz protokołu przesyłania wiadomości, ale mam na myśli strukturę zmian jako wiadomości ... konkretnie zdarzenia. Zmiana po stronie serwera powoduje zdarzenia biznesowe. W twoim scenariuszu opinie klientów są zainteresowane tymi wydarzeniami. Zdarzenia powinny zawierać wszystkie dane istotne dla tej zmiany (niekoniecznie wszystkie dane przeglądania). Strona klienta powinna następnie zaktualizować części widoku, które obsługuje, o dane zdarzenia.

Na przykład, jeśli aktualizowałeś giełdowy giełdowy i zmieniono AAPL, nie chciałbyś zepchnąć wszystkich cen akcji ani nawet wszystkich danych o AAPL (nazwa, opis itp.). Naciskasz tylko AAPL, deltę i nową cenę. Na kliencie zaktualizowałbyś wtedy tylko tę cenę akcji w widoku.

Czy powinienem wysyłać tylko zdarzenia typu „ten zasób został zaktualizowany i należy go ponownie załadować za pośrednictwem wywołania AJAX”, czy też przesłać zaktualizowane dane i zastąpić poprzednie dane załadowane za pomocą początkowych wywołań AJAX?

Nie powiedziałbym ani jednego. Jeśli wysyłasz zdarzenie, śmiało i wyślij wraz z nim odpowiednie dane (nie dane całego obiektu). Nadaj mu nazwę tego rodzaju wydarzenia. (Nazewnictwo i dane istotne dla tego zdarzenia wykraczają poza mechaniczną pracę systemu. Ma to więcej wspólnego z tym, jak modelowana jest logika biznesowa.) Osoby aktualizujące widok muszą wiedzieć, jak przełożyć każde konkretne zdarzenie na precyzyjna zmiana widoku (tj. aktualizuj tylko to, co się zmieniło).

Jak zdefiniować spójny i skalowalny szkielet wysyłanych danych? jest to komunikat o aktualizacji modelu lub komunikat „wystąpił błąd związany z blahblahblah”

Powiedziałbym, że jest to duże, otwarte pytanie, które należy podzielić na kilka innych pytań i opublikować osobno.

Ogólnie jednak system zaplecza powinien tworzyć i wysyłać zdarzenia dotyczące ważnych wydarzeń w firmie. Mogą one pochodzić z zewnętrznych źródeł lub z aktywności w samym zapleczu.

Jak nie wysyłać danych o wszystkim z dowolnego miejsca w wewnętrznej bazie danych?

Użyj wzorca publikowania / subskrypcji . Gdy SPA wczyta nową stronę, która jest zainteresowana otrzymywaniem aktualizacji w czasie rzeczywistym, strona powinna subskrybować tylko te zdarzenia, z których może korzystać, i wywoływać logikę aktualizacji widoku w miarę pojawiania się tych zdarzeń. Prawdopodobnie będziesz potrzebować logiki pub / sub na serwer w celu zmniejszenia obciążenia sieci. Istnieją biblioteki dla publikacji / subskrybentów Websocket, ale nie jestem pewien, jakie są w ekosystemie Railsów.

Jak ograniczyć powielanie logiki biznesowej zarówno po stronie serwera, jak i klienta?

Wygląda na to, że musisz zaktualizować dane widoku zarówno na kliencie, jak i na serwerze. Domyślam się, że potrzebujesz danych widoku po stronie serwera, aby mieć migawkę, aby uruchomić klienta w czasie rzeczywistym. Ponieważ w grę wchodzą dwa języki / platformy (Ruby i Javascript), logika aktualizacji widoku będzie musiała zostać napisana w obu. Oprócz transpilacji (która ma swoje własne problemy), nie widzę rozwiązania tego problemu.

Punkt techniczny: manipulacja danymi (aktualizacja widoku) nie jest logiką biznesową. Jeśli chodzi o sprawdzanie poprawności przypadków użycia, wydaje się to nieuniknione, ponieważ sprawdzanie poprawności klienta jest konieczne dla dobrego doświadczenia użytkownika, ale ostatecznie nie może być zaufane przez serwer.


Oto, w jaki sposób widzę dobrze taką strukturę.

Widoki klienta:

  • Żąda obrazu stanu i ostatnio widzianego numeru zdarzenia
    • Spowoduje to wstępne wypełnienie widoku, aby klient nie musiał budować od zera.
    • Może być przez HTTP GET dla uproszczenia
  • Nawiązuje połączenie typu websocket i subskrybuje określone zdarzenia, zaczynając od ostatniego numeru zdarzenia widoku.
  • Odbiera zdarzenia przez gniazdo sieciowe i aktualizuje jego widok na podstawie typu / danych zdarzenia.

Polecenia klienta:

  • Żądaj zmiany danych (HTTP PUT / POST / DELETE)
    • Odpowiedź to tylko sukces lub porażka + błąd
    • (Zdarzenia wygenerowane przez zmianę przejdą przez websocket i uruchomią aktualizację widoku.)

Po stronie serwera można faktycznie podzielić na kilka komponentów o ograniczonej odpowiedzialności. Taki, który po prostu przetwarza przychodzące żądania i tworzy zdarzenia. Inny może zarządzać subskrypcjami klientów, nasłuchiwać zdarzeń (powiedzmy w toku) i przekazywać odpowiednie zdarzenia subskrybentom. Możesz mieć jedną trzecią, która nasłuchuje zdarzeń i aktualizuje widoki po stronie serwera - być może dzieje się to nawet zanim subskrybenci otrzymają zdarzenia.

To, co opisałem, to forma CQRS + Messaging i typowa strategia rozwiązywania problemów, z którymi się borykasz .

Nie włączyłem w ten opis Sourcing zdarzeń, ponieważ nie jestem pewien, czy jest to coś, co chcesz podjąć, czy też potrzebujesz go koniecznie. Ale jest to powiązany wzór.

Kasey Speakman
źródło
Dużo poczyniłem na ten temat, a podane wskazówki były bardzo przydatne. Przyjąłem odpowiedź, ponieważ skorzystałem z wielu porad, nawet jeśli nie skorzystałem z nich wszystkich. W innej odpowiedzi opiszę ścieżkę, którą podążałem.
Philippe Durix,
4

Po kilku miesiącach pracy głównie nad backendem, mogłem skorzystać z niektórych porad tutaj, aby rozwiązać problemy, z którymi borykała się platforma.

Głównym celem przy ponownym przemyśleniu backendu było jak najtrudniejsze trzymanie się CRUD. Wszystkie działania, wiadomości i żądania rozproszone po wielu trasach zostały zgrupowane w zasoby, które są tworzone, aktualizowane, czytane lub usuwane . Brzmi to teraz oczywisto, ale to bardzo trudny sposób, aby ostrożnie je zastosować.

Po tym, jak wszystko zostało zorganizowane w zasoby, byłem w stanie dołączyć wiadomości w czasie rzeczywistym do modeli.

  • Tworzenie powoduje wyświetlenie komunikatu z dziurą nowego zasobu;
  • Aktualizacja wyzwala komunikat zawierający tylko zaktualizowane atrybuty (plus UUID);
  • Usunięcie powoduje wyświetlenie komunikatu o usunięciu.

W interfejsie API Reszta wszystkie metody tworzenia, aktualizowania i usuwania generują odpowiedź „tylko głowa”, kod HTTP informujący o sukcesie lub niepowodzeniu oraz faktyczne dane przesyłane przez gniazda sieciowe.

W interfejsie każde zasoby są obsługiwane przez określony komponent, który ładuje je przez HTTP podczas inicjalizacji, a następnie subskrybuje aktualizacje i utrzymuje ich stan w czasie. Widoki następnie łączą się z tymi komponentami, aby wyświetlić zasoby i wykonać działania na tych zasobach przez te same komponenty.


Uważam, że CQRS + Messaging i Event Sourcing czyta bardzo interesujące, ale czułem, że było to trochę skomplikowane dla mojego problemu i może być bardziej dostosowane do intensywnych aplikacji, w których przekazywanie danych do scentralizowanej bazy danych jest drogim luksusem. Ale na pewno będę pamiętać o tym podejściu.

W takim przypadku aplikacja będzie miała kilku współbieżnych klientów, a ja postanowiłem polegać na bazie danych. Najbardziej zmieniające się modele są przechowywane w Redis, które, jak ufam, obsługuje kilkaset aktualizacji na sekundę.

Philippe Durix
źródło