Cluster Node – Aproveitando Todos os Núcleos do seu Processador

Desenho de CPU.

Será que você aproveita todos os recursos do seu servidor Node?

Sabemos que o Node é feito para trabalhar somente com uma thread e aliás, lida bem com isso e aguenta muita pancada. Mas, uma máquina comum tem em seu processador, pelo menos, quatro núcleos, não é mesmo? E um servidor em produção, pode ter até mais.

Então, quando você executa o arquivo responsável por iniciar o seu servidor, aquele muitas vezes denominado index.js e comumente colocado na raiz de um projeto node, se o processador de sua máquina possui mais de um núcleo e você não roda esse index.js através de um cluster, seu servidor estará rodando somente em um dos núcleos, sendo que os demais ficarão ociosos.

Para resolver esse problema, os engenheiros do Node criaram uma lib chamada cluster, que irá nos permitir criar threads filha a partir da principal, colocando-as (uma em cada núcleo do processador) para trabalhar em sincronia e sob o comando da thread principal.

Quer saber como implementar isso? Então leia este artigo até o final.

Como essa lib é nativa do Node, você não precisa baixar nada através do npm install. Basta importá-la no arquivo js onde você irá implementar o cluster e sair utilizando.

Você pode então criar um arquivo denominado cluster.js na raiz do seu projeto e importar a lib, caso queira já ir me acompanhando e implementar o cluster para o seu projeto.

const cluster = require('cluster');

Após a importação, já temos acesso à função fork() dessa biblioteca. Ela, quando invocada, gera uma nova thread, que passa a ser filha ou, tecnicamente falando, slave da thread principal (master).

const cluster = require('cluster');
cluster.fork();

Mas, só é possível criar forks a partir da thread principal. Você já vai entender o motivo.

Diante disso, temos que arranjar um jeito para diferenciar entre threads master ou slave.

Existe uma propriedade da biblioteca cluster chamada isMaster, que retorna true quando a thread é master.

É ela que utilizamos então, como condição de um if, para garantir que executaremos forks somente a partir da thread master.

const cluster = require('cluster'); /*Importando a lib*/

if(cluster.isMaster) {
	cluster.fork();
}

Antes de rodar o código, vamos colocar alguns logs nele para entender o que está acontecendo.

const cluster = require('cluster');

console.log('Executando thread');

if(cluster.isMaster) {

	console.log('Thread master');
	cluster.fork();
} else {
	console.log('Thread slave');
}

Executando nosso arquivo, através do comando node cluster.js, surge a thread master, que lê as instruções e cria uma thread slave. A slave nasce, já executando o mesmo arquivo. Com a diferença de que não entra no if, e sim no else. Portanto, a execução do código deve gerar a saída abaixo:

Executando thread
Thread master
Executando thread
Thread slave

Caso o programador se descuide, não fazendo a validação para garantir que um fork somente será executado em threads master, o código quebra. Dizendo que a função fork não está disponível para threads slave.

Veja. A execução do seguinte código:

const cluster = require('cluster');
console.log('Executando thread');
cluster.fork();

Gera o erro abaixo.

Ou seja, a thread master "printou" o log e criou uma slave que, ao executar o arquivo, também imprimiu o log. Mas não conseguiu criar uma filha de si.

Ainda bem que a galera do Node adotou essa postura defensiva. Do contrário, entraríamos em um loop onde o fork seria invocado para cada thread filha, gerando assim, mais threads filhas, a partir das filhas. Isso entupiria o processador de threads e, provavelmente, derrubaría a máquina.

Bom, aprendemos a gerar forks a partir da thread principal. E também, que não é possível gerá-las a partir de filhas. Agora, vamos ver como ter uma thread slave rodando por núcleo da máquina. Para isso, é necessário conhecer mais um módulo do Node, que irá nos auxiliar nesta missão.

Uma Thread Node por Núcleo do Processador

Existe outra lib, que também faz parte do core do Node, chamada OS, que provém de Operator System. Como você já pode imaginar, ela é especialista em fornecer informações sobre o sistema operacional.

O sistema operacional, por sua vez, conhece os recursos da máquina onde está instalado.

Uma das coisas que podemos perguntar para a lib OS é: quantas CPUs a máquina possui?

O que retorna um JSON, listando as CPUs da máquina. Veja:

const os = require('os');

const cpus = os.cpus();
console.log(cpus);

