O Node.js é um runtime JavaScript do lado do servidor que usa um modelo de entrada e saída (I/O) orientado por eventos e sem bloqueio. É amplamente reconhecido por criar aplicativos web rápidos e escaláveis. Além disso, possui uma grande comunidade e uma vasta biblioteca de módulos que simplificam diversas tarefas e processos.

O clustering aprimora o desempenho dos aplicativos Node.js, permitindo que eles sejam executados em vários processos. Essa técnica permite que eles usem todo o potencial de um sistema com vários núcleos.

Este artigo oferece uma análise abrangente sobre clustering no Node.js e como ele impacta o desempenho de um aplicativo.

O que é clustering?

Por padrão, os aplicativos Node.js são executados em um único thread. Essa característica de thread único significa que o Node.js não pode utilizar todos os núcleos em um sistema com múltiplos núcleos, como é o caso da maioria dos sistemas atuais.

O Node.js ainda pode lidar com várias solicitações simultaneamente, aproveitando as operações de (I/O) sem bloqueio e as técnicas de programação assíncrona.

Entretanto, tarefas computacionais pesadas podem bloquear o loop de eventos e fazer com que o aplicativo deixe de responder. Para resolver isso, o Node.js inclui um módulo de cluster nativo que, apesar de sua natureza de thread único, permite aproveitar todo o poder de processamento de um sistema com múltiplos núcleos.

A execução de vários processos aproveita o poder de processamento de vários núcleos da unidade central de processamento (CPU) para permitir o processamento paralelo, reduzir os tempos de resposta e aumentar a taxa de transferência. Isso, por sua vez, melhora o desempenho e o dimensionamento dos aplicativos Node.js.

Como funciona o clustering?

O módulo de cluster do Node.js permite que um aplicativo Node.js crie um cluster de processos secundários em execução simultânea, cada um lidando com uma parte da carga de trabalho do aplicativo.

Ao inicializar o módulo de cluster, o aplicativo cria o processo principal, que, em seguida, bifurca os processos secundários em processos de trabalho. O processo primário atua como um balanceador de carga, distribuindo a carga de trabalho para os processos de trabalho enquanto cada processo de trabalho escuta as solicitações recebidas.

O módulo de cluster do Node.js tem dois métodos de distribuição de conexões de entrada.

  • A abordagem round-robin – O processo principal escuta em uma porta, aceita novas conexões e distribui uniformemente a carga de trabalho para garantir que nenhum processo fique sobrecarregado. Essa é a abordagem padrão em todos os sistemas operacionais, exceto no Windows.
  • A segunda abordagem – O processo principal cria o soquete de escuta e o envia para os trabalhadores “interessados”, que aceitam conexões de entrada diretamente.

Teoricamente, a segunda abordagem, que é mais complicada, deveria proporcionar um desempenho melhor. Mas, na prática, a distribuição das conexões é muito desequilibrada. A documentação do Node.js menciona que 70% de todas as conexões acabam em apenas dois dos oito processos.

Como clusterizar seus aplicativos Node.js

Agora, vamos examinar os efeitos do clustering em um aplicativo Node.js. Este tutorial usa um aplicativo Express que executa intencionalmente uma tarefa computacional pesada para bloquear o loop de eventos.

Primeiro, execute esse aplicativo sem clustering. Em seguida, registre o desempenho com uma ferramenta de benchmarking. Em seguida, o clustering é implementado no aplicativo e o benchmarking é repetido. Por fim, compare os resultados para ver como o clustering melhora o desempenho do aplicativo.

Primeiros passos

Para entender este tutorial, você deve estar familiarizado com o Node.js e o Express. Para configurar seu servidor Express:

  1. Comece criando o projeto.
    mkdir cluster-tutorial
  2. Navegue até o diretório do aplicativo e crie dois arquivos, no-cluster.js e cluster.js, executando o comando abaixo:
    cd cluster-tutorial && touch no-cluster.js && touch cluster.js
  3. Inicialize o NPM em seu projeto:
    npm init -y
  4. Por fim, instale o Express executando o comando abaixo:
    npm install express

Criando um aplicativo sem cluster

Em seu arquivo no-cluster.js, adicione o bloco de código abaixo:

const express = require("express");
const PORT = 3000;

const app = express();

app.get("/", (req, res) => {
  res.send("Response from server");
});

