AzuraJS Logo
AzuraJSFramework
v2.2 Beta

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:

azura.config.ts
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:

index.ts
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á:

  1. Criar um processo primário que gerencia os workers
  2. Criar processos workers (um por núcleo de CPU)
  3. Cada worker executa sua aplicação independentemente
  4. O balanceamento de carga é gerenciado pelo sistema operacional
  5. 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:

azura.config.ts
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 ⚙️

azura.config.ts
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 CPUAumento 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:

azura.config.ts
const config: ConfigTypes = {
  server: {
    cluster: false,  // Deixe o orquestrador lidar com o escalonamento
  },
};

Escale containers ao invés:

docker-compose.yml
services:
  api:
    image: myapp
    deploy:
      replicas: 4  # Executar 4 containers
kubernetes-deployment.yml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: azurajs-app
spec:
  replicas: 4  # Executar 4 pods
  template:
    spec:
      containers:
      - name: app
        image: myapp

Monitoramento 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 📖

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

Próximos Passos 📖

On this page