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

100

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 objets.

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

Que va-t-il se passer:

  • Tous Meteor.Collectionles clients seront mis à jour, c'est-à-dire que l'insertion sera transmise à tous (ce qui signifie qu'un message d'insertion sera envoyé à 1000 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 cela fonctionne-t-il dans la vraie vie? Existe-t-il des repères ou des expériences d'une telle échelle?

Flavien Volken
la source

Réponses:

119

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

Trois parties importantes du serveur Meteor gèrent les abonnements: la fonction de publication , 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 boîte de fusion , qui combine tous les abonnements actifs d'un client et les envoie sur le réseau au client.

Publier des fonctions

Chaque fois qu'un client Meteor s'abonne à une collection, le serveur exécute une fonction de publication . Le travail de la fonction de publication est de déterminer l'ensemble de documents que son client doit avoir et d'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 n'importe quel JavaScript de votre choix dans la fonction de publication, comme un contrôle d'accès arbitrairement complexe à l'aide de this.userId. La fonction de publication envoie des données dans la zone de fusion en appelant this.added, this.changedet this.removed. Consultez la documentation de publication complète pour plus de détails.

La plupart des fonctions publication ne sont pas muck autour avec le bas niveau added, changedet l' removedAPI, cependant. Si une fonction retourne publier un curseur mongo, le serveur Meteor se connecte automatiquement la sortie du pilote Mongo ( insert, updateet removedcallbacks) à l'entrée de la zone de fusion ( this.added, this.changedet this.removed). Il est assez intéressant de pouvoir effectuer toutes les vérifications d'autorisation à l'avance dans une fonction de publication, puis de connecter directement le pilote de base de données à la boîte de fusion sans aucun code utilisateur. Et lorsque la publication automatique est activée, même ce petit élément est masqué: le serveur configure automatiquement une requête pour tous les documents de chaque collection et les pousse dans la zone de fusion.

En revanche, 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.setIntervalou interroge une API REST héritée à partir d'un autre service Web. Dans ces cas, vous auriez émettez des modifications à la boîte de fusion en appelant le bas niveau added, changedet removedAPI DDP.

Le chauffeur Mongo

Le travail du pilote Mongo est de surveiller la base de données Mongo pour les modifications apportées aux requêtes en direct. Ces requêtes fonctionnent en continu et de retour mises à jour que les résultats changent en appelant added, removedet changedcallbacks.

Mongo n'est pas une base de données en temps réel. Alors le chauffeur fait un sondage. Il conserve une copie en mémoire du dernier résultat de la requête pour chaque requête active active. A chaque cycle de vote, il compare le nouveau résultat avec le résultat enregistré précédent, le calcul du jeu minimum de added, removedet les changed événements 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 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 versions futures 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 un minuteur de 10 secondes pour attraper les mises à jour de base de données hors bande qui ont contourné le serveur Meteor.

La boîte de fusion

Le travail de la boîte de fusion est de combiner les résultats ( added, changedet removed appels) de toutes publier les fonctions actives d'un client 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 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 ne l'envoie qu'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 dans lequel le client souscrit aux ensembles de données. Le premier abonnement effectué par un client a la priorité la plus élevée, le deuxième abonnement est le suivant, et ainsi de suite.

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

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({})). La requête étant la même pour chaque client, le pilote n'exécute qu'une seule requête en direct. Il y a 1 000 boîtes de fusion actives. Et chacun de clients publient la fonction a enregistré une added, changedet removedsur cette requête en direct qui alimente l' une des boîtes de fusion. Rien d'autre n'est connecté aux boîtes 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 1000 insertrappels enregistrés .

Ensuite, les fonctions de publication. Il se passe très peu de choses ici: chacun des 1000 insertrappels 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 n'observent pas une valeur existante. Ainsi, la boîte de fusion émet un DATAmessage DDP sur la connexion SockJS à son client et met à jour sa copie en mémoire côté serveur.

Le coût total du processeur correspond au coût de diffraction 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 1000 clients, correspondant au nouveau document de la base de données, plus un message RPC au serveur du client qui a effectué l'insertion d'origine.

Optimisations

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

  • Pilote Mongo plus efficace. Nous avons optimisé le pilote dans 0.5.1 pour n'exécuter qu'un seul observateur par requête distincte.

  • Toutes les modifications de base de données ne doivent 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 d'une seconde 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 processeur pour surveiller la base de données. D'autres n'ont que quelques requêtes distinctes (imaginez un moteur de blog), mais peut-être de nombreux clients connectés - ceux-ci ont besoin de plus de CPU pour les boîtes de fusion. Séparer 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.

