JavaScript Avançado

Boas Práticas de Cluster e Worker Threads em Node.js: Aproveitando Múltiplos Núcleos para Times Ágeis

7 min de leitura

Boas Práticas de Cluster e Worker Threads em Node.js: Aproveitando Múltiplos Núcleos para Times Ágeis

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 do Node.js ou utilizar para paralelizar o trabalho. O permite criar múltiplos processos filhos (workers), enquanto as 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 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

<h2>O Problema de Performance no Node.js</h2>

<p>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 <code>cluster</code> do Node.js ou utilizar <code>Worker Threads</code> para paralelizar o trabalho.</p>

<p>O <code>cluster</code> permite criar múltiplos processos filhos (workers), enquanto as <code>Worker Threads</code> 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.</p>

<h2>Cluster: Criando Múltiplos Processos</h2>

<h3>Como Funciona o Cluster</h3>

<p>O módulo <code>cluster</code> 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.</p>

<pre><code class="language-javascript">const cluster = require(&#039;cluster&#039;);

const http = require(&#039;http&#039;);

const os = require(&#039;os&#039;);

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 &lt; numWorkers; i++) {

cluster.fork();

}

// Se um worker morrer, criar um novo

cluster.on(&#039;exit&#039;, (worker, code, signal) =&gt; {

console.log(Worker ${worker.process.pid} morreu);

cluster.fork();

});

} else {

// Código do worker

const server = http.createServer((req, res) =&gt; {

// Simular processamento

let sum = 0;

for (let i = 0; i &lt; 1000000000; i++) {

sum += i;

}

res.writeHead(200);

res.end(Worker ${process.pid} respondendo\n);

});

server.listen(3000);

console.log(Worker ${process.pid} iniciado);

}</code></pre>

<p>Neste exemplo, o master cria um worker para cada núcleo disponível. Quando você acessa <code>localhost:3000</code>, 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.</p>

<h3>Comunicação Entre Processos</h3>

<p>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.</p>

<pre><code class="language-javascript">if (cluster.isMaster) {

const server = http.createServer((req, res) =&gt; {

// Master pode responder ou delegar

res.writeHead(200);

res.end(&#039;Master respondendo\n&#039;);

}).listen(3000);

for (let i = 0; i &lt; 2; i++) {

const worker = cluster.fork();

worker.on(&#039;message&#039;, (msg) =&gt; {

console.log(Master recebeu: ${msg.count});

});

}

} else {

setInterval(() =&gt; {

process.send({ count: Math.random() });

}, 2000);

}</code></pre>

<h2>Worker Threads: Paralelismo Dentro de Um Processo</h2>

<h3>Quando Usar Worker Threads</h3>

<p>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.</p>

<pre><code class="language-javascript">const { Worker } = require(&#039;worker_threads&#039;);

const path = require(&#039;path&#039;);

// Arquivo: worker.js

// console.log(&#039;Processamento no worker...&#039;);

function criarWorker() {

return new Promise((resolve, reject) =&gt; {

const worker = new Worker(path.join(__dirname, &#039;worker.js&#039;));

worker.on(&#039;message&#039;, (result) =&gt; {

resolve(result);

worker.terminate();

});

worker.on(&#039;error&#039;, reject);

worker.on(&#039;exit&#039;, (code) =&gt; {

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(&#039;worker_threads&#039;);

parentPort.on(&#039;message&#039;, (data) =&gt; {

let sum = 0;

for (let i = 0; i &lt; data.number; i++) {

sum += i;

}

parentPort.postMessage({ result: sum });

});</code></pre>

<p>Execute <code>node arquivo-principal.js</code> após criar ambos os arquivos. O worker processa o número pesado sem bloquear a thread principal.</p>

<h3>Pool de Workers</h3>

<p>Para otimizar, crie um pool reutilizável de workers em vez de criar um novo a cada tarefa:</p>

<pre><code class="language-javascript">const { Worker } = require(&#039;worker_threads&#039;);

const path = require(&#039;path&#039;);

class WorkerPool {

constructor(poolSize = 4) {

this.workers = [];

this.taskQueue = [];

for (let i = 0; i &lt; poolSize; i++) {

const worker = new Worker(path.join(__dirname, &#039;worker.js&#039;));

worker.busy = false;

this.workers.push(worker);

}

}

executeTask(data) {

return new Promise((resolve, reject) =&gt; {

const availableWorker = this.workers.find(w =&gt; !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) =&gt; {

worker.removeListener(&#039;message&#039;, handler);

worker.removeListener(&#039;error&#039;, errorHandler);

worker.busy = false;

resolve(result);

if (this.taskQueue.length &gt; 0) {

const { data, resolve, reject } = this.taskQueue.shift();

this.runTask(worker, data, resolve, reject);

}

};

const errorHandler = reject;

worker.once(&#039;message&#039;, handler);

worker.once(&#039;error&#039;, errorHandler);

worker.postMessage(data);

}

}

// Uso:

const pool = new WorkerPool(4);

async function processar() {

const resultado = await pool.executeTask({ number: 50000000 });

console.log(resultado);

}

processar();</code></pre>

<h2>Cluster vs Worker Threads: Escolhendo a Estratégia Certa</h2>

<p>Use <strong>Cluster</strong> 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 <strong>Worker Threads</strong> 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.</p>

<p>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.</p>

<h2>Conclusão</h2>

<p>Você aprendeu que o Node.js oferece duas soluções poderosas para aproveitar múltiplos núcleos: <strong>cluster</strong> para processos independentes com auto-recuperação, e <strong>Worker Threads</strong> 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.</p>

<h2>Referências</h2>

<ul>

<li><a href="https://nodejs.org/api/cluster.html" target="_blank" rel="noopener noreferrer">Node.js Cluster Documentation</a></li>

<li><a href="https://nodejs.org/api/worker_threads.html" target="_blank" rel="noopener noreferrer">Node.js Worker Threads Documentation</a></li>

<li><a href="https://nodejs.org/en/blog/announcements/workers-threads-move-forward" target="_blank" rel="noopener noreferrer">Using Worker Threads in Node.js - Node.js Blog</a></li>

<li><a href="https://medium.com/@ashleydw/scaling-nodejs-applications-8492ff8d33d1" target="_blank" rel="noopener noreferrer">Scaling Node.js Applications - Medium</a></li>

<li><a href="https://nodejs.org/en/docs/guides/nodejs-performance-best-practices/" target="_blank" rel="noopener noreferrer">Node.js Performance Best Practices - Official Guide</a></li>

</ul>

Comentários

Mais em JavaScript Avançado

Programação Funcional em JavaScript: Imutabilidade, Pureza e Composição: Do Básico ao Avançado
Programação Funcional em JavaScript: Imutabilidade, Pureza e Composição: Do Básico ao Avançado

Imutabilidade: O Fundamento da Programação Funcional A imutabilidade é o pila...

Programação Reativa em JavaScript: Conceitos e RxJS na Prática: Do Básico ao Avançado
Programação Reativa em JavaScript: Conceitos e RxJS na Prática: Do Básico ao Avançado

Fundamentos da Programação Reativa A programação reativa é um paradigma que t...

Dominando React Internals: Reconciler, Fiber Architecture e Rendering Phases em Projetos Reais
Dominando React Internals: Reconciler, Fiber Architecture e Rendering Phases em Projetos Reais

Entendendo o Reconciler: O Coração do React O Reconciler é o mecanismo que Re...