Cluster Mode
Escale sua aplicação através de múltiplos núcleos de CPU automaticamente
Cluster Mode 🖥️
AzuraJS fornece suporte integrado ao modo cluster para escalar automaticamente sua aplicação através de todos os núcleos de CPU disponíveis, sem nenhuma configuração manual.
Ativar Cluster Mode ⚡
Simplesmente ative o cluster mode no seu arquivo de configuração e AzuraJS cuida de tudo automaticamente:
import type { ConfigTypes } from "azurajs/config";
const config: ConfigTypes = {
server: {
port: 3000,
cluster: true, // Ativar cluster mode
},
};
export default config;Só isso! Quando cluster: true está definido, AzuraJS automaticamente:
- ✅ Detecta o número de núcleos de CPU disponíveis
- ✅ Cria um processo worker por núcleo de CPU
- ✅ Distribui conexões entre os workers usando round-robin
- ✅ Reinicia automaticamente workers que crasharam
- ✅ Gerencia o desligamento gracioso de todos os workers
- ✅ Gerencia comunicação entre processos
Como Funciona 🔧
Nenhum código de cluster manual é necessário. O código da sua aplicação permanece simples:
import { AzuraClient, applyDecorators } from "azurajs";
import { HomeController } from "./controllers/HomeController";
const app = new AzuraClient();
applyDecorators(app, [HomeController]);
await app.listen();AzuraJS gerencia internamente toda a lógica de cluster baseado na sua configuração. O framework irá:
- Criar um processo primário que gerencia os workers
- Criar processos workers (um por núcleo de CPU)
- Cada worker executa sua aplicação independentemente
- O balanceamento de carga é gerenciado pelo sistema operacional
- Crashes de workers são detectados e novos workers são criados automaticamente
Você não precisa escrever nenhum código de cluster - AzuraJS gerencia tudo nos bastidores.
Quando Usar Cluster Mode 📊
Use cluster mode quando:
- ✅ Executar em ambientes de produção
- ✅ Lidar com alto tráfego e requisições concorrentes
- ✅ Servidor multi-core disponível (2+ cores)
- ✅ Precisa de melhor performance e confiabilidade
- ✅ Quer recuperação automática de processos
Não use cluster mode quando:
- ❌ Desenvolvendo localmente (processo único é mais fácil de debugar)
- ❌ Executando em sistemas single-core (sem benefício)
- ❌ Usando orquestração de containers (Kubernetes, Docker Swarm)
- ❌ Precisa debugar problemas específicos
- ❌ Executando tarefas agendadas ou cron jobs
Configuração Baseada em Ambiente 🌍
Ative cluster mode apenas em produção:
const isProduction = process.env.NODE_ENV === "production";
const config: ConfigTypes = {
environment: isProduction ? "production" : "development",
server: {
port: 3000,
cluster: isProduction, // Cluster apenas em produção
},
};
export default config;Exemplo de Configuração Completa ⚙️
import type { ConfigTypes } from "azurajs/config";
const config: ConfigTypes = {
environment: "production",
server: {
port: process.env.PORT || 3000,
cluster: true, // Ativar cluster mode
ipHost: false,
},
logging: {
enabled: true,
showDetails: true, // Mostra IDs dos processos workers nos logs
},
plugins: {
cors: {
enabled: true,
origins: ["*"],
methods: ["GET", "POST", "PUT", "DELETE"],
},
},
};
export default config;Considerações sobre Estado Compartilhado 💾
Workers executam em processos separados e não compartilham memória. Use armazenamento externo para estado compartilhado:
❌ Não Funciona entre Workers
// Cache em memória não será compartilhado entre workers
const cache = new Map();
@Get("/data")
getData() {
if (cache.has("key")) {
return cache.get("key");
}
// Este cache é por-worker, não compartilhado!
}✅ Use Armazenamento Externo
// Redis para cache compartilhado entre todos os workers
import Redis from "ioredis";
const redis = new Redis();
@Get("/data")
async getData() {
const cached = await redis.get("key");
if (cached) {
return JSON.parse(cached);
}
const data = await fetchData();
await redis.set("key", JSON.stringify(data));
return data;
}Soluções recomendadas para estado compartilhado:
- Redis para cache e sessões
- PostgreSQL/MySQL para dados persistentes
- MongoDB para armazenamento de documentos
- Filas de mensagens externas (RabbitMQ, Kafka)
Benefícios de Performance 📈
Melhorias de performance esperadas com cluster mode:
| Núcleos CPU | Aumento de Throughput |
|---|---|
| 2 cores | ~1.8x |
| 4 cores | ~3.5x |
| 8 cores | ~6-7x |
| 16 cores | ~12-14x |
Ganhos reais dependem de:
- Operações vinculadas a I/O vs CPU
- Sistema operacional
- Arquitetura da aplicação
- Condições de rede
Docker e Kubernetes 🐳
Quando usar orquestração de containers, desative cluster mode e deixe o orquestrador lidar com o escalonamento:
const config: ConfigTypes = {
server: {
cluster: false, // Deixe o orquestrador lidar com o escalonamento
},
};Escale containers ao invés:
services:
api:
image: myapp
deploy:
replicas: 4 # Executar 4 containersapiVersion: apps/v1
kind: Deployment
metadata:
name: azurajs-app
spec:
replicas: 4 # Executar 4 pods
template:
spec:
containers:
- name: app
image: myappMonitoramento e Logs 👀
Com logging.showDetails: true, os logs do AzuraJS mostram informações dos workers:
[Worker 1] Servidor ouvindo na porta 3000 (PID: 12345)
[Worker 2] Servidor ouvindo na porta 3000 (PID: 12346)
[Worker 3] Servidor ouvindo na porta 3000 (PID: 12347)
[Worker 4] Servidor ouvindo na porta 3000 (PID: 12348)Quando um worker crasha e reinicia automaticamente:
[Primary] Worker 2 (PID: 12346) crashou
[Primary] Iniciando novo worker...
[Worker 5] Servidor ouvindo na porta 3000 (PID: 12350)Melhores Práticas ✨
Ative apenas em produção - Desenvolvimento é mais fácil com um único processo
Use armazenamento externo - Redis, bancos de dados, ou filas de mensagens para estado compartilhado
Teste completamente - Comportamento pode diferir entre modo único e cluster
Monitore seus workers - Acompanhe a saúde dos workers e padrões de reinício em produção
Resolução de Problemas 🔍
Workers Continuam Crashando
Verifique os logs da sua aplicação para identificar o erro. Problemas comuns:
- Exceções não capturadas
- Vazamentos de memória
- Problemas de conexão com banco de dados
- Falta de tratamento de erros
Comportamento Inconsistente entre Requisições
Isso geralmente significa que você está usando estado em memória que não é compartilhado. Solução:
- Mova o estado para Redis ou banco de dados
- Garanta que todos os dados sejam armazenados externamente
- Use arquitetura stateless
Erro de Porta Já em Uso
Se você ver este erro, pode estar executando múltiplas instâncias. Verifique:
- Nenhum outro processo na mesma porta
- Apenas uma instância do AzuraJS executando
- Arquivo de configuração está correto
Próximos Passos 📖
Configuração
Aprenda todas as opções de configuração
Performance
Otimize sua aplicação ainda mais
Tratamento de Erros
Trate erros graciosamente
import { AzuraClient } from "azurajs";
import cluster from "cluster";
import { cpus } from "os";
const numCPUs = cpus().length;
if (cluster.isPrimary) {
console.log(`Primary process ${process.pid} is running`);
// Fork workers
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
cluster.on("exit", (worker, code, signal) => {
console.log(`Worker ${worker.process.pid} died`);
// Restart worker
cluster.fork();
});
} else {
// Workers compartilham a porta TCP
const app = new AzuraClient();
// Registrar controllers
applyDecorators(app, [UserController, PostController]);
await app.listen(3000);
console.log(`Worker ${process.pid} started`);
}Configuração Completa 🔧
Cluster com Configuração Avançada
import cluster from "cluster";
import { cpus } from "os";
import { AzuraClient, applyDecorators } from "azurajs";
import { UserController } from "./controllers/UserController";
const numCPUs = cpus().length;
const PORT = process.env.PORT || 3000;
if (cluster.isPrimary) {
console.log(`🚀 Primary ${process.pid} is running`);
console.log(`📊 CPU cores: ${numCPUs}`);
// Fork workers
for (let i = 0; i < numCPUs; i++) {
const worker = cluster.fork();
console.log(`🔷 Worker ${worker.process.pid} started`);
}
// Monitorar workers
cluster.on("online", (worker) => {
console.log(`✅ Worker ${worker.process.pid} is online`);
});
cluster.on("exit", (worker, code, signal) => {
console.log(`❌ Worker ${worker.process.pid} died (${signal || code})`);
// Reiniciar worker
const newWorker = cluster.fork();
console.log(`🔄 New worker ${newWorker.process.pid} started`);
});
// Graceful shutdown
process.on("SIGTERM", () => {
console.log("SIGTERM received, shutting down workers...");
for (const id in cluster.workers) {
cluster.workers[id]?.kill();
}
});
} else {
// Worker process
const app = new AzuraClient({
environment: "production",
server: {
port: PORT
},
logging: {
level: "info"
}
});
// Registrar controllers
applyDecorators(app, [UserController]);
// Iniciar servidor
await app.listen(PORT);
console.log(`Worker ${process.pid} listening on port ${PORT}`);
}Número Ideal de Workers 📊
// Usar todos os núcleos
const numCPUs = cpus().length;
// Usar todos exceto um (deixar um core livre para o sistema)
const numWorkers = Math.max(1, numCPUs - 1);
// Usar metade dos núcleos (para ambientes compartilhados)
const numWorkers = Math.max(1, Math.floor(numCPUs / 2));
// Usar número configurável
const numWorkers = process.env.WORKERS
? parseInt(process.env.WORKERS)
: numCPUs;Comunicação entre Workers 💬
Workers podem se comunicar através do processo primary:
if (cluster.isPrimary) {
const stats = {
requests: 0,
errors: 0
};
cluster.on("message", (worker, message) => {
if (message.type === "request") {
stats.requests++;
} else if (message.type === "error") {
stats.errors++;
}
// Broadcast stats para todos os workers
for (const id in cluster.workers) {
cluster.workers[id]?.send({ type: "stats", data: stats });
}
});
} else {
// Worker
const app = new AzuraClient();
app.use((req, res, next) => {
// Notificar primary sobre requisição
process.send?.({ type: "request" });
next();
});
// Receber mensagens do primary
process.on("message", (message) => {
if (message.type === "stats") {
console.log("Global stats:", message.data);
}
});
}Estado Compartilhado 🔄
Workers não compartilham memória. Use Redis ou banco de dados para estado compartilhado:
import { createClient } from "redis";
const redis = createClient({ url: "redis://localhost:6379" });
await redis.connect();
@Controller("/api")
export class ApiController {
@Get("/counter")
async getCounter() {
// Incrementar contador compartilhado
const count = await redis.incr("global-counter");
return { count, worker: process.pid };
}
@Post("/cache")
async setCache(@Body() data: any) {
// Cache compartilhado entre workers
await redis.setEx(`cache:${data.key}`, 3600, JSON.stringify(data.value));
return { cached: true };
}
}Sessões em Cluster Mode 🔐
Use Redis para armazenar sessões:
import { createClient } from "redis";
const redis = createClient();
await redis.connect();
// Middleware de sessão
async function sessionMiddleware(
req: RequestServer,
res: ResponseServer,
next: () => void
) {
const sessionId = req.cookies.sessionId;
if (sessionId) {
const sessionData = await redis.get(`session:${sessionId}`);
if (sessionData) {
req.session = JSON.parse(sessionData);
}
}
// Salvar sessão ao finalizar
const originalJson = res.json.bind(res);
res.json = async function(data: any) {
if (req.session) {
await redis.setEx(
`session:${sessionId}`,
3600,
JSON.stringify(req.session)
);
}
return originalJson(data);
};
next();
}
app.use(sessionMiddleware);Monitoramento de Workers 📈
Middleware de Métricas
if (cluster.isPrimary) {
const workerStats = new Map();
// Coletar estatísticas
setInterval(() => {
for (const id in cluster.workers) {
const worker = cluster.workers[id];
worker?.send({ type: "request-stats" });
}
}, 10000); // A cada 10 segundos
cluster.on("message", (worker, message) => {
if (message.type === "stats") {
workerStats.set(worker.id, message.data);
// Calcular estatísticas globais
let totalRequests = 0;
let totalErrors = 0;
workerStats.forEach(stats => {
totalRequests += stats.requests;
totalErrors += stats.errors;
});
console.log(`📊 Total: ${totalRequests} requests, ${totalErrors} errors`);
}
});
} else {
let workerRequests = 0;
let workerErrors = 0;
app.use((req, res, next) => {
workerRequests++;
next();
});
process.on("message", (message) => {
if (message.type === "request-stats") {
process.send?.({
type: "stats",
data: {
requests: workerRequests,
errors: workerErrors,
pid: process.pid
}
});
}
});
}Graceful Shutdown 🛑
Implemente shutdown gracioso para não perder requisições:
if (cluster.isPrimary) {
process.on("SIGTERM", async () => {
console.log("Iniciando graceful shutdown...");
// Notificar workers para parar de aceitar novas conexões
for (const id in cluster.workers) {
cluster.workers[id]?.send({ type: "shutdown" });
}
// Aguardar workers finalizarem
await new Promise((resolve) => {
const timeout = setTimeout(resolve, 30000); // Timeout de 30s
let workersAlive = Object.keys(cluster.workers || {}).length;
cluster.on("exit", () => {
workersAlive--;
if (workersAlive === 0) {
clearTimeout(timeout);
resolve(undefined);
}
});
});
console.log("Shutdown completo");
process.exit(0);
});
} else {
let server: any;
const app = new AzuraClient();
server = await app.listen(3000);
process.on("message", (message) => {
if (message.type === "shutdown") {
console.log(`Worker ${process.pid} iniciando shutdown...`);
// Parar de aceitar novas conexões
server.close(() => {
console.log(`Worker ${process.pid} encerrado`);
process.exit(0);
});
// Forçar shutdown após 10 segundos
setTimeout(() => {
console.log(`Worker ${process.pid} forçando shutdown`);
process.exit(1);
}, 10000);
}
});
}Zero-Downtime Deployments 🚀
Reinicie workers um por vez para zero downtime:
if (cluster.isPrimary) {
let workers: any[] = [];
// Fork inicial
for (let i = 0; i < numCPUs; i++) {
workers.push(cluster.fork());
}
// Reload gracioso
process.on("SIGUSR2", () => {
console.log("Iniciando reload gracioso...");
const reloadWorker = (index: number) => {
if (index >= workers.length) {
console.log("Reload completo!");
return;
}
const oldWorker = workers[index];
const newWorker = cluster.fork();
newWorker.once("listening", () => {
// Matar worker antigo
oldWorker.kill();
workers[index] = newWorker;
// Próximo worker após delay
setTimeout(() => reloadWorker(index + 1), 1000);
});
};
reloadWorker(0);
});
}Exemplo Completo 🎯
// server.ts
import cluster from "cluster";
import { cpus } from "os";
import { AzuraClient, applyDecorators } from "azurajs";
import { createClient } from "redis";
import { UserController } from "./controllers/UserController";
const numCPUs = cpus().length;
const PORT = 3000;
if (cluster.isPrimary) {
console.log(`🚀 AzuraJS Cluster Mode`);
console.log(`📊 CPUs: ${numCPUs}`);
console.log(`🔷 Starting ${numCPUs} workers...`);
// Fork workers
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
// Auto-restart
cluster.on("exit", (worker, code, signal) => {
console.log(`❌ Worker ${worker.process.pid} died`);
const newWorker = cluster.fork();
console.log(`✅ Worker ${newWorker.process.pid} started`);
});
// Graceful shutdown
process.on("SIGTERM", () => {
for (const id in cluster.workers) {
cluster.workers[id]?.send({ type: "shutdown" });
}
});
} else {
// Worker
const redis = createClient();
await redis.connect();
const app = new AzuraClient({
environment: "production",
server: { port: PORT },
plugins: {
cors: { enabled: true, origin: "*" },
rateLimit: { enabled: true, windowMs: 60000, max: 100 }
}
});
// Middleware de sessão compartilhada
app.use(async (req, res, next) => {
const sessionId = req.cookies.sessionId;
if (sessionId) {
const data = await redis.get(`session:${sessionId}`);
req.session = data ? JSON.parse(data) : {};
}
next();
});
// Registrar controllers
applyDecorators(app, [UserController]);
// Iniciar servidor
const server = await app.listen(PORT);
console.log(`✅ Worker ${process.pid} listening on port ${PORT}`);
// Graceful shutdown
process.on("message", (msg) => {
if (msg.type === "shutdown") {
server.close(() => process.exit(0));
setTimeout(() => process.exit(1), 10000);
}
});
}Melhores Práticas ✨
Use Redis para estado compartilhado: Workers não compartilham memória
Implemente auto-restart: Workers podem crashar, sempre reinicie automaticamente
Graceful shutdown: Aguarde requisições finalizarem antes de matar workers
Cuidado com memória: Cada worker consome memória, monitore uso total
