DEV Community

Cover image for Entendendo e solucionando o bloqueio do Event Loop no NodeJs [Parte 1]
Ronaldo Modesto
Ronaldo Modesto

Posted on • Updated on

Entendendo e solucionando o bloqueio do Event Loop no NodeJs [Parte 1]

Olá 😀.
Espero que estejam todos bem nesses tempos difíceis.
Com o passar dos anos o volume de informação disponível para consulta na internet aumentou exponencialmente. Falando especialmente de programação, o número de comunidades e locais de consultas que estão disponíveis para que possam ser acessados a fim de tentar solucionar os mais diversos tipos de problemas cresceu absurdos.

Isso é muito bom porque para nós, programadores, perder tempo com um problema é muito frustante e prejudicial também. Comunidades como StackOverflow por exemplo possuem um vasto conteúdo com descrições e soluções dos mais diversos tipos de problemas. É de fato uma mão na roda.

No entanto, essa grande disponibilidade de informações acabou tornando as pessoas preguiçosas. A maioria dos programadores, quando se deparam com um bug, correm para o Stackoverflow ou Quora e pesquisam pelo problema, acham uma solução e a copiam deliberadamente, sem nem mesmo tentar entender o que foi feito ou porquê aquela solução funciona. Esse hábito tem gerado códigos com uma qualidade cada vez pior.

Por isso é importante entendermos o que estamos fazendo e porquê, pois assim além de conseguirmos produzir códigos melhores, conseguiremos resolver uma gama maior de problemas.

Como eu tentei ser em didático durante o artigo ele acabou ficando um tanto quanto grande então ele será dividido em duas partes. Ao final desse aqui você vai encontrar um link para a segunda parte.

Então bora entender o que é o bloqueio do loop de eventos do NodeJs e como podemos resolver esse problema ?

Event Loop: Uma breve introdução e como funciona

O Event Loop é o mecanismo que possibilita o NodeJs executar operações que poderiam demorar muito de forma assíncrona, não prejudicando assim o desempenho geral do sistema. Uma vez que o processo do node se inicia inicia-se também o Event Loop que roda na thread principal ou main thread, a partir disso ele fica rodando enquanto o processo do node viver.

Ele é formado, não somente, mas principalmente por 5 fases. Em cada fase ele realiza operações específicas visando o não comprometimento da thread principal, delegando tarefas que demandam mais tempo para serem executadas para a libuv.

A libuv é a biblioteca escrita em C que permite ao node executar tarefas relacionadas ao kernel do SO de forma assíncrona. Ela é a responsável por lidar com Thread Pool. O Thread Pool(como o nome já sugere) é um conjunto de threads que ficam disponíveis para executar tarefas que serão entregues a elas pela libuv.

Pera pera pera, parou tudo!!!

Como assim conjunto de threads ??? Não havia uma thread só ?

Calma jovem padawan, eu explico. Ser single thread é uma característica do javascript. Isso se deve à história por trás do Javascript e como e para o quê ele foi concebido. Não vou entrar em detalhes aqui, mas deixarei nas referências onde você pode ler mais sobre isso.

Então, voltando ao assunto principal. O javascript é single thread e o NodeJs utiliza essa única thread que o javascript possui para executar o Event Loop.

Ele por sua vez entrega as tarefas para a libuv e fica ouvindo as respostas, esperando que as tarefas fiquem prontas, quando as tarefas terminam de executar, como por exemplo uma leitura de arquivos, o Event Loop então executa a callback associada àquela tarefa.

Isso é o que chamamos de Event-Driven Patern, o que é muito forte no node devido à essa característica de ele executar o loop de eventos em uma única thread. Event-Driven é um padrão de projetos baseado em eventos, onde uma tarefa é disparada após o término de outra. Mais ou menos assim, "Pegue essa tarefa demorada/pesada e mande processar, e assim que terminar, dispare um evento informando do fim dessa tarefa".

Um conceito importante que precisamos ter em mente para entender o problema que será mostrado, é a CallStack. A CallStack é uma fila do tipo LIFO (Last In Firt Out) ou (Último a entrar, Primeiro a sair). O Loop de eventos checa a todo instante a CallStack verificando se existe algo para ser processado, e caso tenha, ele a processa e então segue para a próxima função, caso exista.

