DEV Community

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

Posted on • Edited on

3 2

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

Image of Timescale

Timescale – the developer's data platform for modern apps, built on PostgreSQL

Timescale Cloud is PostgreSQL optimized for speed, scale, and performance. Over 3 million IoT, AI, crypto, and dev tool apps are powered by Timescale. Try it free today! No credit card required.

Try free

Top comments (0)

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more