Configurar un clúster de Node.js

C

Todos sabemos que Node.js es excelente para manejar muchos eventos de forma asincrónica, pero lo que mucha gente no sabe es que todo esto se hace en un solo hilo. Node.js en realidad no es multiproceso, por lo que todas estas solicitudes solo se manejan en el bucle de eventos de un solo hilo.

Entonces, ¿por qué no aprovechar al máximo su procesador de cuatro núcleos utilizando un clúster Node.js? Esto iniciará varias instancias de su código para manejar aún más solicitudes. Esto puede parecer un poco difícil, pero en realidad es bastante fácil de hacer con racimo módulo, que se introdujo en Node.js v0.8.

Obviamente, esto es útil para cualquier aplicación que pueda dividir el trabajo entre diferentes procesos, pero es especialmente importante para las aplicaciones que manejan muchas solicitudes de IO, como un sitio web.

Desafortunadamente, debido a las complejidades del procesamiento paralelo, agrupar una aplicación en un servidor no siempre es sencillo. ¿Qué hace cuando necesita varios procesos para escuchar en el mismo puerto? Recuerde que solo un proceso puede acceder a un puerto en un momento dado. La solución ingenua aquí es configurar cada proceso para escuchar en un puerto diferente y luego configurar Nginx para equilibrio de carga solicitudes entre los puertos.

Esta es una solución viable, pero requiere mucho más trabajo para configurar y configurar cada proceso, sin mencionar la configuración de Nginx. Con esta solución, solo está agregando más cosas para que usted las administre.

En su lugar, puede bifurcar el proceso maestro en varios procesos secundarios (por lo general, tiene un hijo por procesador). En este caso, los niños pueden compartir un puerto con el padre (gracias a la comunicación entre procesos, o IPC), por lo que no es necesario preocuparse por administrar varios puertos.

Esto es exactamente lo que cluster módulo lo hace por usted.

Trabajar con el módulo de clúster

Agrupar una aplicación es extremadamente simple, especialmente para código de servidor web como Rápido proyectos. Todo lo que necesitas hacer es esto:

var cluster = require('cluster');
var express = require('express');
var numCPUs = require('os').cpus().length;

if (cluster.isMaster) {
    for (var i = 0; i < numCPUs; i++) {
        // Create a worker
        cluster.fork();
    }
} else {
    // Workers share the TCP connection in this server
    var app = express();

    app.get("https://Pharos.sh.com/", function (req, res) {
        res.send('Hello World!');
    });

    // All workers use this port
    app.listen(8080);
}

La funcionalidad del código se divide en dos partes, el código maestro y el código del trabajador. Esto se hace en la sentencia if (if (cluster.isMaster) {...}). El único propósito del maestro aquí es crear todos los trabajadores (la cantidad de trabajadores creados se basa en la cantidad de CPU disponibles), y los trabajadores son responsables de ejecutar instancias separadas del servidor Express.

Cuando un trabajador se bifurca fuera del proceso principal, vuelve a ejecutar el código desde el principio del módulo. Cuando el trabajador llega a la sentencia if, devuelve false para cluster.isMaster, entonces, creará la aplicación Express, una ruta, y luego escuchará en el puerto 8080. En el caso de un procesador de cuatro núcleos, tendríamos cuatro trabajadores engendrados, todos escuchando en el mismo puerto para recibir solicitudes.

Pero, ¿cómo se reparten las solicitudes entre los trabajadores? Obviamente, no pueden (y no deberían) estar todos escuchando y respondiendo a cada solicitud que recibimos. Para manejar esto, en realidad hay un balanceador de carga integrado dentro del cluster módulo que maneja la distribución de solicitudes entre los diferentes trabajadores. En Linux y OSX (pero no en Windows), el round-robin (cluster.SCHED_RR) está en vigor de forma predeterminada. La única otra opción de programación disponible es dejarla en manos del sistema operativo (cluster.SCHED_NONE), que es el predeterminado en Windows.

La política de programación se puede establecer en cluster.schedulingPolicy o configurándolo en la variable de entorno NODE_CLUSTER_SCHED_POLICY (con valores de ‘rr’ o ‘none’).

Quizás también se pregunte cómo diferentes procesos pueden compartir un solo puerto. La parte difícil de ejecutar tantos procesos que manejan solicitudes de red es que tradicionalmente solo uno puede tener un puerto abierto a la vez. El gran beneficio de cluster es que maneja el uso compartido de puertos por usted, por lo que cualquier puerto que tenga abierto, como para un servidor web, será accesible para todos los niños. Esto se hace a través de IPC, lo que significa que el maestro simplemente envía el identificador del puerto a cada trabajador.

Gracias a características como esta, la agrupación en clústeres es muy fácil.

cluster.fork () vs child_process.fork ()

Si tiene experiencia previa con child_processes fork() método, entonces puede estar pensando que cluster.fork() es algo similar (y lo son, en muchos sentidos), por lo que explicaremos algunas diferencias clave sobre estos dos métodos de bifurcación en esta sección.

Hay algunas diferencias principales entre cluster.fork() y child_process.fork(). los child_process.fork() El método es un poco más bajo y requiere que pase la ubicación (ruta del archivo) del módulo como argumento, además de otros argumentos opcionales como el directorio de trabajo actual, el usuario propietario del proceso, las variables de entorno y más.

