Introducción
Node.js es un entorno de tiempo de ejecución de JavaScript basado en el motorV8 de JavaScripts de Google, implementa el patrón de reacción, un paradigma de E/S sin bloqueo y controlado por eventos.
Definitivamente no desea usar Node.js para operaciones intensivas de CPU, usarlo para computación pesada anulará casi todas sus ventajas.
Node.js realmente brilla en la creación de aplicaciones de redes rápidas y escalables, ya que es capaz de manejar una gran cantidad de conexiones simultáneas con alto rendimiento.
En cualqueir caso, para escalar una API Node.js deberias empezar primero por tener una buena Arquitectura de Proyecto como hemos visto en un post anterior.
Cómo escalas las APIs de node.js? 🤔
Hay varios consejos comunes para escribir servidores escalables node.js, pero la clave es la arquitectura detrás de él.
Y cada aplicación es diferente, es decir, un backend de node.js para una aplicación de chat en tiempo real puede manejar una cantidad diferente de conexiones que un sitio de comercio electrónico o una plataforma de transmisión.
Tomemos como ejemplo a Netflix, usan node.js en su infraestructura de microservicios, para hacer pruebas A / B a escala
Aquí hay algunas consideraciones y problemas comunes que enfrentan muchos equipos de desarrollo al escalar una aplicación node.js.
Tabla de contenido 📝
- Sirviendo activos estáticos
- Cron jobs y programación de tareas
- Modo clúster: uso de todos los recursos disponibles
- Conclusión
Mi historia escalando las API de node.js
Trabajé para un par de empresas nuevas que iniciaron sus productos de software solo a partir de una simple plantilla express.js que encontramos en GitHub.
En 2015, fui contratado por una startup que estaba buscando construir el MVP para lo que sería su primer producto, así que con el equipo, decidimos usar el ["Angular Full-Stack"](https://github.com /angular-fullstack/generator-angular-fullstack) plantilla de Yeoman la aplicación de andamios (oh chico, soy viejo)
Con eso, logramos construir MVP en un par de meses y, afortunadamente, el producto tuvo éxito
Comenzó a crecer rápidamente, y en el proceso, tuvimos que volver a escribir el servidor de fondo, cambiar la arquitectura semi-monolítica a una basada en microservicios, alejarnos del cliente web de ser servido en la aplicación node.js para estar alojado en AWS S3 y tener un CDN.
Hicimos muchas otras cosas para escalar las aplicaciones de node.js como mejoras de rendimiento en AMI de Linux, refactorizar a varias capas, implementar typecript en el servidor de node.js, reescribir algunos servicios de node.js, implementar el patrón pub-sub en un microservicio node.js, implementando sockets con socket.io, alejar la solución de búsqueda de node.js a Elastic Search, agregue capas de caché Redis, y demás.
Me llevará un año entero escribir sobre todo
Ahora, estoy trabajando como consultor independiente, y cada nuevo proyecto al que llegué tiene problemas de escalabilidad similares.
Así que hoy quiero hablar sobre cómo están impidiendo que su servidor node.js crezca y alcance un estado de alta escalabilidad.
Sirviendo archivos estáticos 📦
Por qué tiene su aplicación Angular o de React servida por express.js en su servidor node.js?
Node.js no fue diseñado para servir archivos estáticos, se necesita mucho tiempo de CPU%.
Debería usar un CDN proxy como CloudFront delante de sus archivos estáticos.
Creo que la raíz del problema proviene de la cantidad de plantillas de inicio que vienen con una solución de "full stack" para construir un MVP.
Pero cuando su producto y base de usuarios crezcan, enfrentará un problema, su servidor node.js usará demasiado tiempo de CPU.
! node.js y arquitectura de cliente web
Consulte este artículo sobre cómo implementar AWS S3 + AWS Cloudfront para realizar esta tarea.
Además, puede usar Netlify, que es totalmente gratis para una red de 100 GB tráfico mensual y 1 TB de tráfico para cuentas pagas.
Como ejemplo de cuánto es, este blog está alojado en Netlify y cada página tiene casi 800kb, por lo que puedo tener alrededor de 100.000 visitas / mes gratis.
Y por favor NO USE compresión gzip
Lo sé, está en la documentación express.js,debería ser algo bueno, verdad?
Bueno, no, comprimir una respuesta implica el cálculo de la CPU.
Por lo tanto, es mejor delegar ese tipo de tareas al proxy, CloudFront tiene la opción de configurar la compresión, así como otros proxies como Nginx.
Un buen programador de tareas ⏰
Es muy común la necesidad de una tarea recurrente. Tal vez necesite enviar un recordatorio para un usuario una vez al día, o calcular la facturación del servicio para un cliente una vez al mes.
Pero no debe confiar en simple setTimeout
o setInterval
para realizar tales tareas.
Una mala planificación aquí le traerá problemas cuando intente escalar horizontalmente su servidor node.js, los trabajos cron se duplicarán y puede producirse un caos.
Es un mejor enfoque utilizar un framework de programación de tareas como agendajs que tiene un módulo separado para tener un panel de administración
-
Los trabajos programados y recurrentes se almacenan en MongoDB, cada vez que un trabajador inicia un trabajo, bloquean la ejecución, por lo que no hay problema con la ejecución de varios trabajos al mismo tiempo.
-
Puede reprogramar trabajos fácilmente, solo son documentos de MongoDB que se pueden cambiar en cualquier momento.
-
Si la tarea falla, puede reprogramar la ejecución nuevamente.
-
Puede agregar una GUI al panel de administración para monitorear tareas programadas y recurrentes y sus estados.
-
Usando el panel de administración puede ejecutar manualmente un trabajo cuando lo desee.
-
No hay problema con el escalado horizontal del servidor node.js y la duplicación de la ejecución de trabajos.
Configuración de agenda.js
Esta es una buena manera de instalar la agenda en un proyecto.
- Primero, inicialice agendajs y cree un singleton, esto es lo que vamos a usar en toda la aplicación.
Archivos de trabajo / agenda.js
import * as agendajs from 'agenda';
export default (mongoConnection) => {
const agendajs = new agendajs();
(async () => {
await agendajs._ready;
try {
agendajs._collection.ensureIndex({
disabled: 1,
lockedAt: 1,
name: 1,
nextRunAt: 1,
priority: -1
}, {
name: 'findAndLockNextJobIndex'
});
} catch (err) {
console.log('Failed to create agendajs index!');
console.log(err);
throw err;
}
})();
agendajs
.mongo(mongoConnection, 'my-agendajs-jobs')
.processEvery('5 seconds')
.maxConcurrency(20);
return agendajs;
}
- Segundo, cree una declaración de trabajo para cada trabajo al que desee que agendasj llame
Tenga en cuenta que no necesariamente tiene que tener su lógica de servicio aquí, puede usar esta clase como fachada para la implementación real.
Archivos de trabajo/sendWelcomeEmail.js
import MailerService from '../services/mailer';
export default class SendWelcomeEmail {
public async handler (job, done): Promise<any> {
const { email } = job.attrs.data;
const mailerServiceInstance = new MailerService();
await mailerServiceInstance.SendWelcomeEmail(email);
done();
}
}
Servicios de archivo / mailer.js
import * as mailgun from 'mailgun';
export default class Mailer {
constructor() {}
public async SendWelcomeEmail(email){
const data = {
from: 'Hi from Softwareontheroad <sam@softwareontheroad.com>',
to: email,
subject: 'Welcome !',
text: 'Thanks for sign up',
};
return mailgun.messages().send(data);
}
}
- Tercero, registre su trabajo en la definición de trabajo de agendajs
Definimos lo que será el encargado del trabajosend-welcome-email
Archivo jobs/index.js
import SendWelcomeEmail from './send-welcome-email';
export default ({ agendajs }) => {
agendajs.define('send-welcome-email',
{ priority: 'high', concurrency: 10 },
new SendWelcomeEmail().handler, // referencia al manejador, pero no ejecutándolo!
)
agendajs.start();
}
- Cuarto, use la instancia de agendajs para programar sus trabajos
Servicio de archivo/usuario.js
import agendajs from '../jobs/agendajs';
export default class UsersService {
constructor() {}
public async SignUp(userDTO: IUserDTO): Promise<IUser> {
let user;
try {
user = new UserModel(userDTO);
... // hacer cosas elegantes
await user.save();
// Llame a agendajs y programe una tarea, en 10 minutos envíe el correo electrónico de bienvenida al usuario.
agendajs.schedule('in 10 minutes', 'send-welcome-email', { email: user.email },);
... // haz más cosas elegantes
return user;
} catch(e) {
logger.warn('Error on creation of user...')
await user.remove();
throw e;
}
}
}
Usando Agendash como una buena GUI de administrador
Ahora sus trabajos están almacenados en la base de datos y tienen menos errores, pero pueden ocurrir. Una buena manera de monitorear sus trabajos activos, programados y fallidos es usar Agendash la interfaz de usuario web para agendajs También al tener control sobre los trabajos, podemos reprogramarlos, crearlos, ejecutarlos y eliminarlos.
Lee aqui una guia general sobre agendash
Instalación de Agendash
Primero instalar Agendash2 en el proyecto node.js
npm install agendash2 --save
Luego, ir donde comienzan se montan las rutas de express.js y agregar una nueva para Agendash2.
Tenga en cuenta que Agendash no proporciona ningún tipo de seguridad, por lo que aquí estamos agregando una capa mínima de protección con
express-basic-auth
import * as basicAuth from 'express-basic-auth';
import * as agendash from 'agendash2';
export default (app, agendajsInstance) => {
app.use('/agendash',
basicAuth({
users: {
agendajsAdmin: 'super-secure-and-secred-password',
},
challenge: true,
}),
agendash(agendajsInstance)
);
};
Ahora el dashboard de agendajs estará ubicado en /agendash
Agendash2
es nuestro fork de Agendash, lo reescribimos desde 0 usando Vue.js y con nuevas funcionalidades como busqueda, paginacion, UI Responsive para celulares, y esta altamente optimizada.
Usando todos los recursos 💰
Es el año 2020 y aún la mayoría de los desarrolladores no usan la función de clúster que viene incorporada en node.js desde la versión 0.12.0
Por defecto funciona así: el proceso maestro se encuentra en un puerto, acepta nuevas conexiones y las distribuye entre los esclavos de una manera robusta, con algunos conocimientos integrados para evitar sobrecargar a un trabajador proceso.
Verifique estas comparaciones:
- Tiempo de respuesta más bajo es mejor
- Conexiones concurrentes más grande es mejor
Implementación en modo clúster
La implementación es bastante sencilla si tiene su proyecto node.js correctamente estructurado.
Vaya al punto de entrada de su proyecto, requiera el modo clúster y genere su aplicación cuando el proceso sea un trabajador.
Consulte mi guía sobre una buena estructura de proyecto para los servidores node.js.
Archivo: app.js
const express = require('express');
module.exports = () => {
const app = express();
// Solo una ruta básica
app.get('/', function (req, res) {
res.send('Hello World!');
});
app.listen(4000);
console.log('Application running!');
}
Estamos configurando nuestro servidor, nada nuevo o elegante aquí.
Archivo: index.js
const cluster = require('cluster');
const os = require('os');
const runExpressServer = require('./app');
// Comprueba si el proceso actual es maestro.
if (cluster.isMaster) {
// Obtenga el total de núcleos de CPU.
const cpuCount = os.cpus().length;
// Asignar un trabajador para cada núcleo
for (let j = 0; j < cpuCount; j++) {
cluster.fork();
}
} else {
// Este no es el proceso maestro, por lo que generamos el servidor express.
runExpressServer();
}
// API de clúster tiene una variedad de eventos.
// Aquí estamos creando un nuevo proceso si un trabajador muere.
cluster.on('exit', function (worker) {
console.log(`Worker ${worker.id} died'`);
console.log(`Staring a new one...`);
cluster.fork();
});
Primero, estamos requiriendo el módulo cluster
. Luego verificamos si el proceso actual es maestro.
Si es así, obtenemos la cantidad total de núcleos de CPU y generamos procesos de trabajo.
De lo contrario, el proceso actual es un trabajador, por lo que inicializamos nuestra aplicación aquí.
Además, estamos configurando un suscriptor para el evento 'exit'. Si un trabajador muere por algún motivo, generamos uno nuevo en reemplazo.
Puede parecer aterrador probar esto en su servidor de producción node.js, pero no tiene que preocuparse. A menos que esté utilizando algún tipo de CRON casero en el mismo servidor que ejecuta su API.
Y ya hablamos de esto, en la sección anterior.
Conclusión
Servir archivos estáticos con node.js es una tarea que exige muchos recursos de CPU. Node.js no fue diseñado para eso.
Tener sus trabajos cron dentro de agendajs lo beneficiará al escalar horizontalmente.
Y no olvide habilitar la potencia del modo clúster desde el día 1 para utilizar todos los recursos disponibles en la máquina.
Al resolver estos problemas, podrá escalar su servidor node.js para poder manejar más tráfico.
En cualqueir caso, para escalar una API Node.js deberias empezar primero por tener una buena Arquitectura de Proyecto como hemos visto en un post anterior.