Con Node.js podemos crear rápidamente un servidor simple que se ejecuta en único hilo y que, dependiendo de la potencia del procesador y el alcance del proyecto, es posible que sea suficiente para manejar todas las peticiones que necesitemos (aquí tienes un ejemplo para empezar).

Es un único hilo que se ejecuta en un único procesador pero hoy en día, que cualquier equipo trae varios procesadores, da un poco de rabia no sacarles partido si podemos mejorar el rendimiento con ellos. Si nuestro proyecto maneja datos en tiempo real y/o una cantidad más o menos grande de peticiones, el tiempo de respuesta puede aumentar lo suficiente para hacer que la experiencia del usuario no sea satisfactoria.

Un ejemplo

Suponemos que una petición de un usuario tarda 40ms en responderse. Para simplificar también vamos a suponer que no hace ninguna tarea asíncrona , con lo que no procesa otra petición hasta que termina la primera. Es decir, si recibimos 3 peticiones a la vez, una empezará a ejecutarse y las otras 2 se quedan en cola hasta que termina la primera, con lo que la última tendrá un tiempo de respuesta de 3x40ms = 120ms.

Ahora imagina que el proyecto es un juego en tiempo real, que recibe peticiones de actualización de todos los clientes conectados. Suponemos también que el servidor tiene una función actualizarClientes que se ejecuta cada 100ms con setInterval.
Todas las ejecuciones de funciones se van añadiendo a la cola de eventos de Node.
Aclaración sobre setInterval y setTimeout: en realidad NO se ejecutan cuando se cumple el tiempo, sino que en ese momento se añaden a la cola de ejecución.
El problema llega cuando el servidor actualiza los clientes y pone en espera la siguiente actualización (100ms) y entonces recibe  5 peticiones a la vez. La cola de ejecución entonces será esta (en rojo lo que ya se ha ejecutado):

  1. Momento 0ms (ha terminado actualizarClientes y recibe 5 peticiones)
  2. Momento 100ms (pone en cola la función actualizarClientes, recuerda que no la ejecuta, la pone en cola y la ejecuta cuando el hilo principal esté libre)
  3. Momento 200ms ejecuta actualizarClientes

tiempos de ejecución

Vemos entonces que antes los clientes recibían una actualización cada 100ms y ahora cada 200ms con solo 5 peticiones en cola. Ahora piensa que puede pasar si metemos 100 jugadores, parece que las actualizaciones se van a retrasar más de la cuenta ¿verdad?

Una prueba

Para hacer esta prueba voy a utilizar el proyecto creado en este post y lo voy a arrancar en una Raspberry Pi. Para hacer las peticiones voy a usar mi ordenador y las voy a hacer con Siege, un programa de prueba de carga para peticiones http. Si no lo tenéis se puede instalar desde linea de comandos:

Para simular 1 petición de 100 usuarios concurrentes (cambia la ip y puerto por el de tu equipo):

El resultado:

100_usuarios_1peticion_1core

Response time nos indica el tiempo medio de respuesta a cada uno de los usuarios simulados.
En el caso del juego, tenemos que suponer que se han puesto todas estas peticiones en cola a la vez, y que el servidor no va a poder actualizar los datos de los clientes hasta que termine de ejecutarlas, con lo que los usuarios van a tener que esperar como poco 4.03 segundos para ver los datos del resto de jugadores. Todo fluidez ¿verdad? (Recuerda que es una Raspberry PI 😉 )

Una solución quiero

Node.js trae la posibilidad de utilizar otros nucleos con el módulo cluster, para ejecutar el mismo programa de node en paralelo, o con el módulo child_process, para ejecutar otros procesos de larga duración y liberar el hilo principal para que siga procesando peticiones.

En este caso queremos que los 4 nucleos escuchen y procesen peticiones de la misma forma, así que vamos a añadir el módulo cluster al ejemplo de aplicación que tenemos hecha con express creator. Abrimos el archivo bin/www y añadimos:

Luego sustituimos esta parte del archivo:

Por esta otra:

multicore

El modulo cluster se inicia por primera vez como “master”. En ese proceso, cluster.isMaster es true. Entonces crea por cada núcleo, un proceso idéntico con cluster.fork(). Cuando estos procesos se inician, cluster.isMaster es false e inician la aplicación escuch

ando todos por el mismo puerto. Cuando arranquemos de nuevo el proyecto podremos ver en la consola como se inician estos procesos:

Ahora vemos la diferencia de rendimiento:

Un core

100_usuarios_1peticion_1core

 

 

 

 

 

 

 

 

4 cores

100_usuarios_1peticion_multicore

A tener en cuenta

Solo con utilizar un portatil o un ordenador de sobremesa, los resultados mejoran mucho, pero fijaos también en que estas peticiones son de lo más sencillas, con lo que segun el proyecto avance la respuesta será más lenta .
A poco que añadáis una consulta a base de datos, el flujo de los eventos va a cambiar y se va a complicar. He hecho las pruebas más sencillas posibles para verlo más fácil.

Espero que os sirva.

¡Un saludo!

 

Fuentres:

Krasimirtsonev.com

blog.carbonfive.com

Documentación de Node

 

Sobre El Autor

Economista reconvertido a programador. Hoy en día trabajo como Desarrollador de aplicaciones web y móvil con Node.js, Angular.js (MEAN Stack) e Ionic framework. También intento mantenerme al día de lo que se mueve en este mundillo y mantengo este pequeño blog como documentación personal. Si echas algo de menos en el blog no dudes en comentar, y si te ha resultado de utilidad algún artículo, te agradezco que lo agradezcas ;-). ¡Un saludo!

Artículos Relacionados

Hacer Comentario

Su dirección de correo electrónico no será publicada.