Como dimensionar seus aplicativos Node.js?

Jan 25 2023
Vamos fazer um resumo rápido sobre como dimensionar uma das estruturas mais populares atualmente para criar aplicativos. Então vamos partir da motivação: como criar uma API de bom desempenho e replicar essa lógica para X usuários? Temos três formas de escalar nosso app: Vertical, Horizontal e mista (vertical e horizontal).

Vamos fazer um resumo rápido sobre como dimensionar uma das estruturas mais populares atualmente para criar aplicativos.

Então vamos partir da motivação: como criar uma API de bom desempenho e replicar essa lógica para X usuários?

Temos três formas de dimensionar nosso app: Vertical , Horizontal e mista (vertical e horizontal).

Escala horizontal

A escala horizontal significa, na maioria das vezes, duplicar os servidores ou nós. Com esta réplica ou réplicas, precisaremos de um balanceador de carga, para enviar todas as requisições de forma igual para elas.

Temos que levar em conta que esta solução vem com complexidade na hora de implantar a aplicação, pois o novo balanceador de carga deve estar bem configurado senão todo o tráfego pode ir para um servidor e estaremos perdendo dinheiro.

Escala vertical

Vertical significa adicionar mais poder de computação ou ram/discos.

Isso é bastante direto, se o seu computador em nuvem for executado em uma máquina de 16 GB de RAM e 16 núcleos/32 threads, vamos dobrar isso e você pode fornecer o dobro dos usuários .

Mas para isso temos que levar em consideração que teremos um limite em algum momento, também o preço aumentará exponencialmente nas últimas configurações disponíveis.

Então vamos começar!

Esta será uma maleta curta para dimensionamento horizontal .

Um começo fácil seria usar o Módulo Cluster .

Com este módulo podemos criar processos filhos que compartilham todas as portas do servidor. Chega de palavras, aí vem o código!

import cluster from 'node:cluster';
import http from 'node:http';
import { availableParallelism } from 'node:os';
import process from 'node:process';

const numCPUs = availableParallelism();

if (cluster.isPrimary) {
  console.log(`Primary ${process.pid} is running`);

  // Fork workers.
  for (let i = 0; i < numCPUs; i++) {
    cluster.fork();
  }

  cluster.on('exit', (worker, code, signal) => {
    console.log(`worker ${worker.process.pid} died`);
  });
} else {
  // Workers can share any TCP connection
  // In this case it is an HTTP server
  http.createServer((req, res) => {
    res.writeHead(200);
    res.end('hello world\n');
  }).listen(8000);

  console.log(`Worker ${process.pid} started`);
}

$ node server.js
Primary 3596 is running
Worker 4324 started
Worker 4520 started
Worker 6056 started
Worker 5644 started

Para entender um pouco melhor esse negócio de trabalhador , vamos fazer um exemplo:

Leia um grande arquivo json e para cada linha e para cada objeto dentro dele, converta essas informações em XML.

Primeiro o código sem threads de trabalho =>

/* syncMode.js */

// Read all JSON file contents into an array
const contents = getContents()

/* Execution time start */
const start = hrtime.bigint()

// Convert each JSON file content to XML format
const result = contents.map((content) => {
  content = JSON.parse(content)
  return js2xmlparser.parse('user', content)
})

/* Execution time end */
const end = hrtime.bigint()
console.info(`Execution time: ${(end - start) / BigInt(10 ** 6)}ms`)

Digamos que recebemos um diretório com 1.000 arquivos JSON e temos a tarefa de converter cada arquivo JSON em um arquivo XML.

Cada arquivo JSON tem uma matriz de 1.000 objetos, com cada objeto tendo 6 pares chave-valor, conforme mostrado no exemplo abaixo.

{
  "id": 1,
  "first_name": "abc",
  "last_name": "xyz",
  "email": "[email protected]",
  "gender": "Male",
  "ip_address": "163.213.59.129"
}

$ node syncMode.js
Execution time: 1036ms

Para facilitar o gerenciamento das threads de trabalho, vamos utilizar uma biblioteca chamada Piscina , que é perfeita para o trabalho!

/* ThreadMode.js */

const Piscina = require('piscina')

// Read all JSON file contents into an array
const contents = getContents()

/* Execution time start */

// Divide the content into two chunks
const chunks = splitToChunks(contents, 2)

// Create a new thread pool
const pool = new Piscina()
const options = { filename: resolve(__dirname, 'worker-pool') }

// Run operation on the chunks parallely
const result = await Promise.all([pool.run(chunks[0], options), pool.run(chunks[1], options)])

/* Execution time end */

$node ThreadMode.js
Execution time: 687ms

Vimos um aumento de desempenho incrível de mais de 66% !!! com apenas algumas linhas. Aqui é um caso bem específico, mas começamos a ver como aumentar a performance em Node.js usando funcionalidades internas.

Nas próximas etapas, usaremos o balanceamento de carga para distribuir a carga em várias APIs no node com docker, fique atento!!!