Quanto può essere efficiente Meteor mentre condivide una vasta collezione tra molti clienti?


100

Immagina il seguente caso:

  • 1.000 client sono collegati a una pagina Meteor che mostra il contenuto della raccolta "Somestuff".

  • "Somestuff" è una collezione che contiene 1.000 articoli.

  • Qualcuno inserisce un nuovo oggetto nella collezione "Somestuff"

Cosa accadrà:

  • Tutti Meteor.Collectioni messaggi sui client verranno aggiornati, ovvero l'inserimento inoltrato a tutti (il che significa un messaggio di inserimento inviato a 1.000 client)

Qual è il costo in termini di CPU per il server per determinare quale client deve essere aggiornato?

È corretto che solo il valore inserito venga inoltrato ai client e non l'intero elenco?

Come funziona nella vita reale? Sono disponibili benchmark o esperimenti di tale scala?

Risposte:


119

La risposta breve è che solo i nuovi dati vengono inviati lungo il filo. Ecco come funziona.

Ci sono tre parti importanti del server Meteor che gestiscono gli abbonamenti: la funzione di pubblicazione , che definisce la logica per i dati forniti dall'abbonamento; il driver Mongo , che controlla le modifiche nel database; e la casella di unione , che combina tutti gli abbonamenti attivi di un client e li invia tramite la rete al client.

Pubblica funzioni

Ogni volta che un client Meteor si iscrive a una raccolta, il server esegue una funzione di pubblicazione . Il compito della funzione di pubblicazione è individuare il set di documenti che il suo cliente dovrebbe avere e inviare ciascuna proprietà del documento nella casella di unione. Viene eseguito una volta per ogni nuovo client in abbonamento. Puoi inserire qualsiasi JavaScript che desideri nella funzione di pubblicazione, come il controllo degli accessi arbitrariamente complesso utilizzando this.userId. La funzione di pubblicare invia i dati nella casella di merge chiamando this.added, this.changede this.removed. Consulta la documentazione di pubblicazione completa per maggiori dettagli.

La maggior parte delle funzioni di pubblicare non devono muck in giro con il basso livello added, changede removedAPI, però. Se un pubblicano funzione restituisce un cursore Mongo, il server Meteor connette automaticamente l'uscita del driver Mongo ( insert, updatee removedcallback) all'ingresso della scatola di unione ( this.added, this.changede this.removed). È abbastanza chiaro che puoi eseguire tutti i controlli dei permessi in anticipo in una funzione di pubblicazione e quindi connettere direttamente il driver del database alla casella di unione senza alcun codice utente nel modo. E quando la pubblicazione automatica è attiva, anche questa piccola parte è nascosta: il server imposta automaticamente una query per tutti i documenti in ogni raccolta e li inserisce nella casella di unione.

D'altra parte, non sei limitato alla pubblicazione di query di database. Ad esempio, è possibile scrivere una funzione di pubblicazione che legge una posizione GPS da un dispositivo all'interno di una Meteor.setIntervalo esegue il polling di un'API REST legacy da un altro servizio Web. In questi casi, ci si emettono modifiche alla casella di merge chiamando il basso livello added, changede removedDDP API.

L'autista Mongo

Il compito del conducente Mongo è controllare il database Mongo per le modifiche alle query in tempo reale. Queste query eseguite in modo continuo e restituiscono gli aggiornamenti come il cambiamento risultati chiamando added, removede changedcallback.

Mongo non è un database in tempo reale. Quindi l'autista fa i sondaggi. Conserva una copia in memoria del risultato dell'ultima query per ogni query live attiva. Su ogni ciclo di polling, si confronta il nuovo risultato con il risultato precedente salvato, calcolando l'insieme minimo di added, removede changed gli eventi che descrivono la differenza. Se più chiamanti registrano i callback per la stessa query live, il driver guarda solo una copia della query, chiamando ogni callback registrata con lo stesso risultato.

Ogni volta che il server aggiorna una raccolta, il driver ricalcola ogni query in tempo reale su quella raccolta (le versioni future di Meteor esporranno un'API di ridimensionamento per limitare le query in tempo reale che vengono ricalcolate durante l'aggiornamento). Il driver inoltre esegue il polling di ciascuna query in tempo reale su un rilevare gli aggiornamenti del database fuori banda che hanno ignorato il server Meteor.

La scatola di fusione

Il lavoro della scatola di fusione è quello di combinare i risultati ( added, changede removed le chiamate) di tutte le funzioni pubblicare attivi di un cliente in un unico flusso di dati. C'è una casella di unione per ogni client connesso. Contiene una copia completa della cache minimongo del client.

