Saltar al contenido principal

Módulo Cluster

🧩 The Cluster Module en Node.js

  1. 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.
  2. Funcionamiento básico:
    • Usa el método fork para 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.
  3. 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

  1. 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).
  2. 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

  1. Mecanismo:
    • Asigna cada solicitud al siguiente worker en una lista cíclica.
    • Al llegar al final de la lista, vuelve al principio.
  2. Ventajas:
    • Simple y ampliamente usado.
    • Ideal para carga pareja en entornos con trabajadores similares.
  3. 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

  1. Objetivo:

    • Escalar una aplicación Node.js creando múltiples procesos que se distribuyen la carga.
  2. 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");
    }
  3. Explicación del flujo:

    • Cuando se ejecuta clusteredApp.js:
      • isMaster === true → se ejecuta el proceso maestro que hace fork() tantas veces como CPUs disponibles.
      • Cada fork() reinicia el mismo script, pero esta vez en modo worker, donde isMaster === false, y entonces se ejecuta el archivo app.js.
  4. 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 cluster usa child_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

  1. Ejecutar:

    node clusteredApp
  2. En una máquina con 4 CPUs, deberías ver:

    Started 14107
    Started 14099
    Started 14102
    Started 14101
  3. 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

  1. 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.
  2. 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 0 y 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 cluster para construir sistemas más confiables sin herramientas externas de monitoreo o reinicio.

🔁 Zero-downtime restart con el módulo cluster

  1. 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.
  2. 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:

    1. Detiene un worker mediante worker.disconnect().
    2. Espera a que salga.
    3. Crea un nuevo worker (cluster.fork()).
    4. Espera a que el nuevo worker esté listening.
    5. Luego pasa al siguiente worker recursivamente.
  • 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

  1. Iniciar la app normalmente:

    node clusteredApp.js
  2. Obtener el PID del proceso master:

    ps af
  3. Enviar señal de reinicio:

    kill -SIGUSR2 <PID>
  4. Salida esperada:

    Restarting workers
    Stopping worker: 19389
    Started 19407
    Stopping worker: 19390
    Started 19409

📊 Verificación con siege

  • Se puede usar siege durante el reinicio para comprobar que la disponibilidad no se ve afectada significativamente.

🛠️ Alternativa recomendada

  • pm2: utilidad basada en cluster que ofrece balanceo, monitoreo, reinicios sin downtime, y más.