¿Qué tan eficiente puede ser Meteor al compartir una gran colección entre muchos clientes?

100

Imagina el siguiente caso:

  • 1.000 clientes están conectados a una página de Meteor que muestra el contenido de la colección "Somestuff".

  • "Somestuff" es una colección que contiene 1.000 artículos.

  • Alguien inserta un elemento nuevo en la colección "Somestuff"

Lo que sucederá:

  • Todos Meteor.Collectionlos mensajes de correo electrónico de los clientes se actualizarán, es decir, la inserción se reenviará a todos ellos (lo que significa que se enviará un mensaje de inserción a 1000 clientes)

¿Cuál es el costo en términos de CPU para que el servidor determine qué cliente necesita actualizarse?

¿Es exacto que solo se enviará a los clientes el valor insertado y no la lista completa?

¿Cómo funciona esto en la vida real? ¿Hay pruebas de referencia o experimentos de tal escala disponibles?

Flavien Volken
fuente

Respuestas:

119

La respuesta corta es que solo se envían nuevos datos por el cable. Así es como funciona.

Hay tres partes importantes del servidor Meteor que administran las suscripciones: la función de publicación , que define la lógica de los datos que proporciona la suscripción; el controlador Mongo , que vigila la base de datos en busca de cambios; y el cuadro de combinación , que combina todas las suscripciones activas de un cliente y las envía a través de la red al cliente.

Publicar funciones

Cada vez que un cliente de Meteor se suscribe a una colección, el servidor ejecuta una función de publicación . El trabajo de la función de publicación es averiguar el conjunto de documentos que su cliente debe tener y enviar cada propiedad del documento al cuadro de combinación. Se ejecuta una vez por cada nuevo cliente suscrito. Puede poner cualquier JavaScript que desee en la función de publicación, como el uso de control de acceso arbitrariamente complejo this.userId. El publican función envía los datos en la caja de combinación llamando this.added, this.changedy this.removed. Consulte la documentación de publicación completa para obtener más detalles.

La mayoría de publicar funciones no tienen que ensuciar alrededor con el bajo nivel added, changedy la removedAPI, sin embargo. Si una publican función devuelve un cursor Mongo, el servidor Meteor conecta automáticamente la salida del controlador de Mongo ( insert, updatey removeddevoluciones de llamada) a la entrada de la caja de combinación ( this.added, this.changedy this.removed). Es bastante bueno que pueda hacer todas las comprobaciones de permisos por adelantado en una función de publicación y luego conectar directamente el controlador de la base de datos al cuadro de combinación sin ningún código de usuario en el camino. Y cuando la publicación automática está activada, incluso esta pequeña parte está oculta: el servidor configura automáticamente una consulta para todos los documentos de cada colección y los inserta en el cuadro de combinación.

Por otro lado, no está limitado a publicar consultas de bases de datos. Por ejemplo, puede escribir una función de publicación que lea una posición GPS de un dispositivo dentro de un Meteor.setInterval, o sondee una API REST heredada de otro servicio web. En esos casos, usted emiten cambios en el cuadro de combinación llamando al bajo nivel added, changedy removedDDP API.

El conductor de Mongo

El trabajo del conductor de Mongo es observar la base de datos de Mongo para ver si hay cambios en las consultas en vivo. Estas consultas se ejecuten de forma continua y actualizaciones a medida que cambian los resultados vuelven llamando added, removedy changedlas devoluciones de llamada.

Mongo no es una base de datos en tiempo real. Así que el conductor realiza encuestas. Mantiene una copia en memoria del último resultado de la consulta para cada consulta activa en vivo. En cada ciclo de sondeo, se compara el nuevo resultado con el resultado guardado anterior, calcular el conjunto mínimo de added, removedy changed acontecimientos que describen la diferencia. Si varias personas que llaman registran devoluciones de llamada para la misma consulta en vivo, el conductor solo ve una copia de la consulta, llamando a cada devolución de llamada registrada con el mismo resultado.

Cada vez que el servidor actualiza una colección, el controlador vuelve a calcular cada consulta en vivo en esa colección (las versiones futuras de Meteor expondrán una API de escalado para limitar qué consultas en vivo se recalculan en la actualización). El controlador también sondea cada consulta en vivo en un temporizador de 10 segundos para detectar actualizaciones de base de datos fuera de banda que eludieron el servidor Meteor.

El cuadro de combinación

El trabajo del cuadro de combinación es la combinación de los resultados ( added, changedy removed llamadas) de todas las funciones publicar activos de un cliente en un único flujo de datos. Hay un cuadro de combinación para cada cliente conectado. Contiene una copia completa de la caché minimongo del cliente.

