Tudo o que você precisa saber sobre o Node.js

Olá Habr! Apresento a você a tradução do artigo "Tudo o que você precisa saber sobre o Node.js", de Jorge Ramón.



Atualmente, a plataforma Node.js. é uma das plataformas mais populares para a criação de APIs REST eficientes e escalonáveis. Também é adequado para a criação de aplicativos móveis híbridos, programas de desktop e até para IoT.


Trabalho com a plataforma Node.js. há mais de 6 anos e realmente adoro isso. Esta postagem está tentando principalmente ser um guia sobre como o Node.js realmente funciona.


Vamos começar !!


O que será discutido:




Mundo antes do Node.js


Servidor multithread


Os aplicativos da Web gravados seguindo a arquitetura cliente / servidor funcionam da seguinte maneira - o cliente solicita o recurso necessário do servidor e o servidor envia o recurso em resposta. Nesse esquema, o servidor responde à solicitação e finaliza a conexão.


Esse modelo é efetivo porque cada solicitação ao servidor consome recursos (memória, tempo do processador etc.). Para processar cada solicitação subsequente do cliente, o servidor deve concluir o processamento da solicitação anterior.


Isso significa que o servidor pode processar apenas uma solicitação por vez? Na verdade não! Quando o servidor recebe uma nova solicitação, ele cria um thread separado para processá-lo.


O fluxo , em palavras simples, é o tempo e os recursos que a CPU aloca para executar um pequeno bloco de instruções. Com isso dito, o servidor pode processar várias solicitações por vez, mas apenas uma por thread. Esse modelo também é chamado de modelo de thread por solicitação .



Para processar N pedidos, o servidor precisa de N threads. Se o servidor receber solicitações N + 1, deverá aguardar até que um dos encadeamentos fique disponível.


Na figura acima, o servidor pode processar até 4 solicitações (threads) de cada vez e, quando receber as próximas 3 solicitações, essas solicitações deverão aguardar até que qualquer um desses 4 threads fique disponível.


Uma maneira de se livrar das restrições é adicionar mais recursos (memória, núcleos do processador etc.) ao servidor, mas essa não é a melhor solução ....



E, claro, não se esqueça das limitações tecnológicas.


Bloqueio de entrada / saída


O número limitado de threads no servidor não é o único problema. Talvez você tenha se perguntado por que um único encadeamento não pode processar várias solicitações ao mesmo tempo? tudo devido ao bloqueio de operações de E / S.



Suponha que você esteja desenvolvendo uma loja online e precise de uma página na qual o usuário possa visualizar uma lista de todos os produtos.


O usuário bate em http://yourstore.com/products e o servidor renderiza um arquivo HTML com todos os produtos do banco de dados em resposta. Não é nada complicado, certo?


Mas o que acontece nos bastidores?


  • Quando um usuário bate em /products método ou função específica deve ser executada para processar a solicitação. Um pequeno pedaço de código (o seu ou o seu framework) analisa o URL da solicitação e procura um método ou função adequada. O fluxo está em execução .
  • Agora, o método ou função desejado é executado, como no primeiro parágrafo, o thread funciona.
  • Como você é um bom desenvolvedor, salve todos os logs do sistema em um arquivo e, é claro, para garantir que o roteador execute o método / função desejado - você também registra a linha “Método X executando !!”. Mas tudo isso está bloqueando as operações o fluxo de entrada / saída está aguardando .
  • Todos os logs são salvos e as seguintes linhas de função são executadas. O encadeamento está funcionando novamente .
  • Hora de acessar o banco de dados e obter todos os produtos - uma consulta simples como os SELECT * FROM products faz seu trabalho, mas adivinhe? Sim, esta é uma operação de E / S de bloqueio. O fluxo está aguardando .
  • Você recebeu uma matriz ou uma lista de todos os produtos, mas certifique-se de ter prometido tudo isso. O fluxo está aguardando .
  • Agora você tem todos os produtos e é hora de renderizar o modelo para a página futura, mas antes disso você precisa lê-los. O fluxo está aguardando .
  • O mecanismo de renderização faz seu trabalho e envia uma resposta ao cliente. O encadeamento está funcionando novamente .
  • O fluxo é livre, como um pássaro no céu.

