Cómo ejecutar Node.js en múltiples procesos mediante el módulo clúster

Ejecuta aplicaciones de Node.js a gran escala aprovechando todos los núcleos del sistema mediante el módulo clúster.

Arrancar aplicaciones en Node.js es algo muy sencillo, simplemente ejecutamos el comando node fichero.js y listo, ya tenemos nuestra aplicación funcionando. Pero... ¿qué ocurre si nuestra aplicación se cae o sufre un error? Pues que no volverá a funcionar hasta que reiniciemos el proceso. Esta tarea se puede llevar a cabo automáticamente mediante proyectos como forever o node-supervisor, que mantienen el proceso siempre funcionando.

Módulo clúster en Node.js
Módulo clúster en Node.js

Por otro lado, el funcionamiento estándar de Node.js se basa en un único hilo de ejecución, por lo que un solo proceso es el encargado de gestionar todo. Gracias al procesamiento asíncrono, Node.js es capaz de gestionar las peticiones muy rápidamente pero si contamos con más CPUs, se estarán desaprovechando recursos y esto es lo que vamos a mejorar a continuación.

Para aprovechar los núcleos podemos utilizar un magnífico gestor de procesos llamado PM2 creado por Unitech. Desgraciadamente, tras usarlo durante unas semanas lo considero inestable y no apto para el uso en producción, por lo que utilizaremos forever (npm install -g forever) y el módulo clúster de Node.js.

Utilizando el módulo clúster

El módulo clúster de Node.js se encuentra en desarrollo (Stability: 1 - Experimental), pero nos ha parecido lo suficientemente estable como para usarlo en producción. El único problema que hemos notado es que las peticiones no se distribuyen tan aleatoriamente a través de los procesos como nos gustaría, pero recordad que al ser experimental, ¡bajo vuestra cuenta y riesgo!

En el siguiente ejemplo lo que haremos será crear un fichero llamado server.js que lo ejecutaremos mediante forever. Este fichero será el maestro y se encargará de crear esclavos que iniciarán nuestra aplicación, cuyo punto de entrada es app.js.

Para darle uso al módulo clúster, simplemente tenemos que importar el módulo. Si queremos autodetectar el número de procesadores, importamos también os.

import cluster from 'cluster'
import os from 'os'

A continuación asignamos a la variable workers el número de procesos que vamos a correr simultáneamente. Podemos hacerlo automáticamente mediante:

const workers = os.cpus().length

También configuramos el clúster para que los procesos ejecuten nuestra aplicación:

cluster.setupMaster({ exec: 'app.js' })

Para facilitar el registro, hemos creado una función:

function log(msg) {
  console.log(`[SERVER] ${new Date().toISOString().replace(/T/, ' ').replace(/\..+/, '')} ${msg}`)
}

Iniciamos los esclavos mediante cluster.fork():

log(`Master with pid ${process.pid} starting...`)

for (let i = 0; i < workers; i++) {
  cluster.fork()
}

Después utilizamos dos eventos para registrar cuando nacen y mueren los procesos. Además, si mueren serán reiniciados automáticamente:

cluster.on('exit', (worker, code, signal) => {
  log(`worker with pid ${worker.process.pid} died. Restarting...`)
  cluster.fork()
})

cluster.on('online', worker => {
  log(`Worker with pid ${worker.process.pid} started`)
})

Por último, para ejecutar el proceso maestro de una manera que nos garantice la máxima disponibilidad posible, ejecutamos el proceso mediante el comando forever server.js.

Algo a tener en cuenta utilizando el módulo clúster es que al tratarse de procesos independientes, lo que haya en memoria no se compartirá con el resto de procesos esclavos. Por lo tanto, debemos utilizar almacenamiento de datos compartido tipo mongodb o redis para compartir datos entre procesos.

Pruebas

Aunque las siguientes pruebas no están científicamente demostradas ni ningún dentista/dermatólogo las ha aprobado, si servirán para hacernos una idea. Hemos utilizado boom porque es compatible con ARM (siege no lo es) y funciona bien. El comando para pruebas es el siguiente (50000 peticiones, 100 concurrentes):

boom -n 50000 -c 100 http://127.0.0.1:3000/

x86_64 - Intel Xeon E5-2680 v2 @ 2.80GHz (2 procesos)

Modo normal:

Total:        7.9854 secs.
Slowest:      0.1416 secs.
Fastest:      0.0019 secs.
Average:      0.0159 secs.
Requests/sec: 6261.3879

Modo clúster:

Total:        5.8005 secs.
Slowest:      0.1121 secs.
Fastest:      0.0003 secs.
Average:      0.0116 secs.
Requests/sec: 8619.9772

armv7l - Samsung Exynos4412 Prime Cortex-A9 Quad Core 1.7Ghz (4 procesos)

Modo normal:

Total:        18.7339 secs.
Slowest:      0.2366 secs.
Fastest:      0.0073 secs.
Average:      0.0374 secs.
Requests/sec: 2668.9630

Modo clúster:

Total:        14.0123 secs.
Slowest:      0.2929 secs.
Fastest:      0.0010 secs.
Average:      0.0280 secs.
Requests/sec: 3568.2954

Como podemos ver el incremento de peticiones por segundo es del 35%. Si tenemos en cuenta que nuestra aplicación no será un simple hello world, esta diferencia puede ser importante.

TL;DR

A continuación, el fichero server.js entero para que podáis copiar y pegar fácilmente:

import cluster from 'cluster'
import os from 'os'

const workers = os.cpus().length

cluster.setupMaster({ exec: 'app.js' })

function log(msg) {
  console.log(`[SERVER] ${new Date().toISOString().replace(/T/, ' ').replace(/\..+/, '')} ${msg}`)
}

log(`Master with pid ${process.pid} starting...`)

for (let i = 0; i < workers; i++) {
  cluster.fork()
}

cluster.on('exit', (worker, code, signal) => {
  log(`worker with pid ${worker.process.pid} died. Restarting...`)
  cluster.fork()
})

cluster.on('online', worker => {
  log(`Worker with pid ${worker.process.pid} started`)
})

También está disponible el ejemplo en GitHub a través del repositorio felixsanz/nodejs-cluster-module.

Compartir en

Facebook Twitter Google+ LinkedIn