benchmarking meteor

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



(4)

Imagine 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 nuevo artículo en la colección "Somestuff"

Lo que sucederá:

  • Todos los Meteor.Collection s en 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 1,000 clientes)

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

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

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


Desde mi experiencia, usar muchos clientes mientras compartes una gran colección en Meteor es esencialmente inviable, a partir de la versión 0.7.0.1. Trataré de 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 meteoros debe conservar una copia de los datos publicados para cada cliente en el cuadro de combinación . Esto es lo que permite que ocurra la magia de los Meteoros, pero también da como resultado que las grandes bases de datos compartidas se guarden repetidamente en la memoria del proceso del nodo. Incluso cuando se usa una optimización posible para colecciones estáticas como en ( ¿Hay alguna manera de decir meteoros que una colección es estática (nunca cambiará)? ), Experimentamos un gran problema con el uso de CPU y memoria del proceso Nodo.

En nuestro caso, estábamos publicando una colección de 15k documentos para cada cliente que era completamente estática. El problema es que al copiar estos documentos en el cuadro de combinación de un cliente (en la memoria) durante la conexión, básicamente, el proceso de Nodo llegó al 100% de la CPU durante casi un segundo y se generó un gran uso adicional de la memoria. Esto es inherentemente incalable, ya que cualquier cliente que se conecte pondrá al servidor de rodillas (y las conexiones simultáneas se bloquearán entre sí) y el uso de la memoria aumentará linealmente en la cantidad de clientes. En nuestro caso, cada cliente causó unos ~ 60 MB adicionales de uso de memoria, a pesar de que los datos brutos transferidos eran solo de unos 5 MB.

En nuestro caso, como la colección era estática, .json este problema enviando todos los documentos como un archivo .json , que fue comprimido por nginx y cargándolos en una colección anónima, lo que resultó en una transferencia de datos de ~ 1MB sin CPU o memoria adicional en el proceso del nodo y un tiempo de carga mucho más rápido. Todas las operaciones de esta colección se realizaron utilizando _id s 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 escalara a muchos más clientes. Además, como 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 nodo es de subproceso único.

Sin embargo, el problema de compartir grandes colecciones grabables entre múltiples clientes es un problema de ingeniería que Meteor necesita resolver. Probablemente haya una manera mejor que mantener una copia de todo para cada cliente, pero eso requiere una reflexión seria como un problema de sistemas distribuidos. Los problemas actuales de uso masivo de CPU y memoria simplemente no se escalarán.


El experimento que puedes usar para responder esta pregunta:

  1. Instalar un meteorito de prueba: meteor create --example todos
  2. Ejecútelo bajo el inspector de Webkit (WKI).
  3. Examine el contenido de los mensajes XHR que se mueven por el 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 article . Está un poco desactualizado, pero sigue siendo válido, especialmente para esta pregunta.



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

Hay tres partes importantes del servidor de 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 de Mongo , que mira 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 determinar el conjunto de documentos que su cliente debe tener y enviar cada propiedad del documento al cuadro de combinación. Se ejecuta una vez para cada nuevo cliente suscriptor. Puede colocar el JavaScript que desee en la función de publicación, como el control de acceso arbitrariamente complejo utilizando this.userId . La función de publicación envía datos al cuadro de combinación llamando a this.added , this.changed y this.removed . Consulte la documentación completa de publicación para obtener más detalles.

Sin embargo, la mayoría de las funciones de publicación no tienen que ensuciarse con la API added , changed y removed bajo nivel. Si una función de publicación devuelve un cursor Mongo, el servidor Meteor conecta automáticamente la salida del controlador de Mongo ( insert , update y this.removed devoluciones de llamada) a la entrada del cuadro de combinación ( this.added , this.changed y this.removed ). Es bastante claro que puede hacer todas las comprobaciones de permiso 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 se activa autoublish, incluso este pequeño bit está oculto: el servidor automáticamente configura una consulta para todos los documentos en cada colección y los empuja al 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 desde un dispositivo dentro de un Meteor.setInterval , o Meteor.setInterval una API REST heredada de otro servicio web. En esos casos, emitiría cambios al cuadro de combinación llamando a la API de DDP added , changed y removed bajo nivel.