/*
Saída:
[
  {
    model: 'Intel(R) Core(TM) i5-7200U CPU @ 2.50GHz',
    speed: 2712,
    times: {
      user: 11964578,
      nice: 0,
      sys: 30438031,
      idle: 124314625,
      irq: 23672265
    }
  },
  {
    model: 'Intel(R) Core(TM) i5-7200U CPU @ 2.50GHz',
    speed: 2712,
    times: {
      user: 15191890,
      nice: 0,
      sys: 8210578,
      idle: 143314515,
      irq: 629937
    }
  },
  {
    model: 'Intel(R) Core(TM) i5-7200U CPU @ 2.50GHz',
    speed: 2712,
    times: {
      user: 16846500,
      nice: 0,
      sys: 8369765,
      idle: 141500718,
      irq: 313078
    }
  },
  {
    model: 'Intel(R) Core(TM) i5-7200U CPU @ 2.50GHz',
    speed: 2712,
    times: {
      user: 16719671,
      nice: 0,
      sys: 8046203,
      idle: 141951109,
      irq: 225078
    }
  }
]
*/

Continuando então a implementação do nosso cluster, vamos importar também esta nova api e solicitar a informação desejada.

const cluster = require('cluster');
const os = require('os');

const cpus = os.cpus();

console.log('Executando thread');

if(cluster.isMaster) {

	console.log('Thread master');
	cluster.fork();
} else {
	console.log('Thread slave');
}

Tendo a coleção das CPUs em mãos, dentro do if que verifica se a thread é master, faremos um forEach, criando um fork por núcleo.

const cluster = require('cluster');
const os = require('os');

const cpus = os.cpus();

console.log('Executando thread');

if(cluster.isMaster) {
	console.log('Thread master');

	cpus.forEach(() => {
		cluster.fork();
	});

} else {
	console.log('Thread slave');
}

Depois disso, teremos uma thread slave por núcleo do processador, e a master compartilhando um dos núcleos com uma das slaves.

Como toda thread slave entrará no else, é lá que vamos invocar nosso index.js.

O require agora não é de um módulo Node, e sim de um arquivo. Por isso, informamos o caminho até ele.

const cluster = require('cluster');
const os = require('os');

const cpus = os.cpus();

console.log('Executando thread');

if(cluster.isMaster) {
	console.log('Thread master');

	cpus.forEach(() => {
		cluster.fork();
	});

} else {
	console.log('Thread slave');
	require('./index.js');
}

Executando o cluster.js novamente, temos a seguinte saída:

C:\Users\Jonas\servidor-node>node cluster.js
Executando thread
Thread master
Executando thread
Thread slave
Executando thread
Thread slave
Executando thread
Thread slave
Executando thread
Thread slave
consign v0.1.6 Initialized in C:\Users\Jonas\servidor-node
+ .\src\app\controllers\raiz.js
consign v0.1.6 Initialized in C:\Users\Jonas\servidor-node
+ .\src\app\controllers\raiz.js
Servidor ouvindo na porta 3000
Servidor ouvindo na porta 3000
consign v0.1.6 Initialized in C:\Users\Jonas\servidor-node
consign v0.1.6 Initialized in C:\Users\Jonas\servidor-node
+ .\src\app\controllers\raiz.js
+ .\src\app\controllers\raiz.js
Servidor ouvindo na porta 3000
Servidor ouvindo na porta 3000

Poxa, mas quatro servidores compartilhando a porta 3000 não trará problemas?

Na verdade, não. Mesmo que tenhamos quatro threads rodando em uma mesma porta, a lib de cluster já sabe que a master ficará responsável por gerenciar as requisições, distribuindo as tarefas para cada um dos slaves, como uma espécie de balanceador de carga.

Ou seja, é a thread master que recebe as requisições e, através de uma inteligência interna para não sobrecarregar um dos slaves, faz a distribuição rotativa do trabalho.

Melhorando a Inteligência da Thread Master

O que vimos até agora é o básico para criar um cluster, e já atingimos o objetivo de ter uma thread Node rodando por núcleo do processador. Mas podemos aprimorar ainda um pouco essa implementação.

De que forma? Elaborando uma lógica para identificar quando uma nova thread surge ou deixa de existir, além de substituir as que caem inesperadamente.

Para isso, eu preciso te falar primeiro sobre mais alguns recursos disponíveis nessa api de cluster.

Talvez você saiba, talvez não, mas toda vez que uma thread filha é criada, junto com ela nasce também um objeto chamado Worker.

Esse objeto possui as informações, métodos e propriedades públicas dessa nova thread. Entre elas, o id do processo do sistema operacional onde essa thread está rodando. Temos acesso ao id, invocando a propriedade process.id desse objeto. Ele será útil daqui a pouco.

A lib de cluster também nos permite ouvir determinados eventos e executar tarefas específicas, por meio de funções callback, quando eles acontecem.

Escutadores de eventos são colocados no cluster por intermedio do método on. Esse método recebe, no primeiro parâmetro, o evento que estamos interessados em escutar e, no segundo, uma função callback para ser executada quando o evento acontecer.

Então através de escutadores colocados na master, vamos monitorar algumas atividades executadas pelas filhas.

O primeiro evento que estamos interessados em ouvir, é o de listening. Ele é disparado pelo nosso arquivo index.js (em outras palavras, pelo Express) quando seu método listen é invocado.