Quão lentas são as operações de E / S? Bem, isso depende do específico. Vamos olhar para a mesa:


OperaçãoCiclos de CPU
Registradores de CPU3 medidas
Cache L18 medidas
Cache L212 medidas
RAM150 medidas
Disco30.000.000 medidas
Rede250.000.000 medidas

As operações de leitura de rede e disco são muito lentas. Imagine quantas solicitações ou chamadas para APIs externas seu sistema poderia lidar durante esse período.


Para resumir: As operações de E / S fazem o encadeamento aguardar e desperdiçar recursos.




Problema C10K


O problema


C10k (eng. C10k; conexões 10k - problema com 10 mil conexões)


No início dos anos 2000, as máquinas servidor e cliente estavam lentas. O problema surgiu ao processar 10.000 conexões de clientes com a mesma máquina em paralelo.


Mas por que o modelo tradicional de thread por solicitação (thread sob solicitação) não conseguiu resolver esse problema? Bem, vamos usar um pouco de matemática.


A implementação nativa de encadeamentos aloca mais de 1 MB de memória por fluxo, deixando isso - para 10 mil encadeamentos, são necessários 10 GB de RAM e isso é apenas para a pilha de fluxos. Sim, e não se esqueça, estamos no início dos anos 2000 !!



Hoje, os servidores e os computadores clientes trabalham com mais rapidez e eficiência e quase qualquer linguagem ou estrutura de programação pode lidar com esse problema. Mas, de fato, o problema não está resolvido. Para 10 milhões de conexões de clientes com uma máquina, o problema retorna novamente (mas agora é o problema do C10M ).


Resgate de JavaScript?


Spoilers de Cuidado !!!
O Node.js resolve o problema do C10K ... mas como ?!


O JavaScript do lado do servidor não era algo novo e incomum no início dos anos 2000, naquela época já havia implementações na JVM (máquina virtual java) - RingoJS e AppEngineJS, que trabalhavam no modelo de thread por solicitação.


Mas se eles não conseguiram resolver o problema, como o Node.js ?! Tudo porque o JavaScript é de thread único .




Node.js e o loop de eventos


Node.js


O Node.js é uma plataforma de servidor executada no mecanismo do Google Chrome - V8, que pode compilar o código JavaScript no código da máquina.


O Node.js usa um modelo orientado a eventos e uma arquitetura de E / S sem bloqueio , o que o torna leve e eficiente. Isso não é uma estrutura, nem uma biblioteca, é um tempo de execução JavaScript.


Vamos escrever um pequeno exemplo:


 // Importing native http module const http = require('http'); // Creating a server instance where every call // the message 'Hello World' is responded to the client const server = http.createServer(function(request, response) { response.write('Hello World'); response.end(); }); // Listening port 8080 server.listen(8080); 

E / S sem bloqueio


O Node.js usa operações de entrada / saída sem bloqueio, o que isso significa:


  • O encadeamento principal não será bloqueado pelas operações de E / S.
  • O servidor continuará a atender solicitações.
  • Teremos que trabalhar com código assíncrono .

Vamos escrever um exemplo no qual o servidor envia uma página HTML em resposta a uma solicitação para /home e para todas as outras solicitações - 'Hello World'. Para enviar uma página HTML, você deve primeiro lê-la em um arquivo.


home.html


 <html> <body> <h1>This is home page</h1> </body> </html> 

index.js


 const http = require('http'); const fs = require('fs'); const server = http.createServer(function(request, response) { if (request.url === '/home') { fs.readFile(`${ __dirname }/home.html`, function (err, content) { if (!err) { response.setHeader('Content-Type', 'text/html'); response.write(content); } else { response.statusCode = 500; response.write('An error has ocurred'); } response.end(); }); } else { response.write('Hello World'); response.end(); } }); server.listen(8080); 