O Event Loop pode ser dividido, principalmente mas não somente, em 5 fases. São elas ( explicação retirada da documentação oficial: https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/ )

Timers:
Nesta fase são executadas as callbacks agendadas por setTimeout e setInterval

Pedinding Calbacks:
Nesta fase estão as callbacks que foram agendadas para a próxima iteração do loop

idle, prepare:
Esta fase é usada internamente pelo Node. Ou seja, é uma fase que realiza operações internas ao node e não interfere de forma geral no fluxo de execução das tasks que é o que nos interessa para entende o problema de bloqueio do loop de eventos.

poll:
É nessa fase que o NodeJs checa por eventos de IO, como entrada de novas requisições por exemplo. Essa fase é muito importante para entendermos o impacto do bloqueio de eventos na aplicação como um todo.

check:
Nesta fase as callbacks que são agendadas com a função setImediate são executadas. Note que existe uma fase do loop de eventos somente para executar as callbacks agendadas por essa função, e de fato, ela é extremamente importante, inclusive a usaremos para desbloquear o loop de ventos.

close callbacks:
Nesta fase são executadas as callbacks de fechamento, por exemplo quando fechamos um socket com socket.on('close').

Isso foi um breve resumo mas já será o suficiente para entendermos o problema que quero mostrar e principalmente entender as soluções que serão apresentadas, ou seja, entender o porquê e como cada uma dessas soluções age no NodeJs permitindo o desbloqueio do loop de eventos.
No entanto deixarei na seção referências artigos e links da documentação contento explicações muito mais detalhadas sobre o NodeJs como um todo e principalmente sobre o Event Loop.

Recomendo fortemente a leitura de cada um deles pois esse é um dos principais e mais importantes conceitos sobre o NodeJs, além é claro de conter explicações sobre outros conceitos extremanente importantes como a MessageQueue, Libuv, web_workers, micro e macro tasks dentre outros.

Como ocorre o bloqueio do Event Loop ?

Em suma, esse bloqueio ocorre quando realizamos descuidadamente alguma operação bloqueante na thread principal, ou seja na main thread, que por sua vez é a thread sobre a qual o Event Loop executa. Quando bloqueamos essa thread o loop de eventos não consegue avançar para as outras fases, e com isso ele fica travado, ou seja, bloqueado, em uma única parte. Isso compromete toda a sua aplicação.

Lembra que dissemos que a fase de poll é a responsável por processar as requisições que chegam para a sua aplicação ? Pois então, imagine que a sua aplicação fique travada uma fase antes dela, se a fase de Pool não puder ser atingida, novas requisições nunca serão processadas, assim como respostas de outras possíveis requisições que ficaram prontas nesse meio tempo em que o loop estava bloqueado também não serão enviadas de volta para os usuários que as solicitaram.

Vamos ver na prática como podemos simular o bloqueio de Event Loop. Para demonstrar isso vamos utilizar as seguintes ferramentas:
NodeJs
VsCode ( ou qualquer outro editor de sua preferência). Lembrando que deixarei o projeto completo e do VsCode.

O projeto de testes

De forma resumida,essa é a estrutura do projeto que vamos utilizar
Projeto Node:
Vamos utilizar o express para servir 5 rotas. São elas:
/rota-bloqueante: Rota que vai bloquear todo o nosso sistema, será a nossa grande vilã.
/rota-bloqueante-com-chield-process: Executa a mesma operação da rota acima, porém de forma a não bloquear o loop de events se valendo de child_process para isso. É uma das soluções que vamos analisar.
/rota-bloqueante-com-setImediate: Assim como a rota anterior, executa uma opreção bloqueante, mas se utilizando da função setImediate para impedir o bloqueio do event-loop.
/rota-bloqueante-com-worker-thread: Executa a mesma operação bloqueante, mas se utiliza de workers_threads para evitar o bloqueio do event-loop.
/rota-nao-bloqueante: Rota que possui retorno imediato, será utilizada para testar a responsividade do nosso servidor.
Image description

