Clustering e Performance
Aula 6 de 7
Clustering
Node.js é single-thread, mas podemos usar múltiplos processos com o módulo cluster para aproveitar todos os CPUs.
Cluster Module
const cluster = require('cluster');
const http = require('http');
const os = require('os');
const numCPUs = os.cpus().length;
if (cluster.isPrimary) {
console.log(`Primary ${process.pid} rodando`);
// Fork workers
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
cluster.on('exit', (worker, code, signal) => {
console.log(`Worker ${worker.process.pid} morreu. Reiniciando...`);
cluster.fork(); // Auto-healing
});
cluster.on('message', (worker, msg) => {
console.log(`Mensagem do worker ${worker.id}:`, msg);
});
} else {
// Workers compartilham a porta TCP
http.createServer((req, res) => {
// Enviar mensagem ao primary
process.send({ type: 'request', pid: process.pid });
res.writeHead(200);
res.end(`Worker ${process.pid} processou a requisição\n`);
}).listen(3000);
console.log(`Worker ${process.pid} iniciado`);
}
PM2 (Production Process Manager)
npm install -g pm2
# Iniciar em modo cluster
pm2 start src/server.js -i max
pm2 start src/server.js -i 4 # 4 instâncias
# Gerenciar
pm2 list
pm2 status
pm2 logs
pm2 monit
pm2 restart all
pm2 reload all # zero downtime
pm2 stop app
pm2 delete app
// ecosystem.config.cjs
module.exports = {
apps: [{
name: 'api',
script: 'src/server.js',
instances: 'max',
exec_mode: 'cluster',
env: {
NODE_ENV: 'production',
PORT: 3000
},
env_production: {
NODE_ENV: 'production'
},
max_memory_restart: '1G',
error_file: './logs/err.log',
out_file: './logs/out.log',
merge_logs: true,
log_date_format: 'YYYY-MM-DD HH:mm:ss',
watch: false,
max_restarts: 10,
restart_delay: 4000
}]
};
Load Balancing
O módulo cluster distribui requisições entre workers usando round-robin (no Linux) ou delegando ao SO (no Windows).
// Estratégia round-robin (forçar)
cluster.schedulingPolicy = cluster.SCHED_RR;
Performance Profiling
Node.js --prof
# Gerar perfil
node --prof src/server.js
# Analisar
node --prof-process isolate-*.log > profiled.txt
0x Clinic.js
npm install -g 0x
# Flamegraph
0x src/server.js
# Doctor (análise de performance)
npx clinic doctor -- node src/server.js
# Bubbleprof (latência)
npx clinic bubbleprof -- node src/server.js
# Flame (flamegraph detalhado)
npx clinic flame -- node src/server.js
Memory Leaks
// Detectar vazamentos com heap snapshot
const v8 = require('v8');
// Criar snapshot
const snapshot = v8.getHeapSnapshot();
snapshot.pipe(require('fs').createWriteStream('heap.heapsnapshot'));
// Monitorar memória
setInterval(() => {
const usage = process.memoryUsage();
console.log({
rss: `${(usage.rss / 1024 / 1024).toFixed(2)} MB`,
heapTotal: `${(usage.heapTotal / 1024 / 1024).toFixed(2)} MB`,
heapUsed: `${(usage.heapUsed / 1024 / 1024).toFixed(2)} MB`,
external: `${(usage.external / 1024 / 1024).toFixed(2)} MB`
});
}, 5000);
// Causa comum de leak: listeners não removidos
process.on('warning', (warning) => {
if (warning.name === 'MaxListenersExceededWarning') {
console.error('Possível leak de memória!');
}
});
Compressão
npm install compression
const compression = require('compression');
const express = require('express');
const app = express();
app.use(compression({
level: 6, // zlib level (0-9)
threshold: 1024, // mínimo 1KB para comprimir
filter: (req, res) => {
if (req.headers['x-no-compression']) return false;
return compression.filter(req, res);
}
}));
Connection Pooling
PostgreSQL (pg-pool)
const { Pool } = require('pg');
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
max: 20, // máximo de conexões no pool
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 2000
});
async function query(text, params) {
const start = Date.now();
const res = await pool.query(text, params);
const duration = Date.now() - start;
console.log('Query executada', { text, duration, rows: res.rowCount });
return res;
}
Prisma Connection Pool
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient({
datasources: {
db: {
url: process.env.DATABASE_URL
}
},
log: ['query', 'info', 'warn', 'error'],
// Config do pool (configurado via DATABASE_URL)
// Postgres: ?connection_limit=20&pool_timeout=10
});
Redis Caching
npm install ioredis
const Redis = require('ioredis');
const redis = new Redis({
host: process.env.REDIS_HOST || 'localhost',
port: 6379,
maxRetriesPerRequest: 3,
enableReadyCheck: true
});
redis.on('connect', () => console.log('Redis conectado'));
redis.on('error', (err) => console.error('Redis erro:', err));
// Cache middleware
function cacheMiddleware(ttl = 60) {
return async (req, res, next) => {
const key = `cache:${req.originalUrl}`;
try {
const cached = await redis.get(key);
if (cached) {
console.log('Cache HIT:', key);
return res.json(JSON.parse(cached));
}
console.log('Cache MISS:', key);
const originalJson = res.json.bind(res);
res.json = (body) => {
redis.setex(key, ttl, JSON.stringify(body));
originalJson(body);
};
next();
} catch (err) {
next(err);
}
};
}
// Uso
app.get('/users', cacheMiddleware(30), async (req, res) => {
const users = await prisma.user.findMany();
res.json(users);
});
// Invalidação de cache
async function invalidateCache(pattern) {
const keys = await redis.keys(pattern);
if (keys.length > 0) {
await redis.del(keys);
console.log(`Cache invalidado: ${pattern} (${keys.length} chaves)`);
}
}
Lab: Exercício - API Otimizada com Cluster e Cache
// src/server.js
const cluster = require('cluster');
const os = require('os');
if (cluster.isPrimary) {
const numCPUs = os.cpus().length;
console.log(`Primary iniciando ${numCPUs} workers`);
for (let i = 0; i < numCPUs; i++) cluster.fork();
cluster.on('exit', (worker) => {
console.log(`Worker ${worker.process.pid} reiniciado`);
cluster.fork();
});
} else {
require('./app');
}
// src/app.js
const express = require('express');
const compression = require('compression');
const Redis = require('ioredis');
const { PrismaClient } = require('@prisma/client');
const app = express();
const redis = new Redis({ host: process.env.REDIS_HOST });
const prisma = new PrismaClient();
app.use(compression());
app.use(express.json());
const cache = (ttl = 60) => async (req, res, next) => {
const key = `cache:${req.originalUrl}`;
const cached = await redis.get(key);
if (cached) return res.json(JSON.parse(cached));
const send = res.json.bind(res);
res.json = (body) => {
redis.setex(key, ttl, JSON.stringify(body));
send(body);
};
next();
};
app.get('/users', cache(30), async (req, res) => {
const users = await prisma.user.findMany();
res.json(users);
});
app.listen(process.env.PORT || 3000);
# Iniciar com PM2
pm2 start ecosystem.config.cjs
# Monitorar
pm2 monit
pm2 logs api --lines 100
# Testar carga
npx autocannon -c 100 -d 10 http://localhost:3000/users
Use PM2 em modo cluster para multi-threading real. Combine Redis cache com compression para reduzir latência. Clinic.js ajuda a identificar gargalos com flamegraphs. Memory leaks comuns: listeners não removidos, referências em closures, e cache sem expiração.