Se o URL solicitado for /home , o módulo fs nativo será usado para ler o arquivo home.html .


Funções que se enquadram em http.createServer e fs.readFile como argumentos são retornos de chamada . Essas funções serão executadas em algum momento no futuro (a primeira assim que o servidor receber a solicitação e a segunda quando o arquivo for lido do disco e colocado no buffer).


Enquanto o arquivo está sendo lido do disco, o Node.js pode processar outras solicitações e até mesmo ler o arquivo novamente e tudo isso em um fluxo ... mas como ?!


Loop de eventos


O loop de eventos é a mágica que acontece dentro do Node.js. Este é literalmente um loop sem fim e, na verdade, um thread.



Libuv é uma biblioteca C que implementa esse padrão e faz parte do kernel do Node.js. Você pode aprender mais sobre o libuv aqui .


Um ciclo de eventos possui 6 fases, cada execução de todas as 6 fases é chamada de tick .



  • timers : nesta fase, retornos de chamada agendados pelos métodos setTimeout() e setInterval() são executados;
  • retornos de chamada pendentes : quase todos os retornos de chamada são executados, exceto eventos de close , temporizadores e setImmediate() ;
  • ocioso, prepare : usado apenas para fins internos;
  • sondagem : responsável por receber novos eventos de E / S. Node.js pode bloquear neste momento;
  • check : retornos de chamada causados ​​pelo método setImmediate() são executados neste estágio;
  • fechar retornos de chamada : por exemplo socket.on('close', ...) ;

Bem, existe apenas um segmento, e esse segmento é um loop de eventos, mas quem realiza toda a E / S?


Preste atenção !!!
Quando um loop de eventos precisa executar uma operação de E / S, ele usa o encadeamento do SO do conjunto de encadeamentos e, quando a tarefa é concluída, o retorno de chamada é enfileirado durante a fase de retorno de chamada pendente .



Isso não é legal?




O problema das tarefas que consomem muita CPU


O Node.js parece perfeito! Você pode criar o que quiser.


Vamos escrever uma API para calcular números primos.


Um número primo é um número inteiro (natural) maior que um e divisível por apenas 1 e por si só.



Dado um número N, a API deve calcular e retornar os primeiros N primos na lista (ou matriz).


primes.js


 function isPrime(n) { for(let i = 2, s = Math.sqrt(n); i <= s; i++) { if(n % i === 0) return false; } return n > 1; } function nthPrime(n) { let counter = n; let iterator = 2; let result = []; while(counter > 0) { isPrime(iterator) && result.push(iterator) && counter--; iterator++; } return result; } module.exports = { isPrime, nthPrime }; 

index.js


 const http = require('http'); const url = require('url'); const primes = require('./primes'); const server = http.createServer(function (request, response) { const { pathname, query } = url.parse(request.url, true); if (pathname === '/primes') { const result = primes.nthPrime(query.n || 0); response.setHeader('Content-Type', 'application/json'); response.write(JSON.stringify(result)); response.end(); } else { response.statusCode = 404; response.write('Not Found'); response.end(); } }); server.listen(8080); 

prime.js é a implementação dos cálculos necessários: a função isPrime verifica se o número é primo e nthPrime retorna N tais números.


O arquivo index.js é responsável por criar o servidor e usa o módulo prime.js para processar cada solicitação para /primes . O número N é lançado através da string de consulta no URL.


Para obter os 20 primeiros números primos, precisamos fazer uma solicitação para http://localhost:8080/primes?n=20 .


Suponha que tenhamos 3 clientes nos bloqueando e tentando acessar nossa API de E / S sem bloqueio:


  • A primeira consulta 5 inicia a cada segundo.
  • O segundo pede 1000 números primos a cada segundo
  • O terceiro solicita 10.000.000.000 de números primos, mas ...


