Banner Post

Teste: Node JS Single Thread

Quando comecei a estudar Node JS, uma das primeiras coisas que vi foi que ele é Single Threaded, ou seja, atende as requisições de forma sequencial. Mas não é bem assim, Node é single threaded, mas também possui tarefas que rodam em background. Um post interessante com detalhes disso pode pode ser encontrado aqui. O intuito deste post é criar alguns testes simples para mostrar o comportamento do Node, quando ele é single threaded e quando consegue paralelizar requisições.

O Código

Para isso, primeiro vamos criar um package.json com as nossas dependêcias. Vamos utilizar somente o express para este teste.

package.json

{
  "name": "node-tests",
  "version": "1.0",
  "description": "Testes em node.",
  "main": "stTest.js",
  "dependencies": {
    "express" : "~4.0.0"
  }
}

Depois rodamos o npm install para baixar as dependências.

Agora, vamos declarar o uso de express e iniciar o servidor.

var express  = require('express');
var app      = express();
var port     = process.env.PORT || 8080;

//logica que vamos implementar

app.listen(port);
console.log('Servidor rodando na porta ' + port);

O próximo passo é criar os nossos testes. Vamos começar pelo teste de chamadas sequenciais, onde todo o processamento fica no nosso JavaScript e não nas tarefas que rodam em background.

app.get('/t1', function(req, res) {
	for (var i = 0; i < 5000000000; i++){var x = 1+1;}
	res.send('Test end');
});

Criei um loop que irá apenas somar 1+1, 5.000.000.000 (5 bilhões) de vezes. A operação 1+1 não é importante, o intuito era só criar um loop demorado o suficiente para que ficasse humanamente visível se o processamento ocorre de forma sequencial.

O próximo ponto é criar outra página onde as requisições seriam atendidades paralelamente, de forma simples. Para isso, precisamos utilizar recursos que são utilizados pelas tarefas que ficam em background. Neste caso, vamos utilizar o setInterval() com um tempo de 5 segundos.

app.get('/t2', function (req, res) {
	var interval = setInterval(function() {
		res.end('Interval ended');
		clearInterval(interval);
	}, 5000);
});

Nos meus testes, criei uma página inicial para eu poder acessar e testar se os tempos de resposta estavam adequados.

app.get('/', function (req, res) {
	res.end('<a href="/t1">Teste sequencial</a><br/><a href="/t2">Teste paralelo</a>');
});

O Teste

Para os testes, utilizei JMeter. Criei um grupo de usuários para cada página, com 10 usuários (threads), 0 segundos de inicialização entre eles (para simular acesso simultâneo) e somente 1 iteração.

Para o primeiro teste (página '/t1'), o loop de 5 bilhões de iterações, os resultados foram os da tabela abaixo (Listener do tipo Resultados em Tabela).

Amostra# Tempo de Início Tempo da amostra (ms) Estado Bytes Latência Tempo de Conexão (ms)
1 11:17:29.410 6888 191 6888 7
2 11:17:29.395 13765 191 13765 24
3 11:17:29.408 20606 191 20606 11
4 11:17:29.403 27489 191 27489 17
5 11:17:29.406 34337 191 34337 16
6 11:17:29.399 41213 191 41213 24
7 11:17:29.401 48106 191 48106 22
8 11:17:29.402 55050 191 55050 22
9 11:17:29.402 61908 191 61908 22
10 11:17:29.402 68755 191 68755 22

Para o segundo teste (página '/t2'), os resultados estão na tabela abaixo.

Amostra# Tempo de Início Tempo da amostra (ms) Estado Bytes Latência Tempo de Conexão (ms)
1 11:20:26.793 5029 137 5029 26
2 11:20:26.792 5031 137 5031 27
3 11:20:26.788 5040 137 5040 34
4 11:20:26.786 5042 137 5042 37
5 11:20:26.785 5044 137 5044 40
6 11:20:26.791 5042 137 5042 30
7 11:20:26.787 5047 137 5047 37
8 11:20:26.791 5043 137 5043 31
9 11:20:26.788 5048 137 5048 34
10 11:20:26.783 5057 137 5057 35

Como é possível ver, na página t1, as requisições foram retornadas a cada 7 segundos, mostrando que cada requisição foi processada individualmente antes de atender a próxima, enquanto no segundo exemplo todas as requisições foram atendidas após 5 segundos, e não uma a cada 5 segundos.

Explicação

O Node JS é single-threaded no ponto em que o código é executado em apenas uma thread, que fica em loop (Event Loop) atendendo as requisições, e executa chamadas para o pool de threads C++ que fica rodando em background, aguarando o retorno através de callbacks, conforme representado na imagem abaixo.

Todas as operações que são feitas pelo pool de threads (background workers) desbloqueiam o event loop para atender outras requisições/callbacks. Ou seja, o setInterval() passou a responsabilidade para o background e, enquanto isso, o Event Loop pôde responder a próxima requisição. O mesmo acontece quando executamos instruções que podem estar sendo executadas no banco de dados (como a chamada de uma stored procedure), por exemplo.

Conclusão

O Node JS é single-thread, mas possui um pool de threads que trabalham em background para paralelizar tarefas de I/O. Por essa razão, Node JS é performático para aplicações I/O intensive - com vazão elevada de dados -, e não é indicado para aplicações que necessitam CPU (processamento) intenso, uma vez que esses irão bloquear as requisições.

Caso queira testar, o código completo deste teste está abaixo.

var express  = require('express');
var app      = express();
var port     = process.env.PORT || 8080;

app.get('/', function (req, res) {
	res.end('<a href="/t1">Teste sequencial</a><br/><a href="/t2">Teste paralelo</a>');
});

app.get('/t1', function(req, res) {
	for (var i = 0; i < 5000000000; i++){var x = 1+1;}
	res.send('Test end');
});

app.get('/t2', function (req, res) {
	var interval = setInterval(function() {
		res.end('Interval ended');
		clearInterval(interval);
	}, 5000);
});


app.listen(port);
console.log('Servidor rodando na porta ' + port);