Wie kann eine echtzeitlastige, auf Websockets basierende Webanwendung aufgebaut werden?

17

Bei der Entwicklung einer Echtzeit-Einzelseitenanwendung habe ich nach und nach Web-Sockets eingeführt, um meine Benutzer mit aktuellen Daten zu versorgen. Während dieser Phase war ich traurig zu bemerken, dass ich viel zu viel von meiner App-Struktur zerstört habe, und es gelang mir nicht, eine Lösung für dieses Phänomen zu finden.

Bevor wir uns näher mit den Einzelheiten befassen:

  • Die Webapp ist ein Echtzeit-SPA;
  • Das Backend ist in Ruby on Rails. Echtzeitereignisse werden von Ruby auf einen Redis-Schlüssel übertragen, dann zieht ein Mikroknotenserver diesen zurück und überträgt ihn an Socket.Io.
  • Das Frontend ist in AngularJS und verbindet sich direkt mit dem socket.io-Server in Node.

Auf der Serverseite hatte ich vor der Echtzeit eine klare, auf Controllern und Modellen basierende Trennung der Ressourcen, wobei die Verarbeitung mit jeder Ressource verbunden war. Dieses klassische MVC-Design wurde vollständig zerkleinert oder zumindest umgangen, als ich anfing, Inhalte über Websockets an meine Benutzer zu senden. Ich habe jetzt eine einzige Pipe, in der meine gesamte App mehr oder weniger strukturierte Daten herunterfließt . Und ich finde es stressig.

Das Hauptanliegen im Frontend ist die Verdoppelung der Geschäftslogik. Wenn der Benutzer die Seite lädt, muss ich meine Modelle über klassische AJAX-Aufrufe laden. Ich muss mich aber auch um das Überfluten von Echtzeitdaten kümmern und finde mich dabei, einen Großteil meiner clientseitigen Geschäftslogik zu duplizieren, um die Konsistenz meiner clientseitigen Modelle zu gewährleisten.

Nach einigen Recherchen kann ich keine guten Beiträge, Artikel, Bücher oder was auch immer finden, die Ratschläge darüber geben, wie man die Architektur einer modernen Webapp unter Berücksichtigung einiger spezifischer Themen gestalten kann und sollte:

  • Wie strukturiere ich die Daten, die vom Server an den Benutzer gesendet werden?
    • Soll ich nur Ereignisse wie "Diese Ressource wurde aktualisiert und Sie sollten sie über einen AJAX-Aufruf neu laden" senden oder die aktualisierten Daten übertragen und frühere Daten ersetzen, die über erste AJAX-Aufrufe geladen wurden?
    • Wie definiere ich ein kohärentes und skalierbares Gerüst für gesendete Daten? Handelt es sich um eine Modellaktualisierungsmeldung oder um eine Meldung, dass ein Fehler mit blahblahblah aufgetreten ist?
  • Wie kann man nicht von überall im Backend Daten über alles senden?
  • Wie kann die Duplizierung der Geschäftslogik sowohl auf der Server- als auch auf der Clientseite reduziert werden?
Philippe Durix
quelle
Sind Sie sicher, dass Rails die beste Option für Ihr SPA ist? Rails ist großartig, aber es zielt auf die Monolith-Anwendung ab. Vielleicht möchten Sie ein modulares Backend mit Problemtrennung. Abhängig von Ihren Anforderungen würde ich alternative Ruby-Frameworks für ein Echtzeit-SPA in Betracht ziehen.
Myst
1
Ich bin nicht sicher, ob Rails die beste Option ist, aber ich bin sehr zufrieden mit dem Stack, der zumindest im Backend vorhanden ist (wahrscheinlich, weil ich mit diesem speziellen Framework gut umgehen kann). Mein Anliegen ist hier eher, wie man das SPA unter einem technisch agnostischen Gesichtspunkt gestaltet. Und ich möchte auch nicht die Anzahl der Sprachen, Frameworks und Bibliotheken in einem Projekt multiplizieren.
Philippe Durix
Der verknüpfte Benchmark hat möglicherweise Probleme, zeigt jedoch eine Schwachstelle in der aktuellen Implementierung von ActiveRecord. Ich bin vielleicht voreingenommen in Bezug auf plezi.io , aber wie in den späteren Ergebnissen des Benchmarks hervorgehoben , bietet es signifikante Leistungsverbesserungen, sogar vor dem Clustering und Redis (die nicht getestet wurden). Ich denke, es lief besser als node.js ... Bis sich die Dinge ändern, würde ich plezi.io verwenden.
Myst

Antworten:

10

Wie strukturiere ich die Daten, die vom Server an den Benutzer gesendet werden?