app.get("/slow", (req, res) => {
  //Start timer 
  console.time("slow");

  // Generate a large array of random numbers
  let arr = [];
  for (let i = 0; i < 100000; i++) {
  arr.push(Math.random());
  }

  // Perform a heavy computation on the array
  let sum = 0;
  for (let i = 0; i  {
  console.log(`Server listening on port ${PORT}`);
});

O bloco de código acima cria um servidor express que é executado na porta 3000. O servidor tem duas rotas, uma rota raiz (/) e uma rota /slow. A rota raiz envia uma resposta ao cliente com a mensagem: “Response from server.”

No entanto, a rota /slow faz intencionalmente alguns cálculos pesados para bloquear o loop de eventos. Essa rota inicia um cronômetro e, em seguida, preenche uma array com 100.000 números aleatórios usando um loop for.

Em seguida, usando outro loop for, ele eleva cada número da array gerada ao quadrado e os adiciona. O cronômetro termina quando isso é concluído e o servidor responde com os resultados.

Inicie seu servidor executando o comando abaixo:

node no-cluster.js

Em seguida, faça uma solicitação GET para localhost:3000/slow.

Durante esse período, se você tentar fazer outras solicitações ao servidor – como para a rota raiz (/) – as respostas serão lentas, pois a rota /slow está bloqueando o loop de eventos.

Criando um aplicativo em cluster

Crie processos secundários usando o módulo cluster para garantir que seu aplicativo não fique sem resposta e não bloqueie solicitações subsequentes durante tarefas computacionais pesadas.

Cada processo secundário executa seu próprio loop de eventos e compartilha a porta do servidor com o processo principal, permitindo um melhor uso dos recursos disponíveis.

Primeiro, importe os módulos cluster e os do Node.js no seu arquivo cluster.js. O módulo cluster permite a criação de processos secundários para distribuir a carga de trabalho entre vários núcleos de CPU.

O módulo os fornece informações sobre o sistema operacional do seu computador. Você precisa desse módulo para recuperar o número de núcleos disponíveis no seu sistema e garantir que não crie mais processos secundários do que núcleos no sistema.

Adicione o bloco de código abaixo para importar esses módulos e recuperar o número de núcleos em seu sistema:

const cluster = require("node:cluster");
const numCores = require("node:os").cpus().length;

Em seguida, adicione o bloco de código abaixo ao seu arquivo cluster.js:

if (cluster.isMaster) {
  console.log(`Master ${process.pid} is running`);
  console.log(`This machine has ${numCores} cores`);

  // Fork workers.
  for (let i = 0; i  {
  console.log(`worker ${worker.process.pid} died`);

  // Replace the dead worker
  console.log("Starting a new worker");
  cluster.fork();
  });
}

O bloco de código acima verifica se o processo atual é o processo principal ou de trabalho. Se for verdadeiro, o bloco de código gera processos secundários com base no número de núcleos em seu sistema. Em seguida, ele escuta o evento de saída nos processos e os substitui gerando novos processos.

Por fim, envolva toda a lógica expressa relacionada em um bloco else. Seu arquivo cluster.js finalizado deve ser semelhante ao bloco de código abaixo.

//cluster.js
const express = require("express");
const PORT = 3000;
const cluster = require("node:cluster");
const numCores = require("node:os").cpus().length;

if (cluster.isMaster) {
  console.log(`Master ${process.pid} is running`);
  console.log(`This machine has ${numCores} cores`);

  // Fork workers.
  for (let i = 0; i  {
  console.log(`worker ${worker.process.pid} died`);

  // Replace the dead worker
  console.log("Starting a new worker");
  cluster.fork();
  });
} else {
  const app = express();

  app.get("/", (req, res) => {
    res.send("Response from server");
  });

  app.get("/slow", (req, res) => {
   console.time("slow");
  // Generate a large array of random numbers
  let arr = [];
  for (let i = 0; i < 100000; i++) {
  arr.push(Math.random());
    }

   // Perform a heavy computation on the array
   let sum = 0;
  for (let i = 0; i  {
  console.log(`Server listening on port ${PORT}`);
  });
}

Depois de implementar o clustering, vários processos tratarão as solicitações. Isso significa que seu aplicativo permanecerá responsivo mesmo durante uma tarefa computacional pesada.

Como avaliar o desempenho usando o loadtest

Para demonstrar e exibir com precisão os efeitos do clustering em um aplicativo Node.js, use o pacote npm loadtest para comparar o desempenho do seu aplicativo antes e depois do clustering.

Execute o comando abaixo para instalar o loadtest globalmente:

npm install -g loadtest

O pacote loadtest executa um teste de carga em uma URL HTTP/WebSockets especificada.

Em seguida, inicie o arquivo no-cluster.js em uma instância do terminal. Em seguida, abra outra instância de terminal e execute o teste de carga abaixo:

loadtest http://localhost:3000/slow -n 100 -c 10

O comando acima envia 100 solicitações com uma simultaneidade de 10 para o seu aplicativo sem cluster. A execução desse comando produz os resultados abaixo:

Non-clustered app load test results
Resultados do teste de carga do aplicativo não clusterizado.

Com base nos resultados, foram necessários aproximadamente 100 segundos para concluir todas as solicitações sem clustering, e a solicitação mais extensa levou até 12 segundos para ser concluída.

Os resultados variam de acordo com seu sistema.

Em seguida, pare de executar o arquivo no-cluster.js e inicie o arquivo cluster.js em uma instância de terminal. Em seguida, abra outra instância de terminal e execute esse teste de carga:

loadtest http://localhost:3000/slow -n 100 -c 10

O comando acima enviará solicitações 100 com uma simultaneidade 10 para o seu aplicativo em cluster.

A execução desse comando produz os resultados abaixo:

Clustered app load test result
Resultado do teste de carga do aplicativo em cluster.

Com clustering, as solicitações levaram 0,13 segundos (136 ms) para serem concluídas, uma grande diminuição em relação aos 100 segundos necessários para o aplicativo não clusterizado. Além disso, a solicitação mais longa no aplicativo clusterizado levou 41 ms para ser concluída.

Esses resultados demonstram que a implementação do clustering melhora significativamente o desempenho do seu aplicativo. Observe que você deve usar um software de gerenciamento de processos como o PM2 para gerenciar o clustering em ambientes de produção.

Usando o Node.js com a hospedagem de aplicativos da Kinsta

A Kinsta é uma empresa de hospedagem que facilita a implantação de seus aplicativos Node.js. Sua plataforma de hospedagem é construída sobre o Google Cloud Platform, que fornece uma infraestrutura confiável projetada para lidar com alto tráfego e suportar aplicativos complexos. Em última análise, isso melhora o desempenho dos aplicativos Node.js.

A Kinsta oferece vários recursos para implantações do Node.js, como conexões internas de banco de dados, integração com o Cloudflare, implantações do GitHub e Google C2 Machines.

Esses recursos facilitam a implantação e o gerenciamento de aplicativos Node.js e simplificam o processo de desenvolvimento.

Para implantar seu aplicativo Node.js na Hospedagem de Aplicativos da Kinsta, é fundamental enviar o código e os arquivos do aplicativo para o provedor Git que você escolheu (Bitbucket, GitHub ou GitLab).

Depois que seu repositório estiver definido, siga estas etapas para implantar seu aplicativo Express na Kinsta:

  1. Faça login ou crie uma conta para visualizar seu painel MyKinsta.
  2. Autorize a Kinsta no seu provedor Git.
  3. Clique em Aplicativos na barra lateral esquerda e, em seguida, clique em Adicionar aplicativo.
  4. Selecione o repositório e a branch a partir da qual você deseja implantar.
  5. Atribua um nome exclusivo ao seu aplicativo e escolha um local do centro de dados.
  6. Em seguida, configure seu ambiente de build. Selecione a configuração da build machine padrão com a opção Nixpacks recomendada para esta demonstração.
  7. Use todas as configurações padrão e, em seguida, clique em Criar aplicativo.

Resumo

O clustering no Node.js permite a criação de vários processos de trabalho para distribuir a carga de trabalho, melhorando o desempenho e a escalabilidade dos aplicativos Node.js. A implementação adequada do clustering é fundamental para que você alcance todo o potencial dessa técnica.

Projetar a arquitetura, gerenciar a alocação de recursos e minimizar a latência da rede são fatores vitais ao implementar o clustering no Node.js. A importância e a complexidade dessa implementação são o motivo pelo qual os gerenciadores de processos, como o PM2, devem ser usados em ambientes de produção.

O que você pensa sobre o clustering do Node.js? Você já o usou antes? Compartilhe na seção de comentários!

Jeremy Holcombe Kinsta

Editor de Conteúdo & Marketing na Kinsta, Desenvolvedor Web WordPress e Escritor de Conteúdo. Fora do universo WordPress, eu curto praia, golfe e filmes. Também enfrento problemas de gente alta ;).