Clustering and Load Balancing
Scaling Node.js applications
Challenge
Section titled “Challenge”Utilize multi-core processors, distribute load across workers, and handle graceful shutdown.
Explanation
Section titled “Explanation”The cluster module allows Node.js to spawn worker processes and distribute traffic among them for better performance.
Basic Clustering
Section titled “Basic Clustering”const cluster = require('cluster');const http = require('http');const os = require('os');
if (cluster.isMaster) { const numWorkers = os.cpus().length;
console.log(`Master process ${process.pid} is running`);
// Fork workers for (let i = 0; i < numWorkers; i++) { cluster.fork(); }
// Handle worker exit cluster.on('exit', (worker, code, signal) => { console.log(`Worker ${worker.process.pid} died`); cluster.fork(); // Restart worker });} else { // Worker process const server = http.createServer((req, res) => { res.writeHead(200); res.end(`Handled by worker ${process.pid}`); });
server.listen(3000); console.log(`Worker ${process.pid} started`);}Advanced Worker Management
Section titled “Advanced Worker Management”const cluster = require('cluster');const os = require('os');
if (cluster.isMaster) { const numWorkers = os.cpus().length; const workers = [];
// Fork workers for (let i = 0; i < numWorkers; i++) { const worker = cluster.fork(); workers.push(worker); }
// Graceful shutdown process.on('SIGTERM', () => { console.log('Master shutting down gracefully');
workers.forEach(worker => { worker.kill(); });
// Wait for workers to exit cluster.on('exit', (worker) => { console.log(`Worker ${worker.process.pid} exited`); }); });
// Monitor metrics setInterval(() => { Object.values(cluster.workers).forEach(worker => { console.log(`Worker ${worker.process.pid} active`); }); }, 30000);} else { // Worker code require('./app').start();}Zero-Downtime Deployment
Section titled “Zero-Downtime Deployment”if (cluster.isMaster) { let currentWorkers = [];
function spawnNewWorkers() { for (let i = 0; i < numWorkers; i++) { const worker = cluster.fork(); currentWorkers.push(worker); } }
// Listen for reload signal process.on('SIGUSR2', async () => { console.log('Reloading workers...');
const newWorkers = [];
// Spawn new workers for (let i = 0; i < numWorkers; i++) { newWorkers.push(cluster.fork()); }
// Wait for new workers to be ready await new Promise(resolve => setTimeout(resolve, 2000));
// Kill old workers currentWorkers.forEach(worker => { worker.kill(); });
currentWorkers = newWorkers; console.log('Workers reloaded'); });} else { // Worker with graceful shutdown let server;
process.on('SIGTERM', () => { if (server) { server.close(() => { process.exit(0); });
// Force exit after timeout setTimeout(() => { process.exit(1); }, 30000); } });
server = http.createServer(handleRequest); server.listen(3000);}Load Balancing Strategies
Section titled “Load Balancing Strategies”// Round-robin (default) - distributes fairly// Sticky session - keep related requests on same worker
if (cluster.isMaster) { // Session affinity const workers = []; for (let i = 0; i < numWorkers; i++) { workers.push(cluster.fork()); }
const server = http.createServer((req, res) => { // Simple hash-based distribution const sessionId = req.headers['x-session-id']; const workerId = hashFunc(sessionId) % workers.length;
// Forward to worker workers[workerId].send({ req, res }); });
server.listen(3000);}PM2 for Process Management
Section titled “PM2 for Process Management”module.exports = { apps: [{ name: 'api', script: './app.js', instances: 'max', // Use all CPU cores exec_mode: 'cluster', env: { NODE_ENV: 'development' }, env_production: { NODE_ENV: 'production' }, error_file: './logs/error.log', out_file: './logs/out.log', merge_logs: true, max_memory_restart: '1G', max_restarts: 10, min_uptime: '10s' }]};# Commandspm2 start ecosystem.config.jspm2 start ecosystem.config.js --env productionpm2 reload api # Graceful reloadpm2 restart api # Restart allpm2 kill api # Shutdownpm2 logs api # Show logspm2 monit # MonitorSticky Sessions with Redis
Section titled “Sticky Sessions with Redis”const session = require('express-session');const RedisStore = require('connect-redis').default;const { createClient } = require('redis');
// Redis clientconst redisClient = createClient();
app.use(session({ store: new RedisStore({ client: redisClient }), secret: 'your-secret-key', resave: false, saveUninitialized: false, cookie: { secure: true, httpOnly: true, maxAge: 1000 * 60 * 60 * 24 }}));
// Sessions persisted in Redis, accessible to all workersapp.get('/profile', (req, res) => { // Works on any worker console.log(req.session.userId);});Health Checks
Section titled “Health Checks”// Master monitors worker healthif (cluster.isMaster) { const workers = [];
for (let i = 0; i < numWorkers; i++) { workers.push(cluster.fork()); }
// Periodic health check setInterval(() => { workers.forEach((worker, index) => { worker.send({ cmd: 'health-check' });
const timeout = setTimeout(() => { console.log(`Worker ${worker.process.pid} unresponsive`); worker.kill(); workers[index] = cluster.fork(); }, 5000);
worker.on('message', (msg) => { if (msg.cmd === 'health-check-response') { clearTimeout(timeout); } }); }); }, 10000);}
// Worker responds to health checksif (cluster.isWorker) { process.on('message', (msg) => { if (msg.cmd === 'health-check') { process.send({ cmd: 'health-check-response' }); } });}