Nel tuo esempio con un solo abbonamento, la casella di unione è essenzialmente un pass-through. Ma un'app più complessa può avere più abbonamenti che potrebbero sovrapporsi. Se due sottoscrizioni impostano entrambe lo stesso attributo sullo stesso documento, la casella di unione decide quale valore ha la priorità e lo invia solo al client. Non abbiamo ancora esposto l'API per l'impostazione della priorità dell'abbonamento. Per ora, la priorità è determinata dall'ordine in cui il client sottoscrive i set di dati. La prima sottoscrizione effettuata da un client ha la massima priorità, la seconda è la successiva più alta e così via.

Poiché la casella di unione mantiene lo stato del client, può inviare la quantità minima di dati per mantenere ogni client aggiornato, indipendentemente dalla funzione di pubblicazione che lo alimenta.

Cosa succede con un aggiornamento

Quindi ora abbiamo preparato il terreno per il tuo scenario.

Abbiamo 1.000 clienti connessi. Ciascuno è iscritto alla stessa query Mongo dal vivo ( Somestuff.find({})). Poiché la query è la stessa per ogni client, il driver esegue solo una query live. Sono presenti 1.000 caselle di unione attive. E la funzione di pubblicazione di ogni cliente ha registrato un added, changede removedsu quella query in tempo reale che alimenta una delle caselle di unione. Nient'altro è collegato alle caselle di unione.

Per prima cosa l'autista Mongo. Quando uno dei client inserisce un nuovo documento in Somestuff, attiva un ricalcolo. Il driver Mongo riesegue la query per tutti i documenti in Somestuff, confronta il risultato con il risultato precedente in memoria, rileva che è presente un nuovo documento e chiama ciascuno dei 1.000 insertcallback registrati .

Successivamente, le funzioni di pubblicazione. Qui sta succedendo molto poco: ognuno dei 1.000 insertcallback spinge i dati nella casella di unione chiamando added.

Infine, ogni casella di unione controlla questi nuovi attributi rispetto alla sua copia in memoria della cache del suo client. In ogni caso, rileva che i valori non sono ancora presenti sul client e non nascondono un valore esistente. Quindi la casella di unione emette un DATAmessaggio DDP sulla connessione SockJS al suo client e aggiorna la sua copia in memoria lato server.

Il costo totale della CPU è il costo per diff una query Mongo, più il costo di 1.000 caselle di unione che controllano lo stato dei loro clienti e costruiscono un nuovo payload del messaggio DDP. Gli unici dati che fluiscono sulla rete sono un singolo oggetto JSON inviato a ciascuno dei 1.000 client, corrispondente al nuovo documento nel database, più un messaggio RPC al server dal client che ha effettuato l'inserimento originale.

Ottimizzazioni

Ecco cosa abbiamo decisamente pianificato.

  • Driver Mongo più efficiente. Abbiamo ottimizzato il driver in 0.5.1 per eseguire un solo osservatore per query distinta.

  • Non tutte le modifiche al database dovrebbero attivare un ricalcolo di una query. Possiamo apportare alcuni miglioramenti automatici, ma l'approccio migliore è un'API che consente allo sviluppatore di specificare quali query devono essere rieseguite. Ad esempio, è ovvio per uno sviluppatore che l'inserimento di un messaggio in una chatroom non dovrebbe invalidare una query in tempo reale per i messaggi in una seconda stanza.

  • Il driver Mongo, la funzione di pubblicazione e la casella di unione non devono essere eseguiti nello stesso processo o anche sulla stessa macchina. Alcune applicazioni eseguono query live complesse e necessitano di più CPU per controllare il database. Altri hanno solo poche query distinte (immagina un motore di blog), ma forse molti client connessi: questi richiedono più CPU per le caselle di unione. Separare questi componenti ci consentirà di ridimensionare ogni pezzo in modo indipendente.

  • Molti database supportano trigger che si attivano quando una riga viene aggiornata e forniscono le righe vecchie e nuove. Con questa funzione, un driver di database potrebbe registrare un trigger invece di eseguire il polling per le modifiche.


C'è qualche esempio su come utilizzare Meteor.publish per pubblicare dati non del cursore? Come i risultati di un'API di riposo legacy menzionata nella risposta?
Tony

@ Tony: è nella documentazione. Controlla l'esempio di conteggio delle stanze.
Mitar

Vale la pena notare che nelle versioni 0.7, 0.7.1, 0.7.2 Meteor passa al OpLog Osservare driver per la maggior parte delle query (eccezioni sono skip, $neare $wherele query che contengono), che è molto più efficiente in carico della CPU, la larghezza di banda della rete e consente scalabilità dell'applicazione server.
imslavko

E se non tutti gli utenti vedono gli stessi dati. 1. si sono iscritti a diversi argomenti .2. hanno ruoli diversi quindi all'interno dello stesso argomento principale, ci sono alcuni messaggi che non dovrebbero raggiungerli.
tgkprog

