web-dev-qa-db-fra.com

Dans quelle mesure Meteor peut-il être efficace tout en partageant une énorme collection entre de nombreux clients?

Imaginez le cas suivant:

  • 1 000 clients sont connectés à une page Meteor affichant le contenu de la collection "Somestuff".

  • "Somestuff" est une collection contenant 1 000 articles.

  • Quelqu'un insère un nouvel élément dans la collection "Somestuff"

Que se passera-t-il:

  • Tout Meteor.Collections sur les clients seront mis à jour, c'est-à-dire l'insertion transmise à chacun d'eux (ce qui signifie qu'un message d'insertion envoyé à 1 000 clients)

Quel est le coût en termes de CPU pour le serveur pour déterminer quel client doit être mis à jour?

Est-il exact que seule la valeur insérée sera transmise aux clients, et non la liste entière?

Comment ça marche dans la vraie vie? Existe-t-il des repères ou des expériences d'une telle ampleur?

100
Flavien Volken

La réponse courte est que seules les nouvelles données sont envoyées sur le fil. Voici comment ça fonctionne.

Il existe trois parties importantes du serveur Meteor qui gèrent les abonnements: la fonction - publier, qui définit la logique des données fournies par l'abonnement; le pilote Mongo, qui surveille la base de données pour les changements; et la zone de fusion, qui combine tous les abonnements actifs d'un client et les envoie sur le réseau au client.

Fonctions de publication

Chaque fois qu'un client Meteor s'abonne à une collection, le serveur exécute une fonction - publier. Le travail de la fonction de publication consiste à déterminer l'ensemble des documents que son client doit avoir et à envoyer chaque propriété de document dans la zone de fusion. Il s'exécute une fois pour chaque nouveau client abonné. Vous pouvez mettre tout JavaScript que vous souhaitez dans la fonction de publication, tel qu'un contrôle d'accès arbitrairement complexe en utilisant this.userId. La fonction de publication envoie des données dans la zone de fusion en appelant this.added, this.changed Et this.removed. Voir documentation de publication complète pour plus de détails.

Cependant, la plupart des fonctions de publication n'ont pas besoin de contourner les API added, changed et removed de bas niveau. Si une fonction de publication renvoie un curseur Mongo, le serveur Meteor connecte automatiquement la sortie du pilote Mongo (callbacks insert, update et removed) à l'entrée de la fusion (this.added, this.changed et this.removed). Il est assez simple de pouvoir effectuer toutes les vérifications des autorisations à l'avance dans une fonction de publication, puis de connecter directement le pilote de base de données à la zone de fusion sans aucun code utilisateur. Et lorsque la publication automatique est activée, même ce petit morceau est caché: le serveur configure automatiquement une requête pour tous les documents de chaque collection et les pousse dans la zone de fusion.

D'un autre côté, vous n'êtes pas limité à la publication de requêtes de base de données. Par exemple, vous pouvez écrire une fonction de publication qui lit une position GPS à partir d'un appareil à l'intérieur d'un Meteor.setInterval, Ou interroge une API héritée REST d'un autre service Web. Dans ces cas, vous émettrez des modifications dans la zone de fusion en appelant l'API DDP added, changed et removed de bas niveau.

Le pilote Mongo

Le travail du pilote Mongo consiste à surveiller la base de données Mongo pour les modifications apportées aux requêtes en direct. Ces requêtes s'exécutent en continu et renvoient des mises à jour à mesure que les résultats changent en appelant les rappels added, removed et changed.

Mongo n'est pas une base de données en temps réel. Le chauffeur interroge donc. Il conserve une copie en mémoire du résultat de la dernière requête pour chaque requête active active. À chaque cycle d'interrogation, il compare le nouveau résultat avec le résultat enregistré précédent, en calculant l'ensemble minimal d'événements added, removed et changed qui décrivent la différence. Si plusieurs appelants enregistrent des rappels pour la même requête en direct, le pilote ne regarde qu'une seule copie de la requête, appelant chaque rappel enregistré avec le même résultat.

