Construindo aplicativos escalonáveis Node JS: práticas recomendadas, ferramentas e padrões para desempenho ideal

Construindo aplicativos escalonáveis Node JS: práticas recomendadas, ferramentas e padrões para desempenho ideal

Aprenda como construir aplicativos NodeJs escalonáveis ​​e confiáveis. Obtenha as melhores dicas e truques para garantir que seus aplicativos sejam otimizados para o sucesso!

Imagem em destaque

Em um mundo cada vez mais digital, aproveitar os serviços de desenvolvimento Node JS para construir aplicativos escalonáveis ​​e de alto desempenho não é apenas uma vantagem, mas uma necessidade. Este artigo investiga o mundo do NodeJS, um ambiente de tempo de execução preferido por vários desenvolvedores em todo o mundo, oferecendo práticas recomendadas, ferramentas importantes e padrões estratégicos para aumentar o desempenho dos aplicativos escalonáveis ​​Node JS. Quer você seja um novato mergulhando nos serviços de desenvolvimento Node JS ou um desenvolvedor experiente com o objetivo de refinar seu aplicativo, este artigo o guiará pelas etapas fundamentais para transformar seu aplicativo NodeJS de simplesmente operacional em excelente. Aproveite o poder desses insights e estratégias para construir aplicativos escalonáveis ​​Node JS que fazem mais do que apenas atender às suas expectativas de desempenho – eles as superam.

O que é NodeJS?

NodeJs é um tempo de execução JavaScript construído no mecanismo JavaScript V8 do Chrome que usa um modelo de E/S sem bloqueio e orientado a eventos. Ou seja, com NodeJs, os desenvolvedores podem executar código Javascript no lado do servidor, o que permite que os desenvolvedores Javascript escrevam aplicativos front-end e back-end. A natureza de ter uma única linguagem de programação em toda a pilha é apenas um dos muitos pontos de venda dos NodeJs. Alguns dos outros são:

  • NodeJs é assíncrono e orientado a eventos, o que significa que quando uma operação está sendo conduzida, se essa operação demorar muito para ser concluída, o aplicativo pode continuar a executar outras operações enquanto espera a conclusão da primeira. Esse recurso torna os aplicativos NodeJs eficientes e rápidos.
  • Por ser construído no mecanismo Javascript V8, o NodeJs é muito rápido na execução de código.
  • NodeJs possui uma grande comunidade de desenvolvedores. Isso significa que há muitos recursos para aprender quando alguém está travado e muitas bibliotecas para usar para facilitar o desenvolvimento.
  • NodeJs é multiplataforma. Ele pode ser executado em Windows, Linux e Mac OS. E como é basicamente Javascript, mas no lado do servidor, é fácil de aprender, usar e encontrar desenvolvedores. Não é difícil criar uma equipe que possa escrever aplicativos NodeJs, React Native e ReactJS para cobrir todas as partes do processo de desenvolvimento.
  • NodeJs é leve. Não consome muitos recursos e é fácil de escalar. No desenvolvimento de back-end, o escalonamento significa que um aplicativo pode lidar com mais solicitações por segundo sem travar ou ficar lento, tornando a experiência do usuário mais tranquila. Como o dimensionamento é o foco principal deste artigo, discutiremos isso com mais detalhes.

Compreendendo o loop de eventos no NodeJS

Antes de entrarmos no dimensionamento, vamos dar uma olhada rápida no que é o loop de eventos. O loop de eventos é um conceito fundamental no desenvolvimento de NodeJs. É um mecanismo single-thread que roda incessantemente e gerencia a execução de tarefas como leitura de arquivos, consulta de bancos de dados ou realização de solicitações de rede de forma assíncrona em uma aplicação NodeJs. Em vez de esperar que uma tarefa seja concluída, os NodeJs registram funções de retorno de chamada a serem executadas assim que a operação em questão for concluída. Essa natureza não bloqueadora do NodeJs o torna muito rápido e altamente escalável se as técnicas corretas forem usadas.

O que é dimensionamento?

