Módulo Cluster
🧩 The Cluster Module en Node.js
- Propósito del módulo:
- Distribuye la carga de una aplicación entre varias instancias que corren en una misma máquina.
- Es parte de las bibliotecas núcleo de Node.js.
- Funcionamiento básico:
- Usa el método
forkpara crear múltiples instancias (procesos worker) de la misma aplicación. - Las conexiones entrantes son gestionadas por un proceso maestro y distribuidas entre los procesos worker.
- Usa el método
- Ventaja clave:
- Permite aprovechar múltiples núcleos de CPU para mejorar el rendimiento y la escalabilidad.
⚙️ Comportamiento del Cluster Module según versión
-
Node.js 0.8 y 0.10:
- El módulo comparte el mismo socket entre los workers.
- La distribución de carga queda en manos del sistema operativo.
- Problema: el SO no balancea bien las solicitudes de red, solo planifica procesos.
- Resultado: distribución desigual (algunos workers reciben más carga).
-
A partir de Node.js 0.11.2:
-
Se incluye el algoritmo de balanceo Round Robin en el proceso maestro.
-
Se logra una distribución más equitativa de las solicitudes entre los workers.
-
Por defecto está habilitado en todas las plataformas excepto en Windows.
-
Se puede configurar globalmente usando:
js
CopiarEditar
cluster.schedulingPolicy = cluster.SCHED_RR // o cluster.SCHED_NONE
-
🔄 Round Robin Algorithm
- Mecanismo:
- Asigna cada solicitud al siguiente worker en una lista cíclica.
- Al llegar al final de la lista, vuelve al principio.
- Ventajas:
- Simple y ampliamente usado.
- Ideal para carga pareja en entornos con trabajadores similares.
- Limitaciones:
- No considera métricas como carga del servidor o tiempo de respuesta.
- Existen algoritmos más avanzados que permiten:
- Asignar prioridades.
- Seleccionar el servidor menos cargado.
- Usar el de respuesta más rápida.
📚 Más información sobre evolución del módulo:
⚙️ Escalado con el módulo cluster
-
Objetivo:
- Escalar una aplicación Node.js creando múltiples procesos que se distribuyen la carga.
-
Archivo
clusteredApp.js:var cluster = require("cluster");
var os = require("os");
if (cluster.isMaster) {
var cpus = os.cpus().length;
for (var i = 0; i < cpus; i++) {
cluster.fork();
}
} else {
require("./app");
} -
Explicación del flujo:
- Cuando se ejecuta
clusteredApp.js:isMaster === true→ se ejecuta el proceso maestro que hacefork()tantas veces como CPUs disponibles.- Cada
fork()reinicia el mismo script, pero esta vez en modo worker, dondeisMaster === false, y entonces se ejecuta el archivoapp.js.
- Cuando se ejecuta
-
Cada worker es independiente:
- Cada worker es un proceso Node.js separado, con su propio event loop, memoria y módulos cargados.
🔁 Patrón recurrente de uso
if (cluster.isMaster) {
// fork()
} else {
// do work
}
Este patrón se repite siempre que queramos usar cluster.
🧵 Comunicación Master–Worker
- Internamente
clusterusachild_process.fork(). - Hay un canal de comunicación entre el master y los workers.
- Se pueden enviar mensajes fácilmente:
Object.keys(cluster.workers).forEach(function (id) {
cluster.workers[id].send("Hello from the master");
});
🚀 Ejecución del servidor en modo cluster
-
Ejecutar:
node clusteredApp -
En una máquina con 4 CPUs, deberías ver:
Started 14107
Started 14099
Started 14102
Started 14101 -
Luego al hacer requests a
http://localhost:8080:- Se recibe respuesta con un PID diferente cada vez → la carga se distribuye entre los workers.
📈 Prueba de carga (con siege)
- Ejecutar:
siege -c200 -t10S http://localhost:8080 - Resultado esperado (referencia con Node.js 0.10 en Linux):
- Sin cluster: ~90 transacciones/segundo con 20% de CPU.
- Con cluster: ~270 transacciones/segundo (3× más) con 90% de CPU.
🔁 Resiliencia y disponibilidad con el módulo cluster
- Beneficio adicional del escalado:
- Permite mantener un nivel mínimo de servicio aunque algunas instancias fallen.
- Este concepto se conoce como resiliencia y está vinculado a la disponibilidad del sistema.
- Idea clave:
- Al tener múltiples instancias corriendo, si una falla, las otras siguen atendiendo solicitudes.
- Esto genera redundancia y tolerancia a fallos.
💥 Simulación de fallo en app.js
Se modifica el código de app.js para que falle aleatoriamente luego de 1 a 3 segundos:
setTimeout(function () {
throw new Error("Ooops");
}, Math.ceil(Math.random() * 3) * 1000);
- Esto simula caídas inesperadas del proceso.
- En sistemas reales, sin supervisión, el servicio dejaría de responder temporalmente.
🔄 Recuperación automática en clusteredApp.js
Se modifica el proceso maestro para detectar si un worker se cayó y crear uno nuevo:
cluster.on("exit", function (worker, code) {
if (code !== 0 && !worker.suicide) {
console.log("Worker crashed. Starting a new worker");
cluster.fork();
}
});
worker.suicide: indica si la terminación fue intencional (por el master).- Si el código de salida no es
0y no fue suicidio, se considera un crash → se genera un nuevo worker.
📈 Prueba de resiliencia con siege
Al realizar una prueba de carga con siege (como antes), los resultados muestran:
Transactions: 3027 hits
Availability: 99.31 %
Failed transactions: 21
- A pesar de las caídas programadas, el sistema manejó más de 3000 requests.
- Solo 21 fallaron, en su mayoría por interrupción de conexiones en curso durante un crash.
- Error típico:
[error] socket: read error Connection reset by peer
🧠 Conclusiones clave
- La solución demuestra ser resistente a fallas frecuentes.
- El sistema mantiene una alta disponibilidad, incluso si los procesos se reinician constantemente.
- Es un ejemplo útil de cómo usar
clusterpara construir sistemas más confiables sin herramientas externas de monitoreo o reinicio.
🔁 Zero-downtime restart con el módulo cluster
- Motivación:
- Es necesario reiniciar apps Node.js para aplicar actualizaciones de código.
- En aplicaciones con SLA o despliegue continuo, es fundamental mantener disponibilidad durante esos reinicios.
- Estrategia:
- Reiniciar los workers uno por vez, permitiendo que el resto siga atendiendo requests.
⚙️ Implementación (en el proceso master)
-
Se captura la señal
SIGUSR2:process.on("SIGUSR2", function () {
console.log("Restarting workers");
const workers = Object.keys(cluster.workers);
function restartWorker(i) {
if (i >= workers.length) return;
const worker = cluster.workers[workers[i]];
console.log("Stopping worker: " + worker.process.pid);
worker.disconnect();
worker.on("exit", function () {
if (!worker.exitedAfterDisconnect) return;
const newWorker = cluster.fork();
newWorker.on("listening", function () {
restartWorker(i + 1);
});
});
}
restartWorker(0);
}); -
Se define una función
restartWorker(i)que:- Detiene un worker mediante
worker.disconnect(). - Espera a que salga.
- Crea un nuevo worker (
cluster.fork()). - Espera a que el nuevo worker esté
listening. - Luego pasa al siguiente worker recursivamente.
- Detiene un worker mediante
-
Este proceso asegura que nunca todos los workers estén inactivos al mismo tiempo.
💡 Notas adicionales
- No funciona bien en Windows, ya que depende de señales UNIX.
- Alternativas: escuchar sockets, pipes o entrada estándar.
🧪 Pruebas
-
Iniciar la app normalmente:
node clusteredApp.js -
Obtener el PID del proceso master:
ps af -
Enviar señal de reinicio:
kill -SIGUSR2 <PID> -
Salida esperada:
Restarting workers
Stopping worker: 19389
Started 19407
Stopping worker: 19390
Started 19409
📊 Verificación con siege
- Se puede usar
siegedurante el reinicio para comprobar que la disponibilidad no se ve afectada significativamente.
🛠️ Alternativa recomendada
- pm2: utilidad basada en
clusterque ofrece balanceo, monitoreo, reinicios sin downtime, y más.