Cómo escalar su servidor Node.js usando clústeres

La escalabilidad es un tema candente en tecnología, y cada lenguaje de programación o marco proporciona su propia forma de manejar grandes cargas de tráfico.

Hoy, veremos un ejemplo sencillo y sencillo sobre la agrupación en clústeres de Node.js. Esta es una técnica de programación que le ayudará a paralelizar su código y acelerar el rendimiento.

“Una sola instancia de Node.js se ejecuta en un solo hilo. Para aprovechar los sistemas de múltiples núcleos, el usuario a veces querrá lanzar un clúster de procesos Node.js para manejar la carga ".

- Documentación de Node.js

Vamos a crear un servidor web simple usando Koa, que es muy similar a Express en términos de uso.

El ejemplo completo está disponible en este repositorio de Github.

Que vamos a construir

Construiremos un servidor web simple que actuará de la siguiente manera:

  1. Nuestro servidor recibirá una POSTsolicitud, fingiremos que el usuario nos está enviando una foto.
  2. Copiaremos una imagen del sistema de archivos a un directorio temporal.
  3. Lo voltearemos verticalmente usando Jimp, una biblioteca de procesamiento de imágenes para Node.js.
  4. Lo guardaremos en el sistema de archivos.
  5. Lo eliminaremos y enviaremos una respuesta al usuario.

Por supuesto, esta no es una aplicación del mundo real, pero se acerca bastante a una. Solo queremos medir los beneficios de utilizar la agrupación en clústeres.

Configurar el proyecto

Voy a usar yarnpara instalar mis dependencias e inicializar mi proyecto:

Dado que Node.js es de un solo subproceso, si nuestro servidor web falla, permanecerá inactivo hasta que algún otro proceso lo reinicie. Así que instalaremos para siempre, un simple demonio que reiniciará nuestro servidor web si alguna vez falla.

También instalaremos Jimp, Koa y Koa Router.

Empezando con Koa

Esta es la estructura de carpetas que necesitamos crear:

Tendremos una srccarpeta que contiene dos archivos JavaScript: cluster.jsy standard.js.

El primero será el archivo donde experimentaremos con el clustermódulo. El segundo es un servidor Koa simple que funcionará sin agrupaciones.

En el moduledirectorio, crearemos dos archivos: job.jsy log.js.

job.jsrealizará el trabajo de manipulación de imágenes. log.jsregistrará cada evento que ocurra durante ese proceso.

El módulo de registro

El módulo de registro será una función simple que tomará un argumento y lo escribirá en stdout(similar a console.log).

También agregará la marca de tiempo actual al comienzo del registro. Esto nos permitirá verificar cuándo comenzó un proceso y medir su desempeño.

El módulo de trabajo

Seré honesto, este no es un guión hermoso y súper optimizado. Es un trabajo fácil que nos permitirá estresar nuestra máquina.

El servidor web Koa

Vamos a crear un servidor web muy simple. Responderá en dos rutas con dos métodos HTTP diferentes.

Podremos realizar una solicitud GET el //localhost:3000/. Koa responderá con un texto simple que nos mostrará el PID actual (ID de proceso).

La segunda ruta solo aceptará solicitudes POST en la /flipruta y realizará el trabajo que acabamos de crear.

También crearemos un middleware simple que establecerá un X-Response-Timeencabezado. Esto nos permitirá medir el rendimiento.

¡Excelente! Ahora podemos comenzar a escribir en nuestro servidor node ./src/standard.jsy probar nuestras rutas.

El problema

Usemos mi máquina como servidor:

  • Macbook Pro de 15 pulgadas 2016
  • Intel Core i7 de 2,7 GHz
  • 16 GB de RAM

Si hago una solicitud POST, el script anterior me enviará una respuesta en ~ 3800 milisegundos. No tan mal, dado que la imagen en la que estoy trabajando actualmente es de unos 6,7 MB.

Puedo intentar hacer más solicitudes, pero el tiempo de respuesta no disminuirá demasiado. Esto se debe a que las solicitudes se realizarán de forma secuencial.

Entonces, ¿qué pasaría si intentara realizar 10, 100, 1000 solicitudes simultáneas?

Hice un simple script Elixir que realiza múltiples solicitudes HTTP concurrentes:

Elegí Elixir porque es muy fácil crear procesos paralelos, ¡pero puedes usar lo que prefieras!

Prueba de diez solicitudes simultáneas, sin agrupamiento

Como puede ver, generamos 10 procesos concurrentes de nuestro iex (un REPL de Elixir).