Escalabilidade, no sentido mais simples, é a capacidade do aplicativo de lidar com muitas solicitações por segundo de uma só vez. Existem mais dois termos na terminologia de dimensionamento: dimensionamento vertical e horizontal. O escalonamento vertical, também conhecido como escalonamento vertical, refere-se ao processo em que a capacidade de um aplicativo de lidar com solicitações é aprimorada pela atualização de seus recursos, como adição de mais RAM, aumento de CPU, etc. O escalonamento horizontal, por outro lado, conhecido como expansão é o processo em que mais instâncias são adicionadas ao servidor.

Escalando em NodeJS com múltiplas instâncias

Em primeiro lugar, façamos a pergunta: Por que escalar? Simplesmente, em nossa era de muitos usuários, um aplicativo que não consegue lidar com todas as solicitações recebidas de todos os seus usuários não pode esperar permanecer no jogo.

E como desenvolvedores de back-end, precisamos ter certeza de que nosso aplicativo seja rápido, responsivo e seguro. O dimensionamento ajuda os desenvolvedores a obter um desempenho mais aprimorado, pois podem distribuir a carga de trabalho entre várias instâncias ou nós, lidar com mais tráfego e criar tolerância a falhas, que é o processo de ter várias instâncias para que, se uma instância falhar, as outras instâncias possam assumir e manter o aplicativo Node JS em execução.

Agora, enquanto algumas outras linguagens de programação como Go podem lidar com solicitações simultâneas por padrão, NodeJs, devido à sua natureza de thread único, lidam com operações de maneira um pouco diferente. Portanto, as técnicas usadas para escalar também variam.

NodeJs é rápido. Muito rápido. Porém, devido à sua natureza de thread único, ele pode não conseguir lidar com multithreading, pois só pode executar um thread por vez. Ter muitas solicitações ao mesmo tempo pode resultar no bloqueio do loop de eventos.

Como dimensionar aplicativos Node JS

Existem diferentes métodos para dimensionar aplicativos Node.JS. Vejamos alguns deles brevemente, como arquitetura de microsserviços, módulo de cluster e otimização de banco de dados.

Arquitetura de microsserviços

A arquitetura de microsserviços Node JS é o processo de desenvolvimento de software que consiste em entidades independentes e fracamente acopladas. Cada serviço é um aplicativo Node JS diferente que é desenvolvido e implantado, e eles podem se comunicar com cada um por meio de solicitações HTTP ou serviços de mensagens como RabbitMQ ou Apache Kafka. Este método de desenvolvimento de software, em vez de reunir tudo em um monorepo, permite que os desenvolvedores se concentrem em cada serviço de forma independente e implementem as mudanças necessárias sem afetar diretamente os outros. Embora deva ser observado aqui, as vantagens dos microsserviços são um conceito debatido e devem ser usados ​​com cautela.

Para entender a arquitetura de microsserviços, vejamos um exemplo hipotético de aplicação de comércio eletrônico. Este aplicativo pode ser dividido em microsserviços como Produto, Carrinho e Pedido. Cada microsserviço é desenvolvido e implantado de forma independente.

Por exemplo, o microsserviço Produto pode ser responsável por gerenciar os dados do produto no sistema. Forneceria endpoints CRUD e exporia uma API HTTP que outros microsserviços podem usar para interagir com informações do produto.

O microsserviço Cart poderia lidar com todos os recursos de gerenciamento de carrinho, como adicionar itens, alterar quantidades, calcular totais, etc. Ele também exporia uma API para outros microsserviços criarem carrinhos e atualizá-los. E o microsserviço Order pode permitir a criação de pedidos, processamento de pagamentos, rastreamento de status e muito mais. Ele forneceria APIs para funções de checkout de carrinho e pesquisa de pedidos.

Ao separar as preocupações em microsserviços autônomos e desacoplados, o aplicativo fica mais fácil de escalar e manter. Cada microsserviço se concentra em um recurso de domínio específico enquanto trabalha em conjunto para oferecer a experiência completa do aplicativo.

Por exemplo, o microsserviço Cart lidaria com todas as funcionalidades do carrinho de compras – adição de itens, atualização de quantidades, cálculo de totais, etc. Ele gerenciaria os dados do carrinho em seu próprio banco de dados.