En su ejemplo con una sola suscripción, el cuadro de combinación es esencialmente una transferencia. Pero una aplicación más compleja puede tener varias suscripciones que pueden superponerse. Si dos suscripciones establecen el mismo atributo en el mismo documento, el cuadro de combinación decide qué valor tiene prioridad y solo lo envía al cliente. Aún no hemos expuesto la API para establecer la prioridad de suscripción. Por ahora, la prioridad está determinada por el orden que el cliente suscribe a los conjuntos de datos. La primera suscripción que realiza un cliente tiene la prioridad más alta, la segunda suscripción es la siguiente más alta, y así sucesivamente.

Debido a que el cuadro de combinación contiene el estado del cliente, puede enviar la cantidad mínima de datos para mantener a cada cliente actualizado, sin importar la función de publicación que lo alimente.

Qué sucede en una actualización

Así que ahora hemos preparado el escenario para su escenario.

Tenemos 1.000 clientes conectados. Cada uno está suscrito a la misma consulta de Mongo en vivo ( Somestuff.find({})). Dado que la consulta es la misma para cada cliente, el controlador solo ejecuta una consulta en vivo. Hay 1000 cuadros de combinación activos. Y la función de publicación de cada cliente registró un added, changedy removeden esa consulta en vivo que se alimenta en uno de los cuadros de combinación. Nada más está conectado a los cuadros de combinación.

Primero el controlador de Mongo. Cuando uno de los clientes inserta un nuevo documento en Somestuff, se activa un nuevo cálculo. El controlador de Mongo vuelve a ejecutar la consulta para todos los documentos en Somestuff, compara el resultado con el resultado anterior en la memoria, encuentra que hay un documento nuevo y llama a cada una de las 1,000 insertdevoluciones de llamada registradas .

A continuación, las funciones de publicación. Aquí sucede muy poco: cada una de las 1,000 insertdevoluciones de llamada empuja los datos al cuadro de combinación mediante una llamada added.

Por último, cada cuadro de combinación comprueba estos nuevos atributos con su copia en memoria de la caché de su cliente. En cada caso, encuentra que los valores aún no están en el cliente y no sombrean un valor existente. Entonces, el cuadro de combinación emite un DATAmensaje DDP en la conexión SockJS a su cliente y actualiza su copia en memoria del lado del servidor.

El costo total de la CPU es el costo de diferenciar una consulta de Mongo, más el costo de 1,000 cuadros de combinación que verifican el estado de sus clientes y construyen una nueva carga útil de mensajes DDP. Los únicos datos que fluyen a través del cable son un único objeto JSON enviado a cada uno de los 1,000 clientes, correspondiente al nuevo documento en la base de datos, más un mensaje RPC al servidor desde el cliente que hizo la inserción original.

Optimizaciones

Esto es lo que definitivamente hemos planeado.

  • Conductor Mongo más eficiente. Hemos optimizado el controlador en 0.5.1 sólo para ejecutar un solo observador por consulta distinta.

  • No todos los cambios en la base de datos deberían desencadenar un nuevo cálculo de una consulta. Podemos realizar algunas mejoras automatizadas, pero el mejor enfoque es una API que permite al desarrollador especificar qué consultas deben volver a ejecutarse. Por ejemplo, es obvio para un desarrollador que insertar un mensaje en una sala de chat no debería invalidar una consulta en vivo para los mensajes en una segunda sala.

  • El controlador de Mongo, la función de publicación y el cuadro de combinación no necesitan ejecutarse en el mismo proceso, ni siquiera en la misma máquina. Algunas aplicaciones ejecutan consultas complejas en vivo y necesitan más CPU para observar la base de datos. Otros tienen solo unas pocas consultas distintas (imagine un motor de blog), pero posiblemente muchos clientes conectados, estos necesitan más CPU para los cuadros de combinación. Separar estos componentes nos permitirá escalar cada pieza de forma independiente.

  • Muchas bases de datos admiten activadores que se activan cuando se actualiza una fila y proporcionan las filas nuevas y antiguas. Con esa función, un controlador de base de datos podría registrar un disparador en lugar de sondear los cambios.

debergalis
fuente
¿Hay algún ejemplo sobre cómo usar Meteor.publish para publicar datos que no son de cursor? ¿Como los resultados de una API de descanso heredada que se menciona en la respuesta?
Tony
@Tony: Está en documentación. Consulte el ejemplo de recuento de habitaciones.
Mitar
Vale la pena señalar que en las versiones 0.7, 0.7.1, 0.7.2 Meteor cambió a oplog Observar controlador para la mayoría de las consultas (las excepciones son skip, $neary $wherelas consultas que contienen) que es mucho más eficiente en la carga de la CPU, el ancho de banda de la red y permite el escalado de aplicaciones servidores.
imslavko
¿Qué pasa cuando no todos los usuarios ven los mismos datos? 1. Se suscribieron a diferentes temas .2. tienen diferentes roles, por lo que dentro del mismo tema principal, hay algunos mensajes que se supone que no deben llegar a ellos.
tgkprog
@debergalis con respecto a la invalidación de caché, tal vez encuentre ideas de mi artículo vanisoft.pl/~lopuszanski/public/cache_invalidation.pdf que valgan la pena
qbolec
29

