Comment l'architecture d'une application Web basée sur les Websockets lourds en temps réel?

17

Dans le processus de développement d'une application de page unique en temps réel, j'ai progressivement adopté des Websockets pour donner à mes utilisateurs des données à jour. Au cours de cette phase, j'ai été triste de constater que je détruisais beaucoup trop de la structure de mon application, et je n'ai pas trouvé de solution à ce phénomène.

Avant d'entrer dans les détails, juste un peu de contexte:

  • La webapp est un SPA en temps réel;
  • Le backend est en Ruby on Rails. Les événements en temps réel sont poussés par Ruby vers une clé Redis, puis un serveur de micro-nœuds les récupère et les pousse vers Socket.Io;
  • Le Frontend est dans AngularJS et se connecte directement au serveur socket.io dans Node.

Côté serveur, avant le temps réel, j'avais une séparation claire des contrôleurs / modèles basée sur les modèles, avec un traitement attaché à chacune. Cette conception MVC classique a été complètement déchiquetée, ou du moins contournée, juste au moment où j'ai commencé à envoyer des trucs via des sockets Web à mes utilisateurs. J'ai maintenant un seul canal où toute mon application descend des données plus ou moins structurées . Et je trouve ça stressant.

En première ligne, la principale préoccupation est la duplication de la logique métier. Lorsque l'utilisateur charge la page, je dois charger mes modèles via les appels AJAX classiques. Mais je dois également gérer l'afflux de données en temps réel et je me retrouve à dupliquer une grande partie de ma logique métier côté client pour maintenir la cohérence de mes modèles côté client.

Après quelques recherches, je ne trouve pas de bons articles, articles, livres ou quoi que ce soit qui donnerait des conseils sur la façon dont on peut et devrait concevoir l'architecture d'une webapp moderne avec quelques sujets spécifiques à l'esprit:

  • Comment structurer les données envoyées du serveur à l'utilisateur?
    • Dois-je envoyer uniquement des événements comme «cette ressource a été mise à jour et vous devez la recharger via un appel AJAX» ou pousser les données mises à jour et remplacer les données précédentes chargées via les appels AJAX initiaux?
    • Comment définir un squelette cohérent et évolutif aux données envoyées? est-ce un message de mise à jour du modèle ou "il y a eu une erreur avec blahblahblah"
  • Comment ne pas envoyer de données sur tout depuis n'importe où dans le backend?
  • Comment réduire la duplication de la logique métier à la fois côté serveur et côté client?
Philippe Durix
la source
Êtes-vous sûr que Rails est la meilleure option pour votre SPA? Rails est génial, mais il vise l'application monolithique ... vous voudrez peut-être un backend modulaire avec séparation des préoccupations ... En fonction de vos besoins, j'envisagerais d'autres cadres Ruby pour un SPA en temps réel.
Myst
1
Je ne suis pas sûr que Rails soit la meilleure option, mais je suis très satisfait de la pile en place au moins sur le backend (probablement parce que je suis bon avec ce cadre particulier). Ma préoccupation ici est plus sur la façon de concevoir le SPA d'un point de vue technique agnostique. Et je ne veux pas non plus multiplier le nombre de langages, de frameworks et de bibliothèques dans un seul projet.
Philippe Durix
Le benchmark lié peut avoir des problèmes, mais il révèle une faiblesse dans l'implémentation actuelle d'ActiveRecord. Je peux être partisan de plezi.io , mais comme indiqué dans les résultats ultérieurs du test, il fournit des améliorations de performances significatives, avant même le clustering et Redis (qui n'ont pas été testés). Je pense qu'il fonctionnait mieux que node.js ... Jusqu'à ce que les choses changent, j'utiliserais plezi.io.
Myst

Réponses:

10

Comment structurer les données envoyées du serveur à l'utilisateur?

Utilisez le modèle de messagerie . Eh bien, vous utilisez déjà un protocole de messagerie, mais je veux dire structurer les changements sous forme de messages ... spécifiquement des événements. Lorsque le côté serveur change, cela se traduit par des événements commerciaux. Dans votre scénario, les opinions de vos clients sont intéressées par ces événements. Les événements doivent contenir toutes les données pertinentes pour ce changement (pas nécessairement toutes les données de vue). La page client doit ensuite mettre à jour les parties de la vue qu'elle gère avec les données d'événement.

Par exemple, si vous mettiez à jour un symbole boursier et que l'AAPL changeait, vous ne voudriez pas baisser tous les cours des actions ou même toutes les données sur l'AAPL (nom, description, etc.). Vous ne feriez que pousser AAPL, le delta et le nouveau prix. Sur le client, vous ne mettriez alors à jour que le cours de l'action sur la vue.

Dois-je envoyer uniquement des événements comme «cette ressource a été mise à jour et vous devez la recharger via un appel AJAX» ou pousser les données mises à jour et remplacer les données précédentes chargées via les appels AJAX initiaux?

Je ne dirais ni l'un ni l'autre. Si vous envoyez l'événement, allez-y et envoyez des données pertinentes avec lui (pas les données de l'objet entier). Donnez-lui un nom pour le type d'événement qu'il s'agit. (La dénomination et les données pertinentes pour cet événement dépassent le cadre du fonctionnement mécanique du système. Cela a davantage à voir avec la façon dont la logique métier est modélisée.) Vos mises à jour de vues doivent savoir comment traduire chaque événement spécifique en un changement de vue précis (c'est-à-dire uniquement mettre à jour ce qui a changé).

Comment définir un squelette cohérent et évolutif aux données envoyées? est-ce un message de mise à jour du modèle ou "il y a eu une erreur avec blahblahblah"