debergalis
la source
Existe-t-il un exemple d'utilisation de Meteor.publish pour publier des données sans curseur? Tels que les résultats d'une API de repos héritée mentionnée dans la réponse?
Tony
@Tony: C'est dans la documentation. Consultez l'exemple de comptage de pièces.
Mitar
Il est intéressant de noter que dans les versions 0.7, 0.7.1, 0.7.2 Meteor passe à OpLog Observez pilote pour les requêtes ( des exceptions sont skip, $nearet $wherecontenant des requêtes) qui est beaucoup plus efficace de la charge CPU, bande passante réseau et permet la montée en puissance l' application les serveurs.
imslavko
Et si tous les utilisateurs ne voient pas les mêmes données. 1. ils se sont abonnés à différents sujets .2. ils ont des rôles différents donc dans le même sujet principal, il y a quelques messages qui ne sont pas censés les atteindre.
tgkprog
@debergalis concernant l'invalidation du cache, peut-être que vous trouverez des idées de mon article vanisoft.pl/~lopuszanski/public/cache_invalidation.pdf qui en valent la peine
qbolec
29

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

Comme décrit dans le post ci-dessus et également sur https://github.com/meteor/meteor/issues/1821 , le serveur meteor doit conserver une copie des données publiées pour chaque client dans la zone de fusion . C'est ce qui permet à la magie de Meteor de se produire, mais cela entraîne également le maintien de toutes les grandes bases de données partagées dans la mémoire du processus de nœud. Même en utilisant une optimisation possible pour des collections statiques comme dans ( Y a-t-il un moyen de dire à meteor qu'une collection est statique (ne changera jamais)? ), Nous avons rencontré un énorme problème avec l'utilisation du processeur et de la mémoire du processus Node.

Dans notre cas, nous publions une collection de 15 000 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 qui se connecte mettra le serveur à genoux (et les connexions simultanées se bloqueront mutuellement) et l'utilisation de la mémoire augmentera linéairement dans le nombre de clients. Dans notre cas, chaque client a entraîné une utilisation supplémentaire d' environ 60 Mo de mémoire, même si les données brutes transférées ne représentaient que 5 Mo environ.

Dans notre cas, comme la collection était statique, nous avons résolu ce problème en envoyant tous les documents sous forme de .jsonfichier, qui a été gzippé par nginx, et en les chargeant dans une collection anonyme, ce qui n'a entraîné qu'un transfert d'environ 1 Mo de données sans processeur supplémentaire. ou de la mémoire 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 en utilisant des _ids de publications beaucoup plus petites sur le serveur, ce qui permet de conserver la plupart des avantages de Meteor. Cela a permis à l'application de s'adapter à beaucoup plus de 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 instance de Node est monothread.

Cependant, le problème du partage de grandes collections inscriptibles 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 approfondie 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 peuvent tout simplement pas évoluer.

Andrew Mao
la source
@Harry oplog n'a pas d'importance dans cette situation; les données étaient statiques.
Andrew Mao
Pourquoi ne fait-il pas de diffs des copies minimongo côté serveur? Peut-être que tout a changé dans la version 1.0? Je veux dire qu'en général, ils sont les mêmes, j'espère, même les fonctions qu'il rappelle seraient similaires (si je suis le suivant, c'est quelque chose qui est stocké là aussi et potentiellement différent.)
MistereeDevlord
@MistereeDevlord Les différences de changements et les caches de données client sont séparés pour le moment. Même si tout le monde a les mêmes données et qu'un seul diff est nécessaire, le cache par client diffère car le serveur ne peut pas les traiter de manière identique. Cela pourrait certainement être fait plus intelligemment sur la mise en œuvre existante.
Andrew Mao
@AndrewMao Comment vous assurez-vous que les fichiers gzippés sont sécurisés lors de leur envoi au client, c'est-à-dire que seul un client connecté peut y accéder?
FullStack
4

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 l'inspecteur Webkit (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 obtenir des conseils sur l'utilisation de WKI, consultez cet article . C'est un peu dépassé, mais surtout toujours valable, surtout pour cette question.

javajosh
la source
2
Une explication du mécanisme de sondage: eventedmind.com/posts/meteor-liveresultsset
cmather