Chaque fois que le serveur met à jour une collection, le pilote recalcule chaque requête en direct sur cette collection (les futures versions de Meteor exposeront une API de mise à l'échelle pour limiter les requêtes en direct recalculées lors de la mise à jour.) Le pilote interroge également chaque requête en direct sur une minuterie de 10 secondes pour attraper les mises à jour de la base de données hors bande qui ont contourné le serveur Meteor.

La boîte de fusion

Le travail de la zone de fusion consiste à combiner les résultats (added, changed et removed appels) de toutes les publications actives d'un client fonctionne en un seul flux de données. Il existe une boîte de fusion pour chaque client connecté. Il contient une copie complète du cache minimongo du client.

Dans votre exemple avec juste un seul abonnement, la zone de fusion est essentiellement un pass-through. Mais une application plus complexe peut avoir plusieurs abonnements qui peuvent se chevaucher. Si deux abonnements définissent tous deux le même attribut sur le même document, la zone de fusion décide quelle valeur est prioritaire et l'envoie uniquement au client. Nous n'avons pas encore exposé l'API pour définir la priorité d'abonnement. Pour l'instant, la priorité est déterminée par l'ordre d'abonnement des clients aux ensembles de données. Le premier abonnement effectué par un client a la priorité la plus élevée, le deuxième abonnement est ensuite le plus élevé, etc.

Étant donné que la zone de fusion contient l'état du client, elle peut envoyer le minimum de données pour maintenir chaque client à jour, quelle que soit la fonction de publication qui l'alimente.

Que se passe-t-il lors d'une mise à jour

Alors maintenant, nous avons préparé le terrain pour votre scénario.

Nous avons 1 000 clients connectés. Chacun est abonné à la même requête Mongo en direct (Somestuff.find({})). Étant donné que la requête est la même pour chaque client, le pilote exécute uniquement une requête en direct. Il y a 1 000 zones de fusion actives. Et la fonction de publication de chaque client a enregistré un added, changed et removed sur cette requête en direct qui alimente l'une des zones de fusion. Rien d'autre n'est connecté aux zones de fusion.

D'abord le pilote Mongo. Lorsqu'un des clients insère un nouveau document dans Somestuff, il déclenche un recalcul. Le pilote Mongo réexécute la requête pour tous les documents dans Somestuff, compare le résultat au résultat précédent en mémoire, trouve qu'il y a un nouveau document et appelle chacun des 1 000 rappels insert enregistrés.

Ensuite, les fonctions de publication. Il se passe très peu de choses ici: chacun des 1 000 rappels insert pousse les données dans la zone de fusion en appelant added.

Enfin, chaque boîte de fusion vérifie ces nouveaux attributs par rapport à sa copie en mémoire du cache de son client. Dans chaque cas, il constate que les valeurs ne sont pas encore sur le client et ne masquent pas une valeur existante. Ainsi, la zone de fusion émet un message DDP DATA sur la connexion SockJS à son client et met à jour sa copie en mémoire côté serveur.

Le coût total du processeur est le coût de la différence d'une requête Mongo, plus le coût de 1000 boîtes de fusion vérifiant l'état de leurs clients et construisant une nouvelle charge utile de message DDP. Les seules données qui circulent sur le câble sont un seul objet JSON envoyé à chacun des 1 000 clients, correspondant au nouveau document dans la base de données, plus un message RPC vers le serveur du client qui fait l'insert d'origine.

Optimisations

Voici ce que nous avons définitivement prévu.

  • Pilote Mongo plus efficace. Nous optimisé le pilote en 0.5.1 pour exécuter un seul observateur par requête distincte.

  • Chaque changement de base de données ne doit pas déclencher un recalcul d'une requête. Nous pouvons apporter des améliorations automatisées, mais la meilleure approche est une API qui permet au développeur de spécifier les requêtes à réexécuter. Par exemple, il est évident pour un développeur que l'insertion d'un message dans un salon de discussion ne doit pas invalider une requête en direct pour les messages dans une deuxième salle.

  • Le pilote Mongo, la fonction de publication et la zone de fusion n'ont pas besoin de s'exécuter dans le même processus, ni même sur la même machine. Certaines applications exécutent des requêtes en direct complexes et nécessitent plus de CPU pour regarder la base de données. D'autres n'ont que quelques requêtes distinctes (imaginez un moteur de blog), mais probablement de nombreux clients connectés - ceux-ci ont besoin de plus de CPU pour les boîtes de fusion. La séparation de ces composants nous permettra de mettre à l'échelle chaque pièce indépendamment.

  • De nombreuses bases de données prennent en charge les déclencheurs qui se déclenchent lorsqu'une ligne est mise à jour et fournissent les anciennes et les nouvelles lignes. Avec cette fonctionnalité, un pilote de base de données pourrait enregistrer un déclencheur au lieu d'interroger les modifications.

119
debergalis

D'après mon expérience, utiliser de nombreux clients avec tout en partageant une énorme collection dans Meteor est essentiellement impossible, à partir de la version 0.7.0.1. Je vais essayer d'expliquer pourquoi.

Comme décrit dans l'article ci-dessus et également dans https://github.com/meteor/meteor/issues/1821 , le serveur de météores doit conserver une copie des données publiées pour chaque client dans le zone de fusion . C'est ce qui permet à la magie Meteor de se produire, mais entraîne également la conservation répétée de grandes bases de données partagées dans la mémoire du processus de nœud. Même lorsque vous utilisez une optimisation possible pour les collections statiques comme dans (Existe-t-il un moyen de dire à météore qu'une collection est statique (ne changera jamais)?), nous avons rencontré un énorme problème avec l'utilisation du CPU et de la mémoire du processus Node.

Dans notre cas, nous publions une collection de 15k documents pour chaque client qui était complètement statique. Le problème est que la copie de ces documents dans la boîte de fusion d'un client (en mémoire) lors de la connexion a essentiellement amené le processus Node à 100% du processeur pendant près d'une seconde, et a entraîné une utilisation supplémentaire importante de la mémoire . Ceci est intrinsèquement non évolutif, car tout client se connectant mettra le serveur à genoux (et les connexions simultanées se bloqueront) et l'utilisation de la mémoire augmentera de manière linéaire dans le nombre de clients. Dans notre cas, chaque client a causé un supplément ~ 60 Mo d'utilisation de la mémoire, même si les données brutes transférées n'étaient que d'environ 5 Mo.

Dans notre cas, la collection étant statique, nous avons résolu ce problème en envoyant tous les documents sous la forme d'un .json fichier, qui a été compressé par nginx, et les charger dans une collection anonyme, résultant en seulement un transfert de données ~ 1 Mo sans CPU ou mémoire supplémentaire dans le processus de nœud et un temps de chargement beaucoup plus rapide. Toutes les opérations sur cette collection ont été effectuées à l'aide de _ids de publications beaucoup plus petites sur le serveur, permettant de conserver la plupart des avantages de Meteor. Cela a permis à l'application de s'adapter à de nombreux autres clients. De plus, comme notre application est principalement en lecture seule, nous avons encore amélioré l'évolutivité en exécutant plusieurs instances Meteor derrière nginx avec équilibrage de charge (mais avec un seul Mongo), car chaque Node est unique -filé.

Cependant, le problème du partage de grandes collections accessibles en écriture entre plusieurs clients est un problème d'ingénierie qui doit être résolu par Meteor. Il existe probablement un meilleur moyen que de conserver une copie de tout pour chaque client, mais cela nécessite une réflexion sérieuse en tant que problème de systèmes distribués. Les problèmes actuels d'utilisation massive du processeur et de la mémoire ne vont tout simplement pas évoluer.

29
Andrew Mao

L'expérience que vous pouvez utiliser pour répondre à cette question:

  1. Installez un météore de test: meteor create --example todos
  2. Exécutez-le sous Webkit inspector (WKI).
  3. Examinez le contenu des messages XHR se déplaçant sur le fil.
  4. Observez que la collection entière n'est pas déplacée sur le fil.

Pour des conseils sur l'utilisation de WKI, consultez ceci article . C'est un peu dépassé, mais surtout toujours valable, surtout pour cette question.

4
javajosh

Cela fait encore un an maintenant et donc je pense que les connaissances pré-"Meteor 1.0", donc les choses peuvent avoir changé à nouveau? J'étudie toujours ça. http://meteorhacks.com/does-meteor-scale.html conduit à un "Comment mettre à l'échelle Meteor?" article http://meteorhacks.com/how-to-scale-meteor.html

3
MistereeDevlord