Verwenden Sie das Nachrichtenmuster . Nun, Sie verwenden bereits ein Nachrichtenprotokoll, aber ich meine, strukturieren Sie die Änderungen als Nachrichten ... speziell als Ereignisse. Wenn sich die Serverseite ändert, führt dies zu Geschäftsereignissen. In Ihrem Szenario interessieren sich Ihre Kundenansichten für diese Ereignisse. Die Ereignisse sollten alle Daten enthalten, die für diese Änderung relevant sind (nicht unbedingt alle Ansichtsdaten). Die Client-Seite sollte dann die von ihr verwalteten Teile der Ansicht mit den Ereignisdaten aktualisieren.

Wenn Sie beispielsweise einen Börsenticker aktualisieren und AAPL ändern, möchten Sie nicht alle Aktienkurse oder sogar alle Daten zu AAPL (Name, Beschreibung usw.) nach unten drücken. Sie würden nur AAPL, das Delta und den neuen Preis drücken. Auf dem Client würden Sie dann nur diesen Aktienkurs in der Ansicht aktualisieren.

Soll ich nur Ereignisse wie "Diese Ressource wurde aktualisiert und Sie sollten sie über einen AJAX-Aufruf neu laden" senden oder die aktualisierten Daten übertragen und frühere Daten ersetzen, die über erste AJAX-Aufrufe geladen wurden?

Ich würde auch nicht sagen. Wenn Sie das Ereignis senden, senden Sie relevante Daten (nicht die Daten des gesamten Objekts). Geben Sie ihm einen Namen für die Art des Ereignisses. (Die Benennung und die für dieses Ereignis relevanten Daten liegen außerhalb des Bereichs der mechanischen Funktionsweise des Systems. Dies hat mehr mit der Modellierung der Geschäftslogik zu tun.) Ihre Ansichtsaktualisierer müssen wissen, wie sie die einzelnen Ereignisse in das jeweilige Ereignis übersetzen eine genaue Ansichtsänderung (dh nur aktualisieren, was sich geändert hat).

Wie definiere ich ein kohärentes und skalierbares Gerüst für gesendete Daten? Handelt es sich um eine Modellaktualisierungsmeldung oder um eine Meldung, dass ein Fehler mit blahblahblah aufgetreten ist?

Ich würde sagen, dies ist eine große, offene Frage, die in mehrere andere Fragen unterteilt und separat gestellt werden sollte.

Im Allgemeinen sollte Ihr Back-End-System Ereignisse für wichtige Ereignisse in Ihrem Unternehmen erstellen und auslösen. Diese können von externen Feeds oder von Aktivitäten im Back-End selbst stammen.

Wie kann man nicht von überall im Backend Daten über alles senden?

Verwenden Sie das Publish / Subscribe-Muster . Wenn Ihr SPA eine neue Seite lädt, die Echtzeitaktualisierungen erhalten möchte, sollte die Seite nur die Ereignisse abonnieren, die sie verwenden kann, und die Aktualisierungslogik für die Ansicht aufrufen, sobald diese Ereignisse eintreten. Möglicherweise muss die Pub / Sub-Logik aktiviert sein der Server, um die Netzwerklast zu reduzieren. Es gibt Bibliotheken für Websocket Pub / Sub, aber ich bin mir nicht sicher, was diese im Rails-Ökosystem sind.

Wie kann die Duplizierung der Geschäftslogik sowohl auf der Server- als auch auf der Clientseite reduziert werden?

Es hört sich so an, als müssten Sie die Ansichtsdaten sowohl auf dem Client als auch auf dem Server aktualisieren. Vermutlich benötigen Sie die serverseitigen Ansichtsdaten, damit Sie einen Schnappschuss haben, um den Echtzeit-Client zu starten. Da es sich um zwei Sprachen / Plattformen handelt (Ruby und Javascript), muss die Ansichtsaktualisierungslogik in beiden Sprachen geschrieben werden. Abgesehen von der Transpilation (die ihre eigenen Probleme hat) sehe ich keinen Ausweg.

Technischer Punkt: Datenmanipulation (Ansichtsaktualisierung) ist keine Geschäftslogik. Wenn Sie Use-Case-Validierung meinen, dann scheint dies unvermeidbar, da die Validierungen des Clients für eine gute Benutzererfahrung erforderlich sind, aber letztendlich vom Server nicht als vertrauenswürdig eingestuft werden können.


So sehe ich so etwas gut strukturiert.

