Você nunca pode entender tudo, mas você deve se esforçar para entender o sistema - Ryan Dahl, criador do NodeJs
O que é Nodejs?
Para começar vamos colocar que NodeJs, não é:
- Uma linguagem
- Um framework
NodeJs pode ser definido como um ambiente de execução (runtime) de códigos Javascript server-side. Os principais motivos para a sua adoção se dão pela fácil escalabilidade, baixo custo e flexibilidade o que torna o seu uso adequado para a programação de microsserviços e componentes de uma arquitetura serverless.
Seu criador, Ryan Dahl, estava trabalhando numa feature para um sistema que se assemelhava muito com uma barra de progresso, features como essa têm a característica de consumirem muita memória e processamento, isso porque são feitas muitas operações de comunicação com o banco de dados. Naquele momento a grande maioria das plataformas faziam uso de uma abordagem de I/O baseadas em threads, isso significa que operações que precisam esperar por uma resposta, como a query feita ao banco de dados, ficam "presas" dentro de uma thread que tem o seu estado marcado como waiting. Justamente por isso o consumo de memória e processamento da aplicação subia muito, as threads precisam guardar as informações na memória, e o escalonador de processos responsável por fazer a alternância entre as threads consome um bom tempo de processamento.
O principal diferencial do NodeJs para outras tecnologias como PHP, C# e Java é que ele é Single-Thread. Nas outras tecnologias uma thread é criada para cada requisição feita, então se uma dessas threads faz uma operação muito demorada ela vai ficar travada até o final da execução. O modelo Single-Thread é o modelo padrão, mais comum de se encontrar, mais fácil de programar, porém mais oneroso para os recursos computacionais. Já no modelo NodeJs temos apenas uma thread chamada de Event Loop, a princípio as requisições são colocadas na fila de requisições (ou também fila de eventos) e o event loop vai desempilhar cada uma dessas requisições e tratá-las. A máquina virtual do Javascript então verifica o que tem de ser feito, delega a sua execução e volta para atender as outras requisições, quando a atividade delegada termina então ela volta para o fluxo principal para ser devolvida ao requisitante.
A analogia do restaurante
Quando um novo cliente chega num restaurante um garçom vai prontamente atendê-lo, anota o seu pedido e repassa o mesmo para o cozinheiro que começa a prepará-lo então volta para o salão para atender novos clientes que chegam, anota novos pedidos e repassa-os ao cozinheiro, esse ciclo é repetido diversas vezes, então a medida que os pedidos vão ficando prontos eles são entregues aos clientes pelo mesmo garçom. É mais ou menos assim que o Node funciona, podemos ver o garçom como o event loop e o cozinheiro como a worker pool, os clientes são os usuários de uma determinada aplicação, os pratos são os processos de regras de negócio como por exemplo, acessar o banco de dados.
A arquitetura NodeJs
O NodeJs é composto por componentes e cada um deles possui uma função específica, no coração dele temos o V8, uma engine Javascript usada principalmente dentro do navegador Google Chrome, ela vai ser a responsável por executar o código escrito em Javascript, então temos os NodeJs addons, código escrito em C responsável por comunicar (ligar) o V8 com o sistema operacional. Por último, e não menos importante temos a biblioteca libuv, uma biblioteca escrita em C especialmente para o NodeJs que tem como função abstrair operações não bloqueantes de I/O, isso permite que o CPU e outros recursos sejam usados enquanto ainda estão executando operações de entrada e saída.
As threads dentro do NodeJs
A pesar do NodeJs ser em si Single-Thread ele faz uso de outras threads para a execução das atividades, temos dois tipos básicos de threads dentro do Node, a thread do Event Loop e as Threads do Worker Pool.
Qual código é executado dentro do Event Loop?
Os aplicativos NodeJs passam pela fase de inicialização, onde fazem a solicitação dos módulos e registram os callbacks que devem ser executados quando houver a resposta dos eventos. Dentro do event loop as solicitações feitas pelas requisições são respondidas de forma síncrona. O event loop também é responsável por registrar todas as requisições assíncronas para continuar o processamento após a conclusão. Os retornos das funções assíncronas também são executados dentro do event loop.
Em resumo, o Event Loop executa os callbacks JavaScript registrados para eventos e também é responsável por atender solicitações assíncronas sem bloqueio, como entrada e saída de rede.
Qual código é executado dentro do worker pool?
O worker pool é implementado sobre a libuv, que expõe uma api que permite enviar tarefas, porém as tarefas reservadas ao worker pool são aquelas que são "caras" no sentido de processamento, basicamente toda e qualquer tarefa a qual o sistema operacional não fornece de forma não bloqueante. Exemplo: se um programa está consultando o banco de dados, a CPU fica ociosa até que a consulta seja processada, nesse meio tempo o programa fica parado, causando desperdício de recursos do sistema. Para evitar isso, o libuv é usado no NodeJs, o que facilita operações de I/O sem bloqueio.
Como o node decide que código vai ser executado?
Através de filas, o worker pool tem uma fila que armazenas as tarefas a serem executadas, de forma com que ela pega a primeira tarefa dessa fila, trabalha nela e quando termina lança uma evento "At least one task is finished" para o event loop. Este por outro lado, na verdade, não mantém de fato uma fila, mas sim uma coleção de descritores de arquivos que juntamente com o sistema operacional monitora os eventos. Quando o sistema operacional diz que um determinado descritor de arquivo está pronto, o event loop traduz para o evento apropriado e chama o callback associado com ele.
O significado para o design de aplicações
Em sistemas preemptivos que alocam uma thread por cliente como o apache, sempre que uma das threads fica em estado de bloqueado por muito tempo o sistema vai interromper a mesma e passar a vez para outra thread, outro cliente. Mas o Node é não preemptivo, isso significa que nenhuma thread é interrompida. Isso diminui muito o consumo de memória e processamento, porque diminui a incidência de troca de contexto na aplicação.
Em sistemas preemptivos, onde temos um thread por cliente temos de fazer a troca de contexto com uma certa frequência, assim temos de usar muito mais memória se comparado a um sistema não preemptivo. Na imagem acima temos o apache e o nginx, esse último foi de onde o conceito de event loop foi retirado por Ryan Dahl, note que o Apache, preemptivo, que aloca uma thread por requisição, consome muito mais memória que o nginx que possui processamento paralelo.
O processamento paralelo
A libuv é uma biblioteca cross-platform que permite I/O assíncrono, ela foi desenhada primeiramente para lidar com o Nodejs, mas também é usada em outros ambientes como Julia, Luvit e uvloop. Dentre suas principais responsabilidades temos:
- Um event loop que é uportado em qualquer plataforma
- Disponibilização de sockets TCP e UDP de forma assíncrona
- Disponibilização de eventos do file system
- Controle de child processes
- Thread pool
- Signal handling
Bibliografia:
https://www.youtube.com/watch?v=nfrVPzDJZQc
https://stackoverflow.com/questions/36766696/which-is-correct-node-js-architecture
https://www.geeksforgeeks.org/libuv-in-node-js/
https://nodejs.org/en/docs/guides/dont-block-the-event-loop/
Top comments (0)