Otra diferencia es que cluster inicia la ejecución del trabajador desde el principio del mismo módulo desde el que se ejecutó. Entonces, si el punto de entrada de su aplicación es index.js, pero el trabajador se genera en cluster-my-app.js, entonces todavía comenzará su ejecución desde el principio en index.js. child_process es diferente en que genera la ejecución en cualquier archivo que se le pase, y no necesariamente en el punto de entrada de la aplicación dada.

Es posible que ya haya adivinado que el cluster módulo realmente utiliza el child_process módulo de abajo para crear los niños, que se hace con child_processes propio fork() método, lo que les permite comunicarse a través de IPC, que es cómo se comparten los identificadores de puerto entre los trabajadores.

Para ser claros, la bifurcación en Node es muy diferente a una Horquilla POISIX ya que en realidad no clona el proceso actual, sino que inicia una nueva instancia V8.

Aunque esta es una de las formas más fáciles de realizar múltiples subprocesos, debe usarse con precaución. El hecho de que puedas generar 1.000 trabajadores no significa que debas hacerlo. Cada trabajador consume recursos del sistema, por lo que solo genera los que realmente se necesitan. Los documentos de Node establecen que, dado que cada proceso hijo es una nueva instancia de V8, debe esperar un tiempo de inicio de 30 ms para cada uno y al menos 10 MB de memoria por instancia.

Manejo de errores

Entonces, ¿qué hace cuando uno (¡o más!) De sus trabajadores muere? El objetivo de la agrupación en clústeres se pierde básicamente si no puede reiniciar los trabajadores después de que se bloquean. Por suerte para ti el cluster el módulo se extiende EventEmitter y proporciona un evento de “salida”, que le indica cuándo muere uno de sus hijos trabajadores.

Puede usar esto para registrar el evento y reiniciar el proceso:

cluster.on('exit', function(worker, code, signal) {
    console.log('Worker %d died with code/signal %s. Restarting worker...', worker.process.pid, signal || code);
    cluster.fork();
});

Ahora, después de solo 4 líneas de código, ¡es como si tuviera su propio administrador de procesos interno!

Comparaciones de desempeño

Bien, ahora a la parte interesante. Veamos cuánto nos ayuda realmente la agrupación en clústeres.

Para este experimento, configuré una aplicación web similar al código de ejemplo que mostré arriba. Pero la mayor diferencia es que estamos simulando el trabajo que se realiza dentro de la ruta Express utilizando el dormir módulo y devolviendo un montón de datos aleatorios al usuario.

Aquí está la misma aplicación web, pero con agrupamiento:

var cluster = require('cluster');
var crypto = require('crypto');
var express = require('express');
var sleep = require('sleep');
var numCPUs = require('os').cpus().length;

if (cluster.isMaster) {
    for (var i = 0; i < numCPUs; i++) {
        // Create a worker
        cluster.fork();
    }
} else {
    // Workers share the TCP connection in this server
    var app = express();

    app.get("https://Pharos.sh.com/", function (req, res) {
        // Simulate route processing delay
        var randSleep = Math.round(10000 + (Math.random() * 10000));
        sleep.usleep(randSleep);

        var numChars = Math.round(5000 + (Math.random() * 5000));
        var randChars = crypto.randomBytes(numChars).toString('hex');
        res.send(randChars);
    });

    // All workers use this port
    app.listen(8080);
}

Y aquí está el código de ‘control’ a partir del cual haremos nuestras comparaciones. Es esencialmente lo mismo, solo que sin cluster.fork():

var crypto = require('crypto');
var express = require('express');
var sleep = require('sleep');

var app = express();

app.get("https://Pharos.sh.com/", function (req, res) {
    // Simulate route processing delay
    var randSleep = Math.round(10000 + (Math.random() * 10000));
    sleep.usleep(randSleep);

    var numChars = Math.round(5000 + (Math.random() * 5000));
    var randChars = crypto.randomBytes(numChars).toString('hex');
    res.send(randChars);
});

app.listen(8080);

Para simular una gran carga de usuarios, usaremos una herramienta de línea de comandos llamada Cerco, que podemos usar para realizar un montón de solicitudes simultáneas a la URL de nuestra elección.

Siege también es bueno porque rastrea las métricas de rendimiento, como la disponibilidad, el rendimiento y la tasa de solicitudes manejadas.

Aquí está el comando Siege que usaremos para las pruebas:

$ siege -c100 -t60s http://localhost:8080/

Después de ejecutar este comando para ambas versiones de la aplicación, estos son algunos de los resultados más interesantes:

Tipo Total de solicitudes gestionadas Solicitudes / segundo Tiempo medio de respuesta Rendimiento

Sin agrupamiento346758,691,18 segundos0,84 MB / s
Agrupación (4 procesos)11146188,720.03 segundos2,70 MB / s

Como puede ver, la aplicación agrupada tiene una mejora de alrededor de 3.2x sobre la aplicación de proceso único para casi todas las métricas enumeradas, excepto el tiempo de respuesta promedio, que tiene una mejora mucho más significativa.

 

About the author

Ramiro de la Vega

Bienvenido a Pharos.sh

Soy Ramiro de la Vega, Estadounidense con raíces Españolas. Empecé a programar hace casi 20 años cuando era muy jovencito.

Espero que en mi web encuentres la inspiración y ayuda que necesitas para adentrarte en el fantástico mundo de la programación y conseguir tus objetivos por difíciles que sean.

Add comment

Sobre mi

Últimos Post

Etiquetas

Esta web utiliza cookies propias para su correcto funcionamiento. Al hacer clic en el botón Aceptar, aceptas el uso de estas tecnologías y el procesamiento de tus datos para estos propósitos. Más información
Privacidad