El servidor Node.js copiará inmediatamente nuestra imagen y comenzará a voltearla.

La primera respuesta se registrará después de 16 segundos y la última después de 40 segundos.

¡Una disminución tan dramática del rendimiento! Con solo 10 solicitudes simultáneas,¡Disminuimos el rendimiento del servidor web en un 950%!

Introducción a la agrupación en clústeres

¿Recuerdas lo que mencioné al principio del artículo?

Para aprovechar los sistemas de múltiples núcleos, el usuario a veces querrá iniciar un clúster de procesos Node.js para manejar la carga.

Dependiendo del servidor que ejecutemos nuestra aplicación Koa, podríamos tener un número diferente de núcleos.

Cada núcleo será responsable de manejar la carga individualmente. Básicamente, cada solicitud HTTP será satisfecha por un solo núcleo.

Entonces, por ejemplo, mi máquina, que tiene ocho núcleos, manejará ocho solicitudes simultáneas.

Ahora podemos contar cuántas CPU tenemos gracias al osmódulo:

El cpus()método devolverá una matriz de objetos que describen nuestras CPU. Podemos vincular su longitud a una constante que se llamará numWorkers, porque esa es la cantidad de trabajadores que usaremos.

Ahora estamos listos para solicitar el clustermódulo.

Ahora necesitamos una forma de dividir nuestro proceso principal en Nprocesos distintos.

Llamaremos a nuestro proceso principal mastery a los demás procesos workers.

El clustermódulo Node.js ofrece un método llamado isMaster. Devolverá un valor booleano que nos dirá si el proceso actual está dirigido por un trabajador o un maestro:

Excelente. La regla de oro aquí es que no queremos presentar nuestra solicitud de Koa bajo el proceso maestro.

Queremos crear una aplicación Koa para cada trabajador, por lo que cuando llegue una solicitud, el primer trabajador gratuito se encargará de ella.

El cluster.fork()método se ajustará a nuestro propósito:

Bien, al principio eso puede ser un poco complicado.

Como puede ver en el script anterior, si nuestro script ha sido ejecutado por el proceso maestro, declararemos una constante llamada workers. Esto creará un trabajador para cada núcleo de nuestra CPU y almacenará toda la información sobre ellos.

Si no está seguro de la sintaxis adoptada, usar […Array(x)].map()es lo mismo que:

Simplemente prefiero usar valores inmutables mientras desarrollo una aplicación de alta concurrencia.

Añadiendo Koa

Como dijimos antes, no queremos presentar nuestra solicitud de Koa bajo el proceso maestro.

Copiemos la estructura de nuestra aplicación Koa en la elsedeclaración, para estar seguros de que será atendida por un trabajador:

Como puede ver, también agregamos un par de detectores de eventos en la isMasterdeclaración:

El primero nos dirá que se ha generado un nuevo trabajador. El segundo creará un nuevo trabajador cuando otro trabajador falle.

De esa forma, el proceso maestro solo será responsable de crear nuevos trabajadores y orquestarlos. Cada trabajador servirá una instancia de Koa a la que se podrá acceder en el :3000puerto.

Prueba de diez solicitudes simultáneas, con agrupación

Como puede ver, obtuvimos nuestra primera respuesta después de unos 10 segundos y la última después de unos 14 segundos. ¡Es una mejora asombrosa con respecto al tiempo de respuesta de 40 segundos anterior!

Hicimos diez solicitudes simultáneas y el servidor de Koa tomó ocho de ellas de inmediato. Cuando el primer trabajador envió su respuesta al cliente, tomó una de las solicitudes restantes y la procesó.

Conclusión

Node.js tiene una capacidad asombrosa para manejar grandes cargas, pero no sería prudente detener una solicitud hasta que el servidor finalice su proceso.

De hecho, los servidores web Node.js pueden manejar miles de solicitudes simultáneas solo si envía inmediatamente una respuesta al cliente.

Una mejor práctica sería agregar una interfaz de mensajería pub / sub usando Redis o cualquier otra herramienta increíble. Cuando el cliente envía una solicitud, el servidor inicia una comunicación en tiempo real con otros servicios. Esto se hace cargo de trabajos costosos.

Los equilibradores de carga también ayudarían mucho a dividir las cargas de tráfico elevado.

Una vez más, la tecnología nos brinda infinitas posibilidades, ¡y estamos seguros de encontrar la solución adecuada para escalar nuestra aplicación hasta el infinito y más allá!