Je dirais que c'est une grande question ouverte qui devrait être divisée en plusieurs autres questions et publiée séparément.

En général cependant, votre système dorsal doit créer et envoyer des événements pour les événements importants pour votre entreprise. Ceux-ci pourraient provenir de flux externes ou d'une activité dans le back-end lui-même.

Comment ne pas envoyer de données sur tout depuis n'importe où dans le backend?

Utilisez le modèle de publication / abonnement . Lorsque votre SPA charge une nouvelle page qui souhaite recevoir des mises à jour en temps réel, la page ne doit s'abonner qu'aux événements qu'il peut utiliser et appeler la logique de mise à jour de la vue au fur et à mesure que ces événements arrivent. Vous aurez probablement besoin d'une logique pub / sub activée le serveur pour réduire la charge du réseau. Il existe des bibliothèques pour Websocket pub / sub, mais je ne suis pas sûr de ce qu'elles sont dans l'écosystème Rails.

Comment réduire la duplication de la logique métier à la fois côté serveur et côté client?

Il semble que vous deviez mettre à jour les données de vue sur le client et le serveur. Je suppose que vous avez besoin des données de vue côté serveur afin d'avoir un instantané pour démarrer le client en temps réel. Étant donné qu'il y a deux langues / plates-formes impliquées (Ruby et Javascript), la logique de mise à jour de la vue devra être écrite dans les deux. Mis à part le transpiling (qui a ses propres problèmes), je ne vois pas de solution.

Point technique: la manipulation des données (voir la mise à jour) n'est pas une logique métier. Si vous parlez de validation de cas d'utilisation, cela semble inévitable, car les validations du client sont nécessaires pour une bonne expérience utilisateur, mais ne peuvent finalement pas être approuvées par le serveur.


Voici comment je vois une telle chose bien structurée.

Vues du client:

  • Demande un instantané de la vue et le dernier numéro d'événement vu de la vue
    • Cela préremplira la vue afin que le client n'ait pas à créer à partir de zéro.
    • Pourrait être sur HTTP GET pour plus de simplicité
  • Établit une connexion Websocket et s'abonne à des événements spécifiques, à partir du dernier numéro d'événement de la vue.
  • Reçoit les événements via websocket et met à jour sa vue en fonction du type / des données d'événement.

Commandes client:

  • Demande de modification des données (HTTP PUT / POST / DELETE)
    • La réponse est seulement un succès ou un échec + erreur
    • (Les événements générés par la modification surviendront via websocket et déclencheront une mise à jour de la vue.)

Le côté serveur pourrait en fait être divisé en plusieurs composants avec des responsabilités limitées. Celui qui traite simplement les demandes entrantes et crée des événements. Un autre pourrait gérer les abonnements des clients, écouter les événements (par exemple en cours) et transmettre les événements appropriés aux abonnés. Vous pouvez avoir un tiers qui écoute les événements et met à jour les vues côté serveur - cela peut même arriver avant que les abonnés ne reçoivent les événements.

Ce que j'ai décrit est une forme de messagerie CQRS + et une stratégie typique pour résoudre le type de problèmes auxquels vous êtes confronté.

Je n'ai pas apporté Event Sourcing dans cette description car je ne sais pas si c'est quelque chose que vous voulez entreprendre ou si vous en avez nécessairement besoin. Mais c'est un schéma connexe.

Kasey Speakman
la source
J'ai beaucoup progressé sur le sujet et les indications que vous avez données ont été très utiles. J'ai accepté la réponse parce que j'ai utilisé bon nombre des conseils, même si je ne les ai pas tous utilisés. Je vais décrire le chemin que j'ai suivi dans une autre réponse.
Philippe Durix
4

Après quelques mois de travail sur le backend principalement, j'ai pu utiliser certains des conseils ici pour résoudre les problèmes auxquels la plateforme était confrontée.

L'objectif principal en repensant le backend était de coller aussi dur que possible au CRUD. Toutes les actions, messages et demandes dispersés sur de nombreux itinéraires ont été regroupés en ressources créées, mises à jour, lues ou supprimées . Cela semble évident maintenant, mais cela a été une façon très difficile de penser à appliquer soigneusement.

Une fois que tout a été organisé en ressources, j'ai pu joindre des messages en temps réel aux modèles.

  • La création déclenche un message avec une nouvelle ressource de trou;
  • La mise à jour déclenche un message avec uniquement les attributs mis à jour (plus l'UUID);
  • La suppression déclenche un message de suppression.

Sur l'API Rest, toutes les méthodes de création, de mise à jour et de suppression génèrent une réponse en tête uniquement, le code HTTP informant de la réussite ou de l'échec et les données réelles étant transmises via des sockets Web.

Sur le front-end, chaque ressource est gérée par un composant spécifique qui la charge via HTTP lors de l'initialisation, puis s'abonne aux mises à jour et conserve son état dans le temps. Les vues se lient ensuite à ces composants pour afficher les ressources et effectuer des actions sur ces ressources via les mêmes composants.


J'ai trouvé la lecture CQRS + Messaging and Event Sourcing très intéressante, mais je pensais qu'elle était un peu trop compliquée pour mon problème et qu'elle était peut-être plus adaptée aux applications intensives où la saisie de données dans une base de données centralisée est un luxe coûteux. Mais je garderai certainement à l'esprit cette approche.

Dans ce cas, l'application aura peu de clients simultanés et j'ai pris le parti de beaucoup compter sur la base de données. Les modèles les plus changeants sont stockés dans Redis en qui je fais confiance pour gérer quelques centaines de mises à jour par seconde.

Philippe Durix
la source