Bloqueando o Event Loop

Para começar vamos simular uma situação na qual ocorre o bloqueio do loop de eventos. Com ele bloqueado vamos ver o que acontece com o resto do sistema.
Primeiro vamos fazer a requisição que não oferece bloqueio.

requisição-normal

Repare que esta rota leva apenas 22 ms em média para responder.

Agora vamos bloquear o event-loop e ver o que acontece se eu tentar chamar essa rota novamente.
Primeiro chamamos a rota /rota-bloqueante, ela leva mais ou menos 2 minutos e 50 segundos para responder.
rota-bloqueante

E para nossa surpresa(ou não rss), se tentamos fazer uma requisição para a rota nao-bloqueante, que a princípio deveria levar apenas alguns milissegundos para responder, temos uma desagradável surpresa.
requisicao-travada

Como podemos perceber, a requisição não-bloqueante demorou 2 minutos e 53 segundos para responder, isso é mais ou menos 7879 vezes mais lento do que deveria 😯.

Vamos trazer esse problema para uma situação real. Imagine que /rota-nao-bloqueante é uma rota de pagamento em sua api. Se nesse momento milhares de usuários tentassem efetuar um pagamento eles não iriam conseguir e você poderia perder milhares de vendas. Nada legal certo ?

Mas afinal, o que aconteceu ?

Vamos analisar o código atrás de respostas.

//Esse é a declaração da nossa rota bloqueante, ou seja,a  //rota que compromete nosso sistema
router.get('/rota-bloqueante', async (request, response) => {
  const generatedString = operacaoLenta();
  response.status(200).send(generatedString);
});
Enter fullscreen mode Exit fullscreen mode

Vamos analisar o código dessa função chamada operação lenta

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`;
}
Enter fullscreen mode Exit fullscreen mode

Vamos por partes.

const stringHash = crypto.createHash('sha512');
Enter fullscreen mode Exit fullscreen mode

Nesta linha nós criamos um hash vazio utilizando o algoritmo SHA512.

for (let i = 0; i < 10e6; i++) {
    stringHash.update(generateRandomString()); // operação extremamente custosa
  }
Enter fullscreen mode Exit fullscreen mode

Nesta linha nós fazemos 10^6 iterações atualizando o hash que criamos com uma função generateRandomString que gera uma string aleatória em hexadecimal. Aqui utilizamos a função randomBytes do módulo Crypto do NodeJs para deixar o processamento ainda mais pesado. Só por curiosidade esse é o código da função.

function generateRandomString() {
  return crypto.randomBytes(200).toString('hex');
}
Enter fullscreen mode Exit fullscreen mode

Claramente esse loop é o grande culpado pela lentidão. Mas vamos entender o porque esse loop aparentemente inofensivo afetou tão negativamente nosso sistema.

O problema aqui é que esse loop extremamente custoso, tanto em tempo como em processador, está rodando na Main Thead.

Lembra que dissemos que o Javascript possui apenas uma única thread e que era essa thread que o NodeJs utilizava para executar o event-loop ? Pois então, ao fazer essa operação, nós ocupamos essa thread totalmente, e isso impediu o Event Loop de seguir para as próximas fases, e por consequência ele não conseguiu processar a nossa requisição da rota /rota-nao-bloqueante.

Com isso dizemos que o Event Loop ficou bloqueado, ou seja incapaz de fazer qualquer outra coisa até que o trabalho que ocupava a thread principal terminasse.

Por isso que da segunda vez nossa requisição que deveria ser rápida levou 2 minutos e 53 segundos, porque a requisição que enviamos para essa rota ficou esperando até que o Event Loop chegasse na fase de Poll para que ele pegasse essa requisição e colocasse ela na fila para ser processada.

Beleza! Já vimos o que pode acontecer se não respeitarmos essas característica do NodeJs. No próximo artigo vamos ver como resolver esse problema!

Segue o link para a segunda parte e te aguardo lá 😃 😃 😃

Segunda parte

Clique aqui para ir para a segunda parte

Top comments (0)