Quando o terceiro cliente envia uma solicitação, o encadeamento principal é bloqueado e esse é o principal sintoma do problema de tarefas com uso intenso de CPU . Quando o thread principal está ocupado executando uma tarefa "pesada", fica inacessível para outras tarefas.


Mas e o libuv? Se você se lembra, essa biblioteca ajuda o Node.js. a executar operações de entrada / saída usando threads do sistema operacional, evitando o bloqueio do thread principal e você está absolutamente certo, esta é a solução para o nosso problema, mas para que isso seja possível, nosso módulo deve ser escrito no idioma C ++ para que o libuv possa trabalhar com ele.


Felizmente, a partir da v10.5, o módulo Native Threads do Trabalhador foi adicionado ao Node.js.



Trabalhadores e seus fluxos


Como a documentação nos diz:


Os trabalhadores são úteis para executar operações JavaScript com uso intenso de CPU; não os use para operações de entrada / saída; os mecanismos já incorporados ao Node.js são mais eficientes no manuseio dessas tarefas do que o encadeamento Worker.

Correção de código


É hora de reescrever nosso código:


primes-workerthreads.js


 const { workerData, parentPort } = require('worker_threads'); function isPrime(n) { for(let i = 2, s = Math.sqrt(n); i <= s; i++) if(n % i === 0) return false; return n > 1; } function nthPrime(n) { let counter = n; let iterator = 2; let result = []; while(counter > 0) { isPrime(iterator) && result.push(iterator) && counter--; iterator++; } return result; } parentPort.postMessage(nthPrime(workerData.n)); 

index-workerthreads.js


 const http = require('http'); const url = require('url'); const { Worker } = require('worker_threads'); const server = http.createServer(function (request, response) { const { pathname, query } = url.parse(request.url, true); if (pathname === '/primes') { const worker = new Worker('./primes-workerthreads.js', { workerData: { n: query.n || 0 } }); worker.on('error', function () { response.statusCode = 500; response.write('Oops there was an error...'); response.end(); }); let result; worker.on('message', function (message) { result = message; }); worker.on('exit', function () { response.setHeader('Content-Type', 'application/json'); response.write(JSON.stringify(result)); response.end(); }); } else { response.statusCode = 404; response.write('Not Found'); response.end(); } }); server.listen(8080); 

No index-workerthreads.js , cada solicitação para /primes cria uma instância da classe Worker (do módulo nativo worker_threads ) para fazer upload e executar o primes-workerthreads.js no encadeamento do trabalhador. Quando a lista de números primos é calculada e pronta, o evento da message é acionado - o resultado cai no fluxo principal devido ao trabalho que ainda não tem trabalho, ele também aciona o evento de exit , permitindo que o fluxo principal envie dados ao cliente.


primes-workerthreads.js mudou um pouco. Ele importa workerData (esta é uma cópia dos parâmetros passados ​​do encadeamento principal) e parentPort através dos quais o resultado do trabalho do trabalhador é passado de volta para o encadeamento principal.


Agora vamos tentar nosso exemplo novamente e ver o que acontece:



O encadeamento principal não está mais bloqueado !!!!!



Agora tudo funciona como deveria, mas produzir trabalhadores sem motivo ainda não é uma boa prática; criar threads não é um prazer barato. Certifique-se de criar um pool de threads antes disso.


Conclusão


O Node.js é uma tecnologia poderosa que deve ser explorada sempre que possível.
Minha recomendação pessoal - sempre seja curioso! Se você souber como algo funciona por dentro, pode trabalhar com ele de forma mais eficiente.


Isso é tudo por hoje pessoal. Espero que este post tenha sido útil para você e que você aprendeu algo novo sobre o Node.js.


Obrigado pela leitura e até a próxima postagem. .

Source: https://habr.com/ru/post/pt460661/


All Articles