@debergalis per quanto riguarda l'invalidazione della cache, forse troverai idee dal mio documento vanisoft.pl/~lopuszanski/public/cache_invalidation.pdf utile
qbolec

29

Dalla mia esperienza, l'utilizzo di molti client con durante la condivisione di una vasta raccolta in Meteor è essenzialmente impraticabile, a partire dalla versione 0.7.0.1. Cercherò di spiegare perché.

Come descritto nel post sopra e anche in https://github.com/meteor/meteor/issues/1821 , il server meteor deve conservare una copia dei dati pubblicati per ogni client nella casella di unione . Questo è ciò che consente la magia di Meteor, ma fa anche sì che qualsiasi database condiviso di grandi dimensioni venga mantenuto ripetutamente nella memoria del processo del nodo. Anche quando si utilizza una possibile ottimizzazione per le raccolte statiche come in ( C'è un modo per dire a meteor che una raccolta è statica (non cambierà mai)? ), Abbiamo riscontrato un grosso problema con l'utilizzo della CPU e della memoria del processo Node.

Nel nostro caso, stavamo pubblicando una raccolta di 15.000 documenti per ogni client che era completamente statica. Il problema è che la copia di questi documenti nella casella di unione di un client (in memoria) al momento della connessione ha sostanzialmente portato il processo Node al 100% della CPU per quasi un secondo e ha comportato un ampio utilizzo aggiuntivo della memoria. Questo è intrinsecamente non scalabile, perché qualsiasi client in connessione metterà in ginocchio il server (e le connessioni simultanee si bloccheranno a vicenda) e l'utilizzo della memoria aumenterà in modo lineare nel numero di client. Nel nostro caso, ogni cliente ha causato ~ 60 MB aggiuntivi utilizzo di memoria , anche se i dati grezzi trasferiti erano solo di circa 5 MB.

Nel nostro caso, poiché la raccolta era statica, abbiamo risolto questo problema inviando tutti i documenti come .jsonfile, che è stato compresso con gzip da nginx, e caricandoli in una raccolta anonima, ottenendo solo un trasferimento di dati di ~ 1 MB senza CPU aggiuntiva o memoria nel processo del nodo e un tempo di caricamento molto più veloce. Tutte le operazioni su questa raccolta sono state eseguite utilizzando _idi messaggi di posta elettronica da pubblicazioni molto più piccole sul server, consentendo di conservare la maggior parte dei vantaggi di Meteor. Ciò ha consentito all'applicazione di scalare a molti più client. Inoltre, poiché la nostra app è per lo più di sola lettura, abbiamo ulteriormente migliorato la scalabilità eseguendo più istanze di Meteor dietro nginx con bilanciamento del carico (sebbene con un singolo Mongo), poiché ogni istanza di Node è a thread singolo.

Tuttavia, il problema della condivisione di raccolte di grandi dimensioni e scrivibili tra più client è un problema tecnico che deve essere risolto da Meteor. Probabilmente c'è un modo migliore che tenere una copia di tutto per ogni client, ma ciò richiede una seria riflessione come problema dei sistemi distribuiti. Gli attuali problemi di utilizzo massiccio della CPU e della memoria non saranno scalabili.


@Harry oplog non ha importanza in questa situazione; i dati erano statici.
Andrew Mao

Perché non esegue i diff delle copie minimongo lato server? Forse è cambiato tutto nella 1.0? Voglio dire che di solito sono le stesse che spero, anche le funzioni che richiama sarebbero simili (se sto seguendo questo è qualcosa che è memorizzato anche lì e potenzialmente diverso.)
MistereeDevlord

@MistereeDevlord La differenza di modifiche e le cache dei dati dei clienti sono separate in questo momento. Anche se tutti hanno gli stessi dati ed è necessaria solo una differenza, la cache per client è diversa perché il server non può trattarli in modo identico. Questo potrebbe sicuramente essere fatto in modo più intelligente rispetto all'implementazione esistente.
Andrew Mao

@AndrewMao Come ci si assicura che i file compressi con gzip siano protetti quando vengono inviati al client, cioè solo un client connesso può accedervi?
FullStack

4

L'esperimento che puoi utilizzare per rispondere a questa domanda:

  1. Installa una meteora di prova: meteor create --example todos
  2. Eseguilo sotto Webkit Inspector (WKI).
  3. Esamina il contenuto dei messaggi XHR che si muovono attraverso il cavo.
  4. Osserva che l'intera collezione non viene spostata attraverso il filo.

Per suggerimenti su come utilizzare WKI, consulta questo articolo . È un po 'datato, ma per lo più ancora valido, soprattutto per questa domanda.


2
Una spiegazione del meccanismo di votazione: eventedmind.com/posts/meteor-liveresultsset
cmather

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.