O microsserviço Pedido forneceria pontos de extremidade para fazer pedidos, consultar o histórico de pedidos e integrar os microsserviços Carrinho e Produto. Ele serve como uma ponte entre o carrinho e os dados/funcionalidades do produto.

Dessa forma, cada equipe de microsserviços pode focar em sua parte específica da aplicação. A equipe do carrinho gerencia os recursos do carrinho, a equipe do produto lida com dados e APIs do produto e a equipe do pedido lida com o processamento e integração de pedidos.

Em teoria, esta separação de preocupações por domínio acelera o desenvolvimento, dividindo o trabalho e reduzindo a sobreposição de funcionalidades entre as equipes. Também promove a independência e a fraca ligação entre serviços. Cada microsserviço depende menos de outras partes do sistema, reduzindo os efeitos colaterais das alterações e aumentando a confiabilidade.

Cache

O cache é uma técnica usada para melhorar o desempenho e a escalabilidade de aplicativos Node.js, armazenando temporariamente dados acessados ​​com frequência para pesquisa rápida.

Considere este exemplo: Precisamos construir um aplicativo que busque e exiba dados de museus – imagens, títulos, descrições, etc. Também há paginação para permitir que os usuários visualizem diferentes páginas de dados.

Cada solicitação paginada pode buscar 20 itens da API pública do museu. Por ser uma API pública, provavelmente possui limitação de taxa para evitar abusos. Se solicitarmos os dados da API em cada mudança de página, atingiremos rapidamente esses limites de taxa.

Em vez disso, podemos usar o cache para evitar chamadas redundantes de API. Quando a primeira página de dados é solicitada, nós os armazenamos em cache localmente. Nas visitas subsequentes à página, primeiro verificamos se os dados estão no cache. Nesse caso, retornamos os dados armazenados em cache para evitar exceder os limites de taxa.

O cache fornece pesquisa rápida de dados já obtidos. Para APIs públicas ou quaisquer dados que não mudam com frequência, o cache pode melhorar enormemente o desempenho e reduzir custos/limites em serviços de back-end.

Uma ótima maneira de resolver esse problema é armazenar os dados em cache usando um serviço de cache como o Redis. Funciona assim: pegamos os dados da API da página número 1 e armazenamos no Redis, na memória.

Então, quando o usuário muda a página para a página 2, enviamos uma solicitação ao banco de dados do museu normalmente.

Mas o cache realmente demonstra seu valor quando um usuário volta para uma página já visitada. Por exemplo, quando o usuário retorna à página 1 após visualizar outras páginas, em vez de enviar uma nova solicitação de API, primeiro verificamos se os dados da página 1 existem no cache. Se isso acontecer, retornamos os dados armazenados em cache imediatamente, evitando uma chamada de API desnecessária.

Somente se o cache não contiver os dados é que fazemos a solicitação da API, armazenamos a resposta no cache e a devolvemos ao usuário. Dessa forma, reduzimos solicitações duplicadas à API à medida que os usuários revisitam as páginas. Ao servir a partir do cache sempre que possível, melhoramos o desempenho e permanecemos dentro dos limites de taxa da API. O cache atua como um armazenamento de dados de curto prazo, minimizando chamadas para o backend.

Prática: Módulo Cluster, Multithreading e Processos de Trabalho

Teoria sem prática é apenas metade do trabalho realizado. Nesta seção, veremos algumas das técnicas que podemos usar para dimensionar aplicativos NodeJs: módulo cluster e threading múltiplo. Primeiro usaremos o módulo de cluster integrado do NodeJS e, depois de entendermos como ele funciona, usaremos o gerenciador de processos, pacote pm2, para facilitar as coisas. Em seguida, mudaremos um pouco o exemplo e usaremos o módulo threads de trabalho para criar vários threads.

Módulo de agrupamento

