O Problema de Performance no Node.js
Node.js é single-threaded por padrão, o que significa que suas aplicações rodam em um único núcleo de processamento, independentemente de quantos núcleos sua máquina possua. Em um servidor com 8 núcleos, você estaria utilizando apenas 1, deixando 7 ociosos. Isso representa um desperdício enorme de recursos, especialmente em aplicações CPU-intensivas como processamento de imagens, criptografia ou cálculos complexos. A solução para este problema é justamente explorar o módulo cluster do Node.js ou utilizar Worker Threads para paralelizar o trabalho.
O cluster permite criar múltiplos processos filhos (workers), enquanto as Worker Threads utilizam threads dentro do mesmo processo. Ambas as abordagens têm seus casos de uso. Nesta aula, você aprenderá quando e como usar cada uma para maximizar a performance de suas aplicações.
Cluster: Criando Múltiplos Processos
Como Funciona o Cluster
O módulo cluster usa o modelo master-worker. O processo master distribui as conexões entre os workers, que são processos separados do Node.js. Cada worker pode executar código de forma independente, utilizando núcleos diferentes da CPU. Quando um worker morre, o master pode criar um novo, oferecendo alta disponibilidade.
const cluster = require('cluster');
const http = require('http');
const os = require('os');
if (cluster.isMaster) {
const numWorkers = os.cpus().length;
console.log(`Master ${process.pid} iniciando ${numWorkers} workers...`);
// Criar um worker para cada núcleo da CPU
for (let i = 0; i < numWorkers; i++) {
cluster.fork();
}
// Se um worker morrer, criar um novo
cluster.on('exit', (worker, code, signal) => {
console.log(`Worker ${worker.process.pid} morreu`);
cluster.fork();
});
} else {
// Código do worker
const server = http.createServer((req, res) => {
// Simular processamento
let sum = 0;
for (let i = 0; i < 1000000000; i++) {
sum += i;
}
res.writeHead(200);
res.end(`Worker ${process.pid} respondendo\n`);
});
server.listen(3000);
console.log(`Worker ${process.pid} iniciado`);
}
Neste exemplo, o master cria um worker para cada núcleo disponível. Quando você acessa localhost:3000, diferentes workers respondem as requisições. Se um worker falhar, o master automaticamente cria um novo. Este padrão é perfeito para servidores web que precisam de alta disponibilidade.
Comunicação Entre Processos
Os workers podem se comunicar com o master através de mensagens. Isso é útil quando você precisa coletar estatísticas ou coordenar ações entre processos.
if (cluster.isMaster) {
const server = http.createServer((req, res) => {
// Master pode responder ou delegar
res.writeHead(200);
res.end('Master respondendo\n');
}).listen(3000);
for (let i = 0; i < 2; i++) {
const worker = cluster.fork();
worker.on('message', (msg) => {
console.log(`Master recebeu: ${msg.count}`);
});
}
} else {
setInterval(() => {
process.send({ count: Math.random() });
}, 2000);
}
Worker Threads: Paralelismo Dentro de Um Processo
Quando Usar Worker Threads
Diferentemente de cluster, as Worker Threads dividem memória dentro do mesmo processo, tornando a comunicação mais eficiente. São ideais para tarefas CPU-intensivas que não precisam de toda uma instância do Node.js. Se você precisa fazer criptografia pesada, transformação de imagens ou cálculos matemáticos, Worker Threads é sua melhor opção.
const { Worker } = require('worker_threads');
const path = require('path');
// Arquivo: worker.js
// console.log('Processamento no worker...');
function criarWorker() {
return new Promise((resolve, reject) => {
const worker = new Worker(path.join(__dirname, 'worker.js'));
worker.on('message', (result) => {
resolve(result);
worker.terminate();
});
worker.on('error', reject);
worker.on('exit', (code) => {
if (code !== 0) {
reject(new Error(`Worker parou com código ${code}`));
}
});
// Enviar dados para o worker
worker.postMessage({ number: 100000000 });
});
}
// No arquivo worker.js:
const { parentPort } = require('worker_threads');
parentPort.on('message', (data) => {
let sum = 0;
for (let i = 0; i < data.number; i++) {
sum += i;
}
parentPort.postMessage({ result: sum });
});
Execute node arquivo-principal.js após criar ambos os arquivos. O worker processa o número pesado sem bloquear a thread principal.
Pool de Workers
Para otimizar, crie um pool reutilizável de workers em vez de criar um novo a cada tarefa:
const { Worker } = require('worker_threads');
const path = require('path');
class WorkerPool {
constructor(poolSize = 4) {
this.workers = [];
this.taskQueue = [];
for (let i = 0; i < poolSize; i++) {
const worker = new Worker(path.join(__dirname, 'worker.js'));
worker.busy = false;
this.workers.push(worker);
}
}
executeTask(data) {
return new Promise((resolve, reject) => {
const availableWorker = this.workers.find(w => !w.busy);
if (availableWorker) {
this.runTask(availableWorker, data, resolve, reject);
} else {
this.taskQueue.push({ data, resolve, reject });
}
});
}
runTask(worker, data, resolve, reject) {
worker.busy = true;
const handler = (result) => {
worker.removeListener('message', handler);
worker.removeListener('error', errorHandler);
worker.busy = false;
resolve(result);
if (this.taskQueue.length > 0) {
const { data, resolve, reject } = this.taskQueue.shift();
this.runTask(worker, data, resolve, reject);
}
};
const errorHandler = reject;
worker.once('message', handler);
worker.once('error', errorHandler);
worker.postMessage(data);
}
}
// Uso:
const pool = new WorkerPool(4);
async function processar() {
const resultado = await pool.executeTask({ number: 50000000 });
console.log(resultado);
}
processar();
Cluster vs Worker Threads: Escolhendo a Estratégia Certa
Use Cluster quando você tiver múltiplas instâncias HTTP que compartilham a mesma porta, precisar de isolamento total entre processos ou quando a falha de um worker exigir reinicialização completa. Use Worker Threads quando o trabalho for CPU-intensivo e de curta duração, você quiser compartilhar memória eficientemente ou precisar de comunicação frequente entre tarefas paralelas.
Em aplicações reais, as duas abordagens podem coexistir: use cluster para distribuir requisições HTTP entre múltiplos processos e Worker Threads dentro de cada worker para tarefas pesadas. Essa combinação oferece o melhor dos dois mundos: escalabilidade horizontal via cluster e paralelismo fino via threads.
Conclusão
Você aprendeu que o Node.js oferece duas soluções poderosas para aproveitar múltiplos núcleos: cluster para processos independentes com auto-recuperação, e Worker Threads para paralelismo eficiente em memória. Implemente o cluster para suas APIs HTTP e considere Worker Threads para processamento pesado. A escolha certa aumentará drasticamente a performance de suas aplicações em servidores multi-core.