El conductor de Mongo

El trabajo del conductor de Mongo es mirar la base de datos de Mongo para ver cambios en las consultas en vivo. Estas consultas se ejecutan continuamente y devuelven actualizaciones a medida que cambian los resultados al invocar devoluciones de llamada added , removed y changed .

Mongo no es una base de datos en tiempo real. Entonces el conductor sondea. Mantiene una copia en la memoria del último resultado de la consulta para cada consulta en vivo activa. En cada ciclo de sondeo, compara el nuevo resultado con el resultado guardado anterior, calculando el conjunto mínimo de eventos added , removed y changed que describen la diferencia. Si varias personas que llaman registran devoluciones de llamada para la misma consulta en vivo, el controlador solo observa 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 recalcula cada consulta activa en esa colección (las versiones futuras de Meteor expondrán una API de escala para limitar qué consultas en vivo recalculan en la actualización). El controlador también sondea cada consulta en vivo en un temporizador de 10 segundos para detectar actualizaciones de bases de datos fuera de banda que pasaron por alto el servidor Meteor.

El cuadro de combinación

El trabajo del cuadro de combinación consiste en combinar los resultados (llamadas added , changed y removed ) de todas las funciones de publicación activas de un cliente en una única secuencia de datos. Hay un cuadro de combinación para cada cliente conectado. Tiene una copia completa de la memoria caché minimongo del cliente.

En su ejemplo con solo una suscripción, el cuadro de combinación es esencialmente un paso a través. Pero una aplicación más compleja puede tener múltiples 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. Todavía 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 mantiene el estado del cliente, puede enviar la cantidad mínima de datos para mantener cada cliente actualizado, sin importar qué función de publicación 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({}) ). Como la consulta es la misma para cada cliente, el controlador solo ejecuta una consulta en vivo. Hay 1,000 cajas de combinación activas. Y la función de publicación de cada cliente registró una added , changed y removed en esa consulta en vivo que se alimenta en uno de los cuadros de fusión. Nada más está conectado a los cuadros de fusión.

Primero el conductor de Mongo. Cuando uno de los clientes inserta un nuevo documento en Somestuff , desencadena un recálculo. El controlador de Mongo vuelve a ejecutar la consulta de todos los documentos en Somestuff , compara el resultado con el resultado anterior en la memoria, descubre que hay un documento nuevo y llama a cada una de las 1000 devoluciones de insert .

A continuación, las funciones de publicación. Aquí está sucediendo muy poco: cada una de las 1000 retrollamadas insert información en el cuadro de fusión llamando a added .

Finalmente, cada cuadro de combinación comprueba estos nuevos atributos contra su copia en memoria de la memoria 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 mensaje DDP DATA 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 una consulta de Mongo, más el costo de 1,000 cajas de combinación que verifican el estado de sus clientes y construyen una nueva carga de mensajes DDP. Los únicos datos que fluyen a través del cable son un solo 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 del cliente que realizó la inserción original.

Optimizaciones

Esto es lo que definitivamente hemos planeado.

  • Conductor de Mongo más eficiente. Optimizamos el controlador en 0.5.1 para ejecutar solo un solo observador por consulta distinta.

  • No todos los cambios de DB deben desencadenar un recálculo de una consulta. Podemos hacer algunas mejoras automáticas, 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 debe 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 en vivo complejas y necesitan más CPU para ver la base de datos. Otros solo tienen algunas consultas distintas (imagine un motor de blog), pero posiblemente muchos clientes conectados: estos necesitan más CPU para los cuadros de fusión. Separar estos componentes nos permitirá escalar cada pieza de forma independiente.

  • Muchas bases de datos admiten desencadenantes que se activan cuando se actualiza una fila y proporcionan las filas antiguas y nuevas. Con esa característica, un controlador de base de datos podría registrar un desencadenador en lugar de sondear en busca de cambios.