Aprenda a crear aplicaciones NodeJs escalables y confiables. ¡Obtenga los mejores consejos y trucos para garantizar que sus aplicaciones estén optimizadas para tener éxito!
En un mundo cada vez más digital, aprovechar los servicios de desarrollo de Node JS para crear aplicaciones escalables y de alto rendimiento no es sólo un beneficio, sino una necesidad. Este artículo profundiza en el mundo de NodeJS, un entorno de ejecución preferido por muchos desarrolladores de todo el mundo, que ofrece mejores prácticas, herramientas clave y patrones estratégicos para aumentar el rendimiento de las aplicaciones escalables de Node JS. Ya sea que sea un novato que se sumerge en los servicios de desarrollo de Node JS o un desarrollador experimentado que desea perfeccionar su aplicación, este artículo lo guiará a través de los pasos fundamentales para transformar su aplicación NodeJS de simplemente funcionar a excelente. Aproveche el poder de estos conocimientos y estrategias para crear aplicaciones Node JS escalables que hagan más que simplemente cumplir con sus expectativas de rendimiento: las superan.
¿Qué es NodeJS?
NodeJs es un tiempo de ejecución de JavaScript creado en el motor JavaScript V8 de Chrome que utiliza un modelo de E/S sin bloqueo y controlado por eventos. Es decir, con NodeJs, los desarrolladores pueden ejecutar código Javascript en el lado del servidor, lo que permite a los desarrolladores de Javascript escribir aplicaciones front-end y back-end. La naturaleza de tener un único lenguaje de programación en toda la pila es solo uno de los muchos puntos de venta de NodeJ. Algunos de los otros son:
- NodeJs es asíncrono y está controlado por eventos, lo que significa que cuando se realiza una operación, si esa operación tarda mucho en completarse, la aplicación puede continuar realizando otras operaciones mientras espera que se complete la primera. Esta característica hace que las aplicaciones NodeJs sean eficientes y rápidas.
- Al estar construido sobre el motor Javascript V8, NodeJs es muy rápido en la ejecución de código.
- NodeJs tiene una gran comunidad de desarrolladores. Esto significa que hay muchos recursos que aprender cuando uno está atascado y muchas bibliotecas que usar para facilitar el desarrollo.
- NodeJs es multiplataforma. Puede ejecutarse en Windows, Linux y Mac OS. Y como es básicamente Javascript, pero del lado del servidor, es fácil de aprender, usar y encontrar desarrolladores. No es difícil crear un equipo que pueda escribir aplicaciones NodeJs, React Native y ReactJS para cubrir todas las partes del proceso de desarrollo.
- NodeJs es liviano. No consume muchos recursos y es fácil de escalar. En el desarrollo backend, escalar significa que una aplicación puede manejar más solicitudes por segundo sin fallar ni ralentizarse, lo que hace que la experiencia del usuario sea más fluida. Dado que el escalado es el objetivo principal de este artículo, lo discutiremos con más detalle.
Comprender el bucle de eventos en NodeJS
Antes de entrar en escala, echemos un vistazo rápido a qué es el bucle de eventos. El bucle de eventos es un concepto fundamental en el desarrollo de NodeJs. Es un mecanismo de un solo subproceso que se ejecuta incesantemente y gestiona la ejecución de tareas como leer archivos, consultar bases de datos o realizar solicitudes de red de forma asíncrona en una aplicación NodeJs. En lugar de esperar a que se complete una tarea, los NodeJ registran funciones de devolución de llamada que se ejecutarán tan pronto como se complete la operación en cuestión. Esta naturaleza sin bloqueo de NodeJs lo hace muy rápido y altamente escalable si se utilizan las técnicas adecuadas.
¿Qué es el tamaño?
La escalabilidad, en el sentido más simple, es la capacidad de una aplicación para manejar muchas solicitudes por segundo a la vez. Hay dos términos más en la terminología de escala: escala vertical y horizontal. El escalado vertical, también conocido como escalado vertical, se refiere al proceso en el que se mejora la capacidad de una aplicación para manejar solicitudes actualizando sus recursos, como agregar más RAM, aumentar la CPU, etc. El escalado horizontal, por otro lado, conocido como escalamiento horizontal, es el proceso mediante el cual se agregan más instancias al servidor.
Escalando en NodeJS con múltiples instancias
Primero, hagamos la pregunta: ¿Por qué escalar? En pocas palabras, en nuestra era de muchos usuarios, una aplicación que no puede manejar todas las solicitudes que recibe de todos sus usuarios no puede esperar permanecer en el juego.
Y como desarrolladores backend, debemos asegurarnos de que nuestra aplicación sea rápida, receptiva y segura. El escalado ayuda a los desarrolladores a lograr un mejor rendimiento, ya que pueden distribuir la carga de trabajo entre múltiples instancias o nodos, manejar más tráfico y crear tolerancia a fallas, que es el proceso de tener múltiples instancias de modo que si una falla, las otras instancias puedan tomar el control y mantenerlas. la aplicación Node JS ejecutándose.
Ahora, mientras que otros lenguajes de programación como Go pueden manejar solicitudes simultáneas de forma predeterminada, NodeJs, debido a su naturaleza de un solo subproceso, maneja las operaciones de manera un poco diferente. Por tanto, las técnicas utilizadas para la escalada también varían.
NodeJs es rápido. Muy rapido. Sin embargo, debido a su naturaleza de un solo subproceso, es posible que no pueda manejar múltiples subprocesos, ya que solo puede ejecutar un subproceso a la vez. Tener demasiadas solicitudes al mismo tiempo puede provocar el bloqueo del bucle de eventos.
Cómo escalar aplicaciones Node JS
Existen diferentes métodos para escalar aplicaciones Node.JS. Veamos algunos de ellos brevemente, como la arquitectura de microservicios, el módulo de clúster y la optimización de la base de datos.
Arquitectura de microservicios
La arquitectura de microservicios de Node JS es el proceso de desarrollo de software que consta de entidades independientes y débilmente acopladas. Cada servicio es una aplicación Node JS diferente que se desarrolla e implementa, y pueden comunicarse con cada uno a través de solicitudes HTTP o servicios de mensajería como RabbitMQ o Apache Kafka. Este método de desarrollo de software, en lugar de reunir todo en un monorepo, permite a los desarrolladores centrarse en cada servicio de forma independiente e implementar los cambios necesarios sin afectar directamente a los demás. Si bien cabe señalar aquí, las ventajas de los microservicios son un concepto debatido y deben utilizarse con precaución.
Para comprender la arquitectura de los microservicios, veamos un ejemplo hipotético de una aplicación de comercio electrónico. Esta aplicación se puede dividir en microservicios como Producto, Carrito y Pedido. Cada microservicio se desarrolla e implementa de forma independiente.
Por ejemplo, el microservicio de Producto puede ser responsable de gestionar los datos del producto en el sistema. Proporcionaría puntos finales CRUD y expondría una API HTTP que otros microservicios pueden utilizar para interactuar con la información del producto.
El microservicio Cart podría manejar todas las funciones de administración del carrito, como agregar artículos, cambiar cantidades, calcular totales, etc. También expondría una API para que otros microservicios creen carritos y los actualicen. Y el microservicio Order puede permitir la creación de pedidos, el procesamiento de pagos, el seguimiento del estado y más. Proporcionaría API para funciones de pago de carrito y búsqueda de pedidos.
Al separar las preocupaciones en microservicios autónomos y desacoplados, la aplicación se vuelve más fácil de escalar y mantener. Cada microservicio se centra en una capacidad de dominio específica mientras trabaja en conjunto para ofrecer una experiencia de aplicación completa.
Por ejemplo, el microservicio Cart manejaría todas las funciones del carrito de compras: agregar artículos, actualizar cantidades, calcular totales, etc. Gestionaría los datos del carrito en su propia base de datos.
El microservicio de pedidos proporcionaría puntos finales para realizar pedidos, consultar el historial de pedidos e integrar los microservicios de carrito y producto. Sirve como puente entre el carrito y los datos/funcionalidades del producto.
De esta forma, cada equipo de microservicios puede centrarse en su parte específica de la aplicación. El equipo de carrito administra las funciones del carrito, el equipo de producto maneja los datos del producto y las API, y el equipo de pedidos maneja el procesamiento y la integración de pedidos.
En teoría, esta separación de preocupaciones por dominio acelera el desarrollo al dividir el trabajo y reducir la superposición de funciones entre los equipos. También promueve la independencia y los vínculos débiles entre los servicios. Cada microservicio depende menos de otras partes del sistema, lo que reduce los efectos secundarios de los cambios y aumenta la confiabilidad.
Cache
El almacenamiento en caché es una técnica utilizada para mejorar el rendimiento y la escalabilidad de las aplicaciones Node.js mediante el almacenamiento temporal de datos a los que se accede con frecuencia para una búsqueda rápida.
Considere este ejemplo: necesitamos crear una aplicación que busque y muestre datos del museo: imágenes, títulos, descripciones, etc. También hay paginación para permitir a los usuarios ver diferentes páginas de datos.
Cada solicitud paginada puede recuperar 20 elementos de la API pública del museo. Dado que es una API pública, es probable que tenga una velocidad limitada para evitar abusos. Si solicitamos datos de la API en cada cambio de página, rápidamente alcanzaremos estos límites de velocidad.
En su lugar, podemos utilizar el almacenamiento en caché para evitar llamadas API redundantes. Cuando se solicita la primera página de datos, la almacenamos en caché localmente. En visitas posteriores a la página, primero verificamos si los datos están en el caché. En este caso, devolvemos datos almacenados en caché para evitar exceder los límites de velocidad.
El caché proporciona una búsqueda rápida de datos ya obtenidos. Para las API públicas o cualquier dato que no cambie con frecuencia, el almacenamiento en caché puede mejorar enormemente el rendimiento y reducir los costos/límites de los servicios backend.
Una excelente manera de resolver este problema es almacenar en caché los datos utilizando un servicio de almacenamiento en caché como Redis. Funciona así: tomamos los datos de la API de la página número 1 y los almacenamos en Redis, en la memoria.
Luego, cuando el usuario cambia de página a la página 2, enviamos una solicitud a la base de datos del museo como de costumbre.
Pero el almacenamiento en caché realmente demuestra su valor cuando un usuario regresa a una página ya visitada. Por ejemplo, cuando el usuario regresa a la página 1 después de ver otras páginas, en lugar de enviar una nueva solicitud API, primero verificamos si los datos de la página 1 existen en el caché. Si esto sucede, devolvemos los datos almacenados en caché inmediatamente, evitando una llamada API innecesaria.
Solo si el caché no contiene los datos realizamos la solicitud de API, almacenamos la respuesta en el caché y se la devolvemos al usuario. De esta manera, reducimos las solicitudes API duplicadas cuando los usuarios vuelven a visitar las páginas. Al servir desde la caché siempre que sea posible, mejoramos el rendimiento y nos mantenemos dentro de los límites de tasa de API. La caché actúa como un almacén de datos a corto plazo, minimizando las llamadas al backend.
Práctica: Módulo Cluster, Multithreading y Procesos de Trabajo
La teoría sin práctica es sólo la mitad del trabajo realizado. En esta sección, veremos algunas de las técnicas que podemos usar para escalar aplicaciones NodeJs: agrupación de módulos y subprocesos múltiples. Primero usaremos el módulo de clúster integrado de NodeJS y, una vez que comprendamos cómo funciona, usaremos el administrador de procesos, el paquete pm2, para facilitar las cosas. A continuación, cambiaremos un poco el ejemplo y usaremos el módulo de subprocesos de trabajo para crear múltiples subprocesos.
Módulo de agrupación
Ahora bien, dado que NodeJs tiene un solo subproceso, no importa cuántos núcleos tenga, solo utilizará un único núcleo de su CPU. Esto es completamente aceptable para operaciones de entrada/salida, pero si el código consume demasiada CPU, su aplicación Node puede terminar con problemas de rendimiento. Para solucionar este problema, podemos utilizar el módulo de clúster. Este módulo nos permite crear procesos secundarios que comparten el mismo puerto de servidor que el proceso principal.
De esta forma podremos aprovechar todos los núcleos de la CPU. Para entender qué significa esto y cómo funciona, creemos una aplicación NodeJs sencilla que nos servirá de ejemplo.
Comenzaremos creando una nueva carpeta llamada nodeJs-scaling y dentro de esa carpeta crearemos un archivo llamado no-cluster.js. Dentro de este archivo, escribiremos el siguiente fragmento de código:
const http = requerir("http"); servidor constante = http.createServer((req, res) => { si (req.url === " { res.writeHead(200, { "tipo de contenido": "texto/html" }); res.end("Página de inicio"); } else if (req.url === "/página lenta") { res.writeHead(200, { "tipo de contenido": "texto/html" }); // simula una página lenta para (sea i = 0; i < 9000000000; i++) { res.write("Página lenta"); } res.end; // Enviar la respuesta después de que se complete el ciclo } }); servidor.escuchar(5000, => { console.log("Servidor escuchando en el puerto: 5000..."); });
Aquí, comenzamos importando el módulo HTTP integrado de NodeJs. Lo usamos para crear un servidor que tiene dos puntos finales, un punto final base y un punto final de página lenta. Lo que queremos con esta estructura es que cuando vayamos al punto final base, se ejecute y abra la página normalmente. Pero como puede ver, debido al bucle for que se ejecutará cuando lleguemos al punto final de la página lenta, la página tardará mucho en cargarse. Si bien este es un ejemplo simple, es una excelente manera de comprender cómo funciona el proceso.
Ahora, si iniciamos el servidor ejecutando el nodo cluster.js y luego enviamos una solicitud al punto final base a través de CURL, o simplemente abrimos la página en un navegador, se cargará muy rápidamente. Un ejemplo de solicitud CURL es curl -i Ahora, si hacemos lo mismo con curl -i, notaremos que lleva mucho tiempo e incluso puede resultar en un error. Esto se debe a que el bucle for bloquea el bucle de eventos y no puede manejar ninguna otra solicitud hasta que se complete el bucle. Ahora bien, existen algunas formas de resolver este problema. Primero comenzaremos usando el módulo de clúster integrado y luego usaremos una biblioteca útil llamada PM2.
Módulo de clúster integrado
Ahora creemos un nuevo archivo llamado cluster.js en el mismo directorio y escribamos el siguiente fragmento dentro de él:
const clúster = requerir("clúster"); const sistema operativo = requerir("sistema operativo"); const http = requerir("http"); // Comprobar si el proceso actual es el proceso maestro si (cluster.isMaster) { // Obtener el número de CPU const cpus = os.cpus .longitud; console.log(`${cpus} CPU`); } demás { console.log("Proceso de trabajo" + proceso.pid); }
Aquí comenzamos importando el clúster, el sistema operativo y los módulos http.
Lo que haremos a continuación es comprobar si el proceso es el clúster maestro o no; Si es así, estamos registrando el recuento de CPU.
Esta máquina tiene 6, sería diferente para usted dependiendo de su máquina. Cuando ejecutamos node cluster.js deberíamos obtener una respuesta como "6 CPU". Ahora modifiquemos un poco el código:
const clúster = requerir("clúster"); const sistema operativo = requerir("sistema operativo"); const http = requerir("http"); // Comprobar si el proceso actual es el proceso maestro si (cluster.isMaster) { // Obtener el número de CPU const cpus = os.cpus .longitud; console.log(`Bifurcación para ${cpus} CPU`); console.log(`El proceso maestro ${process.pid} se está ejecutando`); // Bifurca el proceso para cada CPU for (sea i = 0; i < cpus; i++) { cluster.fork; } } demás { console.log("Proceso de trabajo" + proceso.pid); servidor constante = http.createServer((req, res) => { si (req.url === " { res.writeHead(200, { "tipo de contenido": "texto/html" }); res.end("Página de inicio"); } else if (req.url === "/página lenta") { res.writeHead(200, { "tipo de contenido": "texto/html" }); // simula una página lenta para (sea i = 0; i < 1000000000; i++) { res.write("Página lenta"); // Usa res.write en lugar de res.end dentro del bucle } res.end; // Enviar la respuesta después de que se complete el ciclo } }); servidor.escuchar(5000, => { console.log("Servidor escuchando en el puerto: 5000..."); }); }
En esta versión actualizada, estamos bifurcando el proceso para cada CPU. También podríamos haber escrito cluster.fork una cantidad máxima de 6 veces (dado que este es el recuento de CPU de la máquina que estamos usando, sería diferente para usted).
Aquí hay un problema: no debemos sucumbir a la tentadora idea de crear más bifurcaciones que CPU, ya que esto creará problemas de rendimiento en lugar de resolverlos. Entonces, lo que estamos haciendo es bifurcar el proceso para cada CPU mediante un bucle for.
Ahora, si ejecutamos node cluster.js, deberíamos obtener una respuesta como esta:
6 CPU El proceso maestro 39340 se está ejecutando Proceso de trabajo39347 Proceso de trabajo39348 Proceso de trabajo39349 Servidor escuchando en el puerto: 5000.... Proceso de trabajo39355 Servidor escuchando en el puerto: 5000.... Servidor escuchando en el puerto: 5000.... Proceso de trabajo39367 Proceso de trabajo39356 Servidor escuchando en el puerto: 5000.... Servidor escuchando en el puerto: 5000.... Servidor escuchando en el puerto: 5000....
Como puede ver, todos estos procesos tienen una ID diferente. Ahora, si intentamos abrir primero el punto final de la página lenta y luego el punto final base, veremos que en lugar de esperar a que se complete el bucle for largo, obtendremos una respuesta más rápida del punto final base.
Esto se debe a que el punto final de la página lenta está siendo manejado por un proceso diferente.
Paquete PM2
En lugar de trabajar con el módulo del clúster en sí, podemos usar un paquete de terceros como pm2. Como lo usaremos en la terminal, lo instalaremos globalmente ejecutando sudo npm i -g pm2. Ahora, creemos un nuevo archivo en el mismo directorio llamado no-cluster.js y rellénelo con el siguiente código:
const http = requerir("http"); servidor constante = http.createServer((req, res) => { si (req.url === " { res.writeHead(200, { "tipo de contenido": "texto/html" }); res.end("Página de inicio"); } else if (req.url === "/página lenta") { res.writeHead(200, { "tipo de contenido": "texto/html" }); // simula una página lenta para (sea i = 0; i < 9000000000; i++) { res.write("Página lenta"); // Usa res.write en lugar de res.end dentro del bucle } res.end; // Enviar la respuesta después de que se complete el ciclo } }); servidor.escuchar(5000, => { console.log("Servidor escuchando en el puerto: 5000...."); });
Ahora que hemos aprendido cómo ejecutar múltiples procesos, aprendamos cómo crear múltiples subprocesos.
Temas variados
Si bien el módulo de clúster nos permite ejecutar múltiples instancias de NodeJs que pueden distribuir cargas de trabajo, el módulo trabajador_threads nos permite ejecutar múltiples subprocesos de aplicaciones en una sola instancia de NodeJs.
Por tanto, el código Javascript se ejecutará en paralelo. Debemos tener en cuenta aquí que el código que se ejecuta en un subproceso de trabajo se ejecuta en un proceso secundario separado, lo que evita que bloquee nuestra aplicación principal.
Veamos nuevamente este proceso en acción. Creemos un nuevo archivo llamado main-thread.js y agreguemos el siguiente código:
const http = requerir("http"); const { Trabajador } = require("worker_threads"); servidor constante = http.createServer((req, res) => { si (req.url === " { res.writeHead(200, { "tipo de contenido": "texto/html" }); res.end("Página de inicio"); } else if (req.url === "/página lenta") { // Crea un nuevo trabajador trabajador constante = nuevo trabajador("./worker-thread.js"); trabajador.on("mensaje", (j) => { res.writeHead(200, { "tipo de contenido": "texto/html" }); res.end("página lenta" + j); // Envía la respuesta después de que se complete el ciclo }); } }); servidor.escuchar(5000, => { console.log("Servidor escuchando en el puerto: 8000...."); });
También creemos un segundo archivo llamado trabajador-thread.js y agreguemos el siguiente código:
const {parentPort} = require("worker_threads"); // simula una página lenta sea j = 0; para (sea i = 0; i < 1000000000; i++) { // res.write(" Página lenta"); // Usa res.write en lugar de res.end dentro del bucle j++; } parentPort.postMessage(j);
Ahora, ¿qué está pasando aquí? En el primer archivo, estamos desestructurando la clase Worker del módulo work_threads.
Con trabajador.on más una función alternativa, podemos escuchar el archivo trabajador-thread.js que envía su mensaje a su padre, que es el archivo main.thread.js. Este método también nos ayuda a ejecutar código paralelo en NodeJs.
Conclusión
En este tutorial, analizamos diferentes enfoques para escalar aplicaciones NodeJs, como la arquitectura de microservicios, el almacenamiento en caché de memoria, el uso de módulos de clúster y subprocesos múltiples. práctica. Siempre es crucial trabajar con un socio de desarrollo de NodeJS externo confiable o contratar desarrolladores de NodeJS que sean competentes y capaces de implementar perfectamente cualquier funcionalidad requerida.
Si le gustó este artículo, consulte nuestras otras guías a continuación;
- Cambiar la versión del nodo: una guía paso a paso
- Liberando el poder de Websocket Nodejs
- Los mejores editores de texto y Node JS IDE para el desarrollo de aplicaciones
- Mejores prácticas para aumentar la seguridad en Node JS
Conclusión
¿Cómo puedo utilizar el módulo Cluster en Node.js para mejorar la escalabilidad?
El módulo Cluster en Node.js le permite crear procesos secundarios (trabajadores) que se ejecutan simultáneamente y comparten el mismo puerto del servidor. Esto aprovecha toda la potencia de varios núcleos en la misma máquina para procesar todas las solicitudes en paralelo (o al menos una gran cantidad de ellas), lo que puede mejorar significativamente la escalabilidad de su aplicación Node.js.
¿Qué papel juega PM2 en la escalabilidad de Node.js y en qué se diferencia del administrador de procesos integrado?
PM2 es un potente administrador de procesos para Node.js que proporciona varias funciones además del módulo Cluster integrado, como reinicios automáticos en caso de fallas, recargas sin tiempo de inactividad y registro centralizado. También simplifica la gestión de clústeres al proporcionar una interfaz de línea de comandos fácil de usar. Estas características hacen de PM2 una opción popular para administrar y escalar aplicaciones Node.js de producción.
¿Cómo mejora el almacenamiento en memoria caché el rendimiento y la escalabilidad de una aplicación web Node.js?
La caché en memoria como Redis almacena en la memoria los datos a los que se accede con frecuencia, lo que reduce la necesidad de costosas operaciones de bases de datos. Esto puede aumentar significativamente el rendimiento y la escalabilidad de su aplicación web Node.js y, cuando se combina con un equilibrador de carga, debería proporcionar mejoras de rendimiento significativas. Al ofrecer datos almacenados en caché y aplicar un equilibrador de carga, puede manejar más solicitudes más rápido, mejorar la experiencia del usuario y permitir que su aplicación escale de manera más efectiva para manejar cargas elevadas. Sin embargo, es fundamental implementar una estrategia sólida de invalidación de caché para garantizar la coherencia de los datos.
Fuente: BairesDev