Então toda vez que uma slave executa o nosso index.js, o evento de listening será percebido pela thread master, através do escutador, e o worker responsável pela ação estará disponível no parâmetro do callback passado para esse escutador.

Isso porque, todo escutador da api de cluster fornece o worker que o disparou.

Pegamos então esse worker e pedimos para ele qual é id do processo do sistema operacional que o executa. É exatamente isso que está sendo feito na implementação abaixo.

const cluster = require('cluster');
const os = require('os');

const cpus = os.cpus();

console.log('Executando thread');

if(cluster.isMaster) {
	console.log('Thread master');

	cpus.forEach(() => {
		cluster.fork();
	});

	cluster.on('listening', worker => {
		console.log('Processo %d executando slave', worker.process.pid);
	});

} else {
	console.log('Thread slave');
	require('./index.js');
}

Na sequência, ao executar o código novamente, temos:

C:\Users\Jonas\servidor-node>node cluster.js
Executando thread
Thread master
Executando thread
Thread slave
Executando thread
Thread slave
Executando thread
Thread slave
Executando thread
Thread slave
consign v0.1.6 Initialized in C:\Users\Jonas\servidor-node
+ .\src\app\controllers\raiz.js
consign v0.1.6 Initialized in C:\Users\Jonas\servidor-node
+ .\src\app\controllers\raiz.js
Servidor ouvindo na porta 3000
Processo 13444 executando novo slave
Servidor ouvindo na porta 3000
Processo 14756 executando novo slave
consign v0.1.6 Initialized in C:\Users\Jonas\servidor-node
+ .\src\app\controllers\raiz.js
Servidor ouvindo na porta 3000
Processo 13684 executando novo slave
consign v0.1.6 Initialized in C:\Users\Jonas\servidor-node
+ .\src\app\controllers\raiz.js
Servidor ouvindo na porta 3000
Processo 15304 executando novo slave

Outra situação possível de acontecer, é a morte de algum slave. Nesse momento, o objeto Worker emite o evento exit.

Uma vez que o master está preparado para ouvir este evento, ele pode tomar a liberdade de criar um novo fork, para substituir o antigo que está morto. Portanto, incluímos também um escutador com esta finalidade abaixo.

const cluster = require('cluster');
const os = require('os');

const cpus = os.cpus();

console.log('Executando thread');

if(cluster.isMaster) {
	console.log('Thread master');

	cpus.forEach(() => {
		cluster.fork();
	});

	cluster.on('listening', worker => {
		console.log('Processo %d executando novo slave', worker.process.pid);
	});

	cluster.on('exit', worker => {
		console.log('Slave %d saiu inesperadamente', worker.process.pid);
		cluster.fork();
	});

} else {
	console.log('Thread slave');
	require('./index.js');
}

Para que possamos simular um worker sendo perdido e, em seguida, observar se outro é colocado no lugar, vamos executar um comando de terminal usado em sistemas operacionais Unix like para matar manualmente um processo.

Geralmente, a aba do terminal onde o servidor roda no momento, está ocupada e não permite a execução de novos comandos. Neste caso, abra outra ou até uma nova janela do terminal para fazer o teste.

Vou escolher matar o processo 15304. Em sistemas Unix, o comando é o seguinte:

kill -9 15304

Já no Windows ele é um pouco diferente, e encontra-se abaixo.

tskill 15304

Ou;

TASKKILL /PID 15304 /F
C:\Users\Jonas\servidor-node>node cluster.js
Executando thread
Thread master
Executando thread
Thread slave
Executando thread
Thread slave
Executando thread
Thread slave
Executando thread
Thread slave
consign v0.1.6 Initialized in C:\Users\Jonas\servidor-node
+ .\src\app\controllers\raiz.js
consign v0.1.6 Initialized in C:\Users\Jonas\servidor-node
+ .\src\app\controllers\raiz.js
Servidor ouvindo na porta 3000
Processo 13444 executando novo slave
Servidor ouvindo na porta 3000
Processo 14756 executando novo slave
consign v0.1.6 Initialized in C:\Users\Jonas\servidor-node
+ .\src\app\controllers\raiz.js
Servidor ouvindo na porta 3000
Processo 13684 executando novo slave
consign v0.1.6 Initialized in C:\Users\Jonas\servidor-node
+ .\src\app\controllers\raiz.js
Servidor ouvindo na porta 3000
Processo 15304 executando novo slave
Slave 15304 saiu inesperadamente                     <<<<<<<<< Observe Aqui
Executando thread
Thread slave
consign v0.1.6 Initialized in C:\Users\Jonas\servidor-node
+ .\src\app\controllers\raiz.js
Servidor ouvindo na porta 3000
Processo 1124 executando novo slave

Assim, finalizamos a construção do nosso cluster. Mas essa api é muito mais extensa e nos permite fazer bastante coisa. Tendo interesse, você pode se aprofundar melhor consultando a documentação oficial do Node.

Artigos Relacionados

Ir para o topo