Kundenansichten:

  • Fordert einen Ansichtsschnappschuss und die Nummer des zuletzt gesehenen Ereignisses der Ansicht an
    • Dadurch wird die Ansicht vorab ausgefüllt, sodass der Client nicht von Grund auf neu erstellen muss.
    • Könnte der Einfachheit halber über HTTP GET erfolgen
  • Stellt eine Websocket-Verbindung her und abonniert bestimmte Ereignisse, beginnend mit der letzten Ereignisnummer der Ansicht.
  • Empfängt Ereignisse über das Websocket und aktualisiert seine Ansicht basierend auf Ereignistyp / -daten.

Client-Befehle:

  • Datenänderung anfordern (HTTP PUT / POST / DELETE)
    • Antwort ist nur Erfolg oder Misserfolg + Fehler
    • (Die durch die Änderung erzeugten Ereignisse werden über den Websocket übertragen und lösen eine Ansichtsaktualisierung aus.)

Die Serverseite könnte tatsächlich in mehrere Komponenten mit begrenzten Verantwortlichkeiten aufgeteilt werden. Eine, die nur die eingehenden Anforderungen verarbeitet und Ereignisse erstellt. Ein anderer kann Client-Abonnements verwalten, auf Ereignisse warten (etwa in Bearbeitung) und entsprechende Ereignisse an Abonnenten weiterleiten. Sie könnten ein Drittel haben, das Ereignisse abhört und serverseitige Ansichten aktualisiert - möglicherweise geschieht dies sogar, bevor Abonnenten die Ereignisse empfangen.

Was ich beschrieben habe, ist eine Form von CQRS + Messaging und eine typische Strategie zur Lösung der Probleme, mit denen Sie konfrontiert sind.

Ich habe Event Sourcing nicht in diese Beschreibung aufgenommen, da ich nicht sicher bin, ob Sie es übernehmen möchten oder ob Sie es unbedingt brauchen. Aber es ist ein ähnliches Muster.

Kasey Speakman
quelle
Ich habe bei diesem Thema große Fortschritte gemacht, und die von Ihnen gegebenen Hinweise waren sehr nützlich. Ich habe die Antwort akzeptiert, weil ich viele der Ratschläge verwendet habe, auch wenn ich nicht alle verwendet habe. Ich werde den Weg, den ich gegangen bin, in einer anderen Antwort beschreiben.
Philippe Durix
4

Nach einigen Monaten der Arbeit hauptsächlich am Backend konnte ich einige der hier enthaltenen Ratschläge verwenden, um die Probleme zu lösen, mit denen die Plattform konfrontiert war.

Das Hauptziel beim Überdenken des Backends war es, so hart wie möglich an CRUD festzuhalten. Alle auf vielen Routen verteilten Aktionen, Nachrichten und Anforderungen wurden in Ressourcen zusammengefasst, die erstellt, aktualisiert, gelesen oder gelöscht wurden . Es klingt jetzt offensichtlich, aber es war sehr schwierig, dies sorgfältig zu überlegen.

Nachdem alles in Ressourcen organisiert wurde, konnte ich Echtzeitnachrichten an Modelle anhängen.

  • Die Erstellung löst eine Nachricht mit einer neuen Ressource aus.
  • Update löst eine Nachricht mit nur den aktualisierten Attributen (plus der UUID) aus;
  • Das Löschen löst eine Löschnachricht aus.

In der Rest-API generieren alle Methoden zum Erstellen, Aktualisieren und Löschen eine Nur-Kopf-Antwort, wobei der HTTP-Code über den Erfolg oder Misserfolg und die tatsächlichen Daten informiert, die über Websockets übertragen werden.

Am Front-End werden alle Ressourcen von einer bestimmten Komponente verwaltet, die sie bei der Initialisierung über HTTP lädt, dann Updates abonniert und ihren Status über die Zeit beibehält. Sichten werden dann an diese Komponenten gebunden, um Ressourcen anzuzeigen und Aktionen für diese Ressourcen über dieselben Komponenten auszuführen.


Ich fand die CQRS + Messaging- und Event-Sourcing-Lektüre sehr interessant, fand sie jedoch für mein Problem etwas überkompliziert und ist möglicherweise eher für intensive Anwendungen geeignet, bei denen das Übertragen von Daten in eine zentralisierte Datenbank ein teurer Luxus ist. Aber ich werde diesen Ansatz auf jeden Fall im Hinterkopf behalten.

In diesem Fall hat die App nur wenige gleichzeitige Clients, und ich habe mich viel auf die Datenbank verlassen. Die sich am meisten ändernden Modelle werden in Redis gespeichert. Ich vertraue darauf, dass sie ein paar hundert Updates pro Sekunde verarbeiten.

Philippe Durix
quelle