Desde mi experiencia, usar muchos clientes mientras se comparte una gran colección en Meteor es esencialmente inviable, a partir de la versión 0.7.0.1. Intentaré explicar por qué.

Como se describe en la publicación anterior y también en https://github.com/meteor/meteor/issues/1821 , el servidor de meteoritos debe guardar una copia de los datos publicados para cada cliente en el cuadro de combinación . Esto es lo que permite que suceda la magia del meteorito, pero también hace que las grandes bases de datos compartidas se guarden repetidamente en la memoria del proceso del nodo. Incluso cuando usamos una posible optimización para colecciones estáticas como en ( ¿Hay alguna manera de decirle a meteor que una colección es estática (nunca cambiará)? ), Experimentamos un gran problema con el uso de CPU y memoria del proceso Node.

En nuestro caso, estábamos publicando una colección de 15k documentos para cada cliente que era completamente estática. El problema es que copiar estos documentos en el cuadro de combinación de un cliente (en la memoria) después de la conexión básicamente llevó el proceso de nodo al 100% de la CPU durante casi un segundo y dio como resultado un gran uso adicional de memoria. Esto es inherentemente no escalable, porque cualquier cliente que se conecte pondrá de rodillas al servidor (y las conexiones simultáneas se bloquearán entre sí) y el uso de la memoria aumentará linealmente en el número de clientes. En nuestro caso, cada cliente provocó un uso adicional de ~ 60 MB de memoria, a pesar de que los datos sin procesar transferidos eran solo de unos 5 MB.

En nuestro caso, debido a que la colección era estática, resolvimos este problema enviando todos los documentos como un .jsonarchivo, que fue comprimido con gzip por nginx, y cargándolos en una colección anónima, lo que resultó en una transferencia de datos de solo ~ 1 MB sin CPU adicional o memoria en el proceso del nodo y un tiempo de carga mucho más rápido. Todas las operaciones sobre esta colección se realizaron utilizando _idcorreos electrónicos de publicaciones mucho más pequeñas en el servidor, lo que permitió conservar la mayoría de los beneficios de Meteor. Esto permitió que la aplicación escale a muchos más clientes. Además, debido a que nuestra aplicación es principalmente de solo lectura, mejoramos aún más la escalabilidad al ejecutar varias instancias de Meteor detrás de nginx con equilibrio de carga (aunque con un solo Mongo), ya que cada instancia de Node es de un solo subproceso.

Sin embargo, el problema de compartir grandes colecciones de escritura entre varios clientes es un problema de ingeniería que debe ser resuelto por Meteor. Probablemente haya una manera mejor que guardar una copia de todo para cada cliente, pero eso requiere un pensamiento serio como un problema de sistemas distribuidos. Los problemas actuales del uso masivo de CPU y memoria simplemente no se escalarán.

Andrew Mao
fuente
@Harry oplog no importa en esta situación; los datos eran estáticos.
Andrew Mao
¿Por qué no hace diferencias de las copias minimongo del lado del servidor? ¿Quizás todo eso cambió en 1.0? Quiero decir que, por lo general, son los mismos, espero, incluso las funciones a las que devuelve la llamada serían similares (si sigo que eso es algo que también se almacena allí y es potencialmente diferente).
MistereeDevlord
@MistereeDevlord La diferenciación de cambios y cachés de datos de clientes están separados en este momento. Incluso si todos tienen los mismos datos y solo se necesita una diferencia, la caché por cliente es diferente porque el servidor no puede tratarlos de manera idéntica. Definitivamente, esto podría hacerse de manera más inteligente que la implementación existente.
Andrew Mao
@AndrewMao ¿Cómo se asegura de que los archivos comprimidos con gzip estén seguros al enviarlos al cliente, es decir, solo un cliente que haya iniciado sesión puede acceder a ellos?
FullStack
4

El experimento que puede utilizar para responder a esta pregunta:

  1. Instale un meteoro de prueba: meteor create --example todos
  2. Ejecútelo en Webkit inspector (WKI).
  3. Examine el contenido de los mensajes XHR que se mueven a través del cable.
  4. Observe que toda la colección no se mueve a través del cable.

Para obtener consejos sobre cómo usar WKI, consulte este artículo . Está un poco desactualizado, pero en su mayoría sigue siendo válido, especialmente para esta pregunta.

javajosh
fuente
2
Una explicación del mecanismo de votación: eventedmind.com/posts/meteor-liveresultsset
cmather