Agora que já vimos o problemão que o travamento do loop de eventos pode nos causar quando não respeitamos o funcionamento do NodeJs, vamos ver 3 formas de resolver esse problema.
Caso tenha chegado aqui de pára-quedas aqui vai o link para a primeira parte deste artigo. Para acessar a primeira parte clique aqui
Vamos resolver o problema!!
Beleza, já sabemos o que aconteceu, mas como podemos resolver esse problema e evitar que a nossa api inteira seja comprometida por causa um único endpoint?
Vou apresentar três soluções para essa questão, explicando o porquê cada solução funciona. Vamos lá.
Criando um processo filho
Uma das formas de solucionar esse problema é criando um processo filho (child process). Child Process, como o próprio nome sugere, são sub-processos que são criados e que possuem um canal de comunicação com o processo pai, que no caso é o processo principal.
Cada ChildProcess possui seu próprio eventLoop e sua thread de execução, isso permite que cada processo lide com suas operações.Esse canal é o meio pelo qual o processo filho envia as informações para o processo pai em forma de eventos. Novamente, sugiro que pesquisem sobre o Event Driven Pattern caso não o conheçam.
No entanto é importante usar o childProcess com cuidado. Todas as vezes que você cria um processo filho ele aloca tudo que ele precisa novamente, pois é criado um novo processo do NodeJs e isso pode custar muito caro em termos de memória.
Essa solução funciona assim:
- Requisição chega ao endpoint.
- Cria-se um processo filho utilizando o módulo de "child-process" do Nodejs.
- Todo o processamento é feito em uma novo processo, permitindo que a thread principal continue a executar o Event Loop e por consequência não comprometendo mais o sistema. Ou seja, o sistema fica livre para processar outras requisições que chegarem.
- Quando o processamento da função terminar, ele devolve o conteúdo solicitado através de um evento para o processo pai e este então encaminha o conteúdo para o response que por fim termina a requisição enviando o hash criado para o usuário. Vamos ver isso em código.
router.get('/rota-bloqueante-com-chield-process', async (request, response) => {
const worker = fork('./src/heavy-load/compute-hash.js');
worker.send('message');
worker.on('message', (generatedString) => {
response.status(200).send(generatedString);
worker.kill();
});
});
A função "Fork" foi importada do módulo child-process.
A seguir o código utilizado para criar o child-process
const { operacaoLenta } = require('../helpers/functions');
process.on('message', () => {
const hashedSring = operacaoLenta();
process.send(hashedSring);
});
Função bloqueante que demora a retornar
function operacaoLenta() {
const stringHash = crypto.createHash('sha512');
// eslint-disable-next-line no-plusplus
for (let i = 0; i < 10e6; i++) {
stringHash.update(generateRandomString()); // operação extremamente custosa
}
return `${stringHash.digest('hex')}\n`;
}
Utilizando worker_threads
Uma outra forma de solucionar esse problema é utilizando worker_threads. worker_threads são threads filhas que derivam da thread principal, semelhante ao que ocorre com os processos filhos.
No entanto elas diferem dos processos filhos pelo fato de serem muito mais leves, dado que ela reutilizam o contexto de execução da thread principal, sendo assim, sempre que uma thread filha é criada ela não re-instancia todos os recursos que ela precisa, sendo assim muito mais eficiente no uso de memória.
Cada thread possui seu próprio eventLoop, o que a possibilita lidar com suas próprias operações, assim como os processos filhos.
Essa solução funciona de modo semelhante ao que foi feito com o ChildProcess:
- Requisição chega ao endpoint.
- Cria-se um worker que irá operar uma thread filha.Ele recebe o path do arquivo onde a lógica do worker está implementada.
- Todo o processamento é feito em uma nova thread, permitindo, assim como a implementação que usa child-process, que a thread principal continue a executar o Event Loop e por consequência não comprometa mais o sistema.
- Quando o processamento da função terminar, ele devolve o conteúdo solicitado através de um evento para a thread principal e esta então encaminha o conteúdo para o response que por fim termina a requisição enviando o hash calculado para o usuário.
Vamos ao código.
router.get('/rota-bloqueante-com-worker-thread', async (request, response) => {
const worker = new Worker('./src/heavy-load/worker.js');
// Listen for a message from worker
worker.on('message', (generatedString) => {
response.status(200).send(generatedString.hashedSring);
});
worker.postMessage('message');
});
Lógica do worker que fica separada em um arquivo a parte
const { parentPort } = require('worker_threads');
const { operacaoLenta } = require('../helpers/functions');
parentPort.on('message', () => {
const hashedSring = operacaoLenta();
parentPort.postMessage({
hashedSring,
});
});
Dado que as soluções acima apresentadas parecem,a princípio, serem a mesma coisa, segue uma imagem que exemplifica bem a diferença entre workers_threads e child-process. Repare que, child-process aloca todo um novo processo do NodeJs,e por consequência re-aloca todos os recursos necessários.
Utilizando a função setImmediate
Uma terceira solução que irei apresentar aqui é o uso da função setImmediate().
Para entender como essa função funciona precisamos relembrar quais são as fases do event-loop e, principalmente, qual a ordem na qual elas são chamadas.
Vamos olhar atentamente o que diz a documentação oficial do NodeJs a respeito das fases do event-loop.
Retirado da documentação oficial do NodeJs.
Repare que, a fase de check ocorre depois da fase de poll. A fase de poll é a responsável por obter novos eventos de IO, ou seja, novas requisições que chegam à aplicação.
Dado que a função setImmediate(()=>{}) agenda uma callback para a próxima iteração do event-loop, quando usamos a função setImmediate(()=>{}), estamos dizendo ao NodeJs o seguinte, "Essa função só deve ser chamada na sua próxima iteração", e, como a fase de pool está antes da fase de check, o loop de eventos não fica travado, pois ele não vai ficar esperando o resultado da callback agendada com a setImmediate(()=>{}), ele vai continuar sua iteração e quando ele chegar na fase de Timers ele vai verificar o pool de callbacks e caso a função agendada já esteja pronta para ser chamada ela será então colocada na iteração atual do event-loop, e por consequência será invocada na próxima fase de pending callbacks.
Segue um diagrama, de própria autoria, que demonstra como ocorre esse processo e porque ele permite ao event-loop continuar operando sem ser bloqueado.
Essa solução funciona assim:
- Requisição chega ao endpoint.
- Chama-se a função que encapsula a solução utilizando setImmediate().
- Então, dentro do for de iteração nós registramos, para cada iteração, uma callback que será chamada na próxima iteração do loop, quando chegar na última iteração, vai agendar a última callback que, quando for chamada na fase de Timers,será colocada na fila para ser evocada na próxima iteração do loop e irá retornar o resultado da operação bloqueante.
Neste caso em específico, não é uma saída muito interessante pois você está agendando 10⁶ callbacks, mas cada caso é um caso e aqui estamos apenas fazendo um estudo de o porquê tais soluções funcionam.
Vamos ao código dessa solução.
router.get('/rota-bloqueante-com-setImediate', async (request, response) => {
const generatedString = await operacaoLentaComSetImediate();
response.status(200).send(generatedString);
});
Código da função que processa a operação agendando as callbacks de retorno.
async function operacaoLentaComSetImediate() {
const stringHash = crypto.createHash('sha512');
// eslint-disable-next-line no-plusplus
for (let i = 0; i < 10e6; i++) {
stringHash.update(generateRandomString()); // operação extremamente custosa
// eslint-disable-next-line no-await-in-loop
await setImmediatePromise();
}
return `${stringHash.digest('hex')}\n`;
}
A seguir o código da função setImmediatePromise()
function setImmediatePromise() {
return new Promise((resolve) => {
setImmediate(() => resolve());
});
}
Essas foram apenas algumas opções, com seus prós e contras, existem várias formas de se resolver o problema proposto.
O importante é entender o que é o event-loop e como ele funciona, dessa forma, se você topar com algum problema relacionado a isso você saberá como proceder para resolver a situação.
Dicas para evitar o bloqueio do event-loop
Evite usar as versões síncronas (Sync) das funções disponíveis nos módulos Zlib, crypto, fs entre outros que possuem funções que fazem alto uso da cpu.
Não realize operações de cálculos intensivas na thread principal, como por exemplo cálculos pesados de cpu.
Muito cuidado ao operar json's muito grandes.
Muito cuidado com expressões regulares, pois a avaliação dela pode custar caro para o sistema em questão de performance, inclusive existem padrões de expressões regulares que são vulneráveis a ataques, mas isso é assunto para outro artigo.
Bom é isso, espero que tenham gostado e principalmente entendido a importância de conhecer o event-loop. NodeJs é uma tecnologia incrível, mas demanda certo domínio que muitos programadores não possuem, e isso pode gerar um mal uso da tecnologia que pode anular os seus grandes benefícios.
Fiquem bem e até a próxima 😃 !!!
Repositório do projeto
Repositório do projeto utilizado
========================================================
Referências
O que é o NodeJs(Documentação oficial)
========================================================
O que é NodeJs (Complemento)
========================================================
Event Loop(Documentação Oficial)
========================================================
Não-bloqueio do event loop (Documentação oficial)
========================================================
Diferença entre worker threads e child process
========================================================
Trabalhando com worker threads
========================================================
História do javascript .
Entre outras páginas, foram muitos sites visitados para leitura e entendimento desse assunto 😃
Top comments (4)
I started read your posts before some times and I got really helpful information. I am thankful to you for sharing such an amazing post. Keep it up!!
Regards, Viaana.
Hire NodeJS Developer
Hey friend thank you very much :)
Muito bom. Acho que é o único texto que li em português sobre o assunto.
Não sei se leu esse texto que pode ajudar a complementar o que você escreveu!
snyk.io/blog/nodejs-how-even-quick...
Abraços
Opa boa tarde amigo. Ainda não li esse artigo mas com certeza vou ler e possivelmente até incrementar este artigo que fiz. Muito obrigado pela dica :)) Esse é meu primeiro artigo então toda possibilidade de melhora é super bem vinda :)