Agora, como o NodeJs é de thread único, não importa quantos núcleos você tenha, ele usará apenas um único núcleo da sua CPU. Isso é totalmente aceitável para operações de entrada/saída, mas se o código consumir muito da CPU, o aplicativo Node pode acabar com problemas de desempenho. Para resolver este problema, podemos usar o módulo cluster. Este módulo nos permite criar processos filhos que compartilham a mesma porta do servidor que o processo pai.

Desta forma, podemos aproveitar todos os núcleos da CPU. Para entender o que isso significa e como funciona, vamos criar uma aplicação NodeJs simples que servirá de exemplo.

Começaremos criando uma nova pasta chamada nodeJs-scaling e dentro dessa pasta criaremos um arquivo chamado no-cluster.js. Dentro desse arquivo, escreveremos o seguinte trecho de código:

const http = require("http");

const server = http.createServer((req, res) => {
  if (req.url === " {
    res.writeHead(200, { "content-type": "text/html" });
    res.end("Home Page");
  } else if (req.url === "/slow-page") {
    res.writeHead(200, { "content-type": "text/html" });
    // simulate a slow page
    for (let i = 0; i < 9000000000; i++) {
      res.write("Slow Page");
    }

    res.end ; // Send the response after the loop completes
  }
});

server.listen(5000,   => {
  console.log("Server listening on port : 5000....");
});

Aqui, começamos importando o módulo HTTP integrado do NodeJs. Nós o usamos para criar um servidor que possui dois endpoints, um endpoint base e um endpoint de página lenta. O que pretendemos com esta estrutura é que quando formos para o endpoint base, ele será executado e abrirá a página normalmente. Mas, como você pode ver, por causa do loop for que será executado quando chegarmos ao ponto final da página lenta, a página levará muito tempo para carregar. Embora este seja um exemplo simples, é uma ótima maneira de entender como o processo funciona.

Agora, se iniciarmos o servidor executando node cluster.js e, em seguida, enviarmos uma solicitação ao endpoint base via CURL, ou apenas abrirmos a página em um navegador, ela carregará muito rapidamente. Um exemplo de solicitação CURL é curl -i Agora, se fizermos o mesmo com curl -i, perceberemos que leva muito tempo e até pode resultar em erro. Isso ocorre porque o loop de eventos é bloqueado pelo loop for e não pode lidar com nenhuma outra solicitação até que o loop seja concluído. Agora, existem algumas maneiras de resolver esse problema. Começaremos primeiro usando o módulo de cluster integrado e, em seguida, usaremos uma biblioteca útil chamada PM2.

Módulo de cluster integrado

Agora vamos criar um novo arquivo chamado cluster.js no mesmo diretório e escrever o seguinte trecho dentro dele:

const cluster = require("cluster");
const os = require("os");
const http = require("http");

// Check if the current process is the master process
if (cluster.isMaster) {
  // Get the number of CPUs
  const cpus = os.cpus .length;
  console.log(`${cpus} CPUs`);
} else {
  console.log("Worker process" + process.pid);
}

Aqui, começamos importando o cluster, o sistema operacional e os módulos http.

O que faremos a seguir é verificar se o processo é o cluster mestre ou não; em caso afirmativo, estamos registrando a contagem de CPU.

Esta máquina tem 6, seria diferente para você dependendo da sua máquina. Quando executamos node cluster.js devemos obter uma resposta como “6 CPUs”. Agora, vamos modificar um pouco o código:

const cluster = require("cluster");
const os = require("os");
const http = require("http");

// Check if the current process is the master process
if (cluster.isMaster) {
  // Get the number of CPUs
  const cpus = os.cpus .length;

  console.log(`Forking for ${cpus} CPUs`);
  console.log(`Master process ${process.pid} is running`);

  // Fork the process for each CPU
  for (let i = 0; i < cpus; i++) {
    cluster.fork ;
  }
} else {
  console.log("Worker process" + process.pid);
  const server = http.createServer((req, res) => {
    if (req.url === " {
      res.writeHead(200, { "content-type": "text/html" });
      res.end("Home Page");
    } else if (req.url === "/slow-page") {

      res.writeHead(200, { "content-type": "text/html" });

      // simulate a slow page
      for (let i = 0; i < 1000000000; i++) {
        res.write("Slow Page"); // Use res.write instead of res.end inside the loop
      }

      res.end ; // Send the response after the loop completes
    }
  });

  server.listen(5000,   => {
    console.log("Server listening on port : 5000....");
  });
}

Nesta versão atualizada, estamos bifurcando o processo para cada CPU. Poderíamos ter escrito cluster.fork uma quantidade máxima de 6 vezes também (como esta é a contagem de CPU da máquina que estamos usando, seria diferente para você).

Há um problema aqui: não devemos sucumbir à ideia tentadora de criar mais forks do que o número de CPUs, pois isso criará problemas de desempenho em vez de resolvê-los. Então, o que estamos fazendo é bifurcar o processo para cada CPU por meio de um loop for.

Agora, se executarmos node cluster.js, devemos obter uma resposta como esta:

6 CPUs
Master process 39340 is running
Worker process39347
Worker process39348
Worker process39349
Server listening on port : 5000....
Worker process39355
Server listening on port : 5000....
Server listening on port : 5000....
Worker process39367
Worker process39356
Server listening on port : 5000....
Server listening on port : 5000....
Server listening on port : 5000....

Como você pode ver, todos esses processos têm um ID diferente. Agora, se tentarmos abrir primeiro o endpoint da página lenta e depois o endpoint base, veremos que, em vez de esperar a conclusão do loop for longo, obteremos uma resposta mais rápida do endpoint base.

Isso ocorre porque o endpoint de página lenta está sendo tratado por um processo diferente.

Pacote PM2

Em vez de trabalhar com o próprio módulo de cluster, podemos usar um pacote de terceiros como pm2. Como iremos utilizá-lo no terminal, vamos instalá-lo globalmente executando sudo npm i -g pm2. Agora, vamos criar um novo arquivo no mesmo diretório, chamado no-cluster.js, e preenchê-lo com o seguinte código:

const http = require("http");

const server = http.createServer((req, res) => {
  if (req.url === " {
    res.writeHead(200, { "content-type": "text/html" });
    res.end("Home Page");
  } else if (req.url === "/slow-page") {
    res.writeHead(200, { "content-type": "text/html" });

    // simulate a slow page
    for (let i = 0; i < 9000000000; i++) {
      res.write("Slow Page"); // Use res.write instead of res.end inside the loop
    }

    res.end ; // Send the response after the loop completes
  }
});

server.listen(5000,   => {
  console.log("Server listening on port : 5000....");
});

Agora que aprendemos como executar vários processos, vamos aprender como criar vários threads.

Vários tópicos

Embora o módulo cluster nos permita executar várias instâncias de NodeJs que podem distribuir cargas de trabalho, o módulo trabalhador_threads nos permite executar vários threads de aplicativo em uma única instância de NodeJs.

Portanto, o código Javascript será executado em paralelo. Devemos observar aqui que o código executado em um thread de trabalho é executado em um processo filho separado, evitando que ele bloqueie nosso aplicativo principal.

Vejamos novamente esse processo em ação. Vamos criar um novo arquivo chamado main-thread.js e adicionar o seguinte código:

const http = require("http");
const { Worker } = require("worker_threads");

const server = http.createServer((req, res) => {
  if (req.url === " {
    res.writeHead(200, { "content-type": "text/html" });
    res.end("Home Page");
  } else if (req.url === "/slow-page") {
    // Create a new worker
    const worker = new Worker("./worker-thread.js");
    worker.on("message", (j) => {
      res.writeHead(200, { "content-type": "text/html" });

      res.end("slow page" + j); // Send the response after the loop completes
    });
  }
});

server.listen(5000,   => {
  console.log("Server listening on port : 8000....");
});

Vamos também criar um segundo arquivo chamado trabalhador-thread.js e adicionar o seguinte código:

const { parentPort } = require("worker_threads");
// simulate a slow page
let j = 0;
for (let i = 0; i < 1000000000; i++) {
  //   res.write("Slow Page"); // Use res.write instead of res.end inside the loop
  j++;
}

parentPort.postMessage(j);

Agora, o que está acontecendo aqui? No primeiro arquivo, estamos desestruturando a classe Worker do módulo worker_threads.

Com trabalhador.on mais uma função de fallback, podemos ouvir o arquivo trabalhador-thread.js que envia sua mensagem para seu pai, que é o arquivo main.thread.js. Este método também nos ajuda a executar código paralelo em NodeJs.

Conclusão

Neste tutorial, discutimos diferentes abordagens para dimensionar aplicativos NodeJs, como arquitetura de microsserviços, cache de memória, uso do módulo cluster e multithreading. prática. É sempre crucial trabalhar com um parceiro terceirizado de desenvolvimento NodeJS confiável ou contratar desenvolvedores NodeJS que sejam competentes e capazes de implementar qualquer funcionalidade necessária perfeitamente.

Se você gostou deste artigo, confira nossos outros guias abaixo;

  • Alterar versão do nó: um guia passo a passo
  • Desbloqueando o poder do Websocket Nodejs
  • Melhores editores de texto e IDE Node JS para desenvolvimento de aplicativos
  • Melhores práticas para aumentar a segurança no Node JS

Conclusão

Como posso utilizar o módulo Cluster em Node.js para melhorar a escalabilidade?

O módulo Cluster em Node.js permite criar processos filhos (trabalhadores) que são executados simultaneamente e compartilham a mesma porta do servidor. Isso aproveita todo o poder de vários núcleos na mesma máquina para processar todas as solicitações em paralelo (ou pelo menos um grande número delas), o que pode melhorar significativamente a escalabilidade do seu aplicativo Node.js.

Qual é a função do PM2 na escalabilidade do Node.js e como ele difere do gerenciador de processos integrado?

PM2 é um poderoso gerenciador de processos para Node.js que fornece vários recursos além do módulo Cluster integrado, como reinicializações automáticas em caso de falhas, recargas sem tempo de inatividade e registro centralizado. Ele também simplifica o gerenciamento de clusters, fornecendo uma interface de linha de comando fácil de usar. Esses recursos tornam o PM2 uma escolha popular para gerenciar e dimensionar aplicativos Node.js de produção.

Como o cache na memória melhora o desempenho e a escalabilidade de um aplicativo da web Node.js?

O cache na memória, como o Redis, armazena dados acessados ​​com frequência na memória, reduzindo a necessidade de operações caras de banco de dados. Isso pode aumentar significativamente o desempenho e a escalabilidade de seu aplicativo da web Node.js e, quando combinado com um balanceador de carga, deverá apresentar melhorias significativas de desempenho. Ao servir dados armazenados em cache e aplicar um balanceador de carga, você pode lidar com mais solicitações com mais rapidez, melhorando a experiência do usuário e permitindo que seu aplicativo seja dimensionado de forma mais eficaz para lidar com cargas altas. No entanto, é crucial implementar uma estratégia robusta de invalidação de cache para garantir a consistência dos dados.

Fonte: BairesDev

Conteúdo Relacionado

O Rails 8 sempre foi um divisor de águas...
A GenAI está transformando a força de trabalho com...
Entenda o papel fundamental dos testes unitários na validação...
Aprenda como os testes de carga garantem que seu...
Aprofunde-se nas funções complementares dos testes positivos e negativos...
Vídeos deep fake ao vivo cada vez mais sofisticados...
Entenda a metodologia por trás dos testes de estresse...
Descubra a imprevisibilidade dos testes ad hoc e seu...
A nomeação de Nacho De Marco para o Fast...
Aprenda como os processos baseados em IA aprimoram o...
O Node.js, o popular tempo de execução JavaScript assíncrono...
A web está em constante evolução, e com ela,...
A Inteligência Artificial (IA) tem sido um tema cada...
Você já se sentiu frustrado com a complexidade de...
O OpenStack é uma plataforma de computação em nuvem...
Você já se sentiu frustrado com a criação de...
A era digital trouxe uma transformação profunda na forma...
Nos dias atuais, a presença digital é fundamental para...
Vissza a blogba

Hozzászólás írása

Felhívjuk a figyelmedet, hogy a hozzászólásokat jóvá kell hagyni a közzétételük előtt.