Что такое cluster?
Комментарии (2)
Ответ сгенерирован нейросетью и может содержать ошибки
Что такое cluster в Node.js?
Cluster — это встроенный модуль Node.js, который позволяет использовать многоядерные процессоры, запуская несколько worker процессов параллельно. Это критически важно для production приложений.
Проблема: Node.js однопоточный
Дефолтно Node.js работает в одном потоке и может использовать только одно ядро процессора.
// app.js
const http = require('http');
const server = http.createServer((req, res) => {
// Тяжелая операция блокирует весь процесс
let sum = 0;
for (let i = 0; i < 1000000000; i++) {
sum += i;
}
res.writeHead(200);
res.end('OK');
});
server.listen(3000);
// Используем только 1 ядро из 8 доступных
На 8-ядерном сервере: только одно ядро работает, остальные 7 простаивают.
Решение: Cluster Module
const cluster = require('cluster');
const http = require('http');
const os = require('os');
const numCPUs = os.cpus().length;
if (cluster.isMaster) {
// Главный процесс
console.log(`Master ${process.pid} is running`);
// Запускаем worker для каждого ядра
for (let i = 0; i < numCPUs; i++) {
cluster.fork(); // Создаем worker процесс
}
// Когда worker упадет — перезапускаем его
cluster.on('exit', (worker, code, signal) => {
console.log(`Worker ${worker.process.pid} died`);
cluster.fork(); // Создаем новый worker
});
} else {
// Worker процесс
console.log(`Worker ${process.pid} started`);
const server = http.createServer((req, res) => {
res.writeHead(200);
res.end('Hello World');
});
server.listen(3000);
}
Результат: 8 worker процессов работают параллельно, используя все 8 ядер.
Как это работает
1. Master процесс
- Запускает worker процессы
- Слушает порт (если конфигурировать sticky sessions)
- Мониторит health workers
- Перезапускает упавших workers
2. Worker процессы
- Выполняют фактическую работу
- Обрабатывают requests
- Могут упасть независимо
- Переживают друг друга
Полный пример с обработкой ошибок
const cluster = require('cluster');
const http = require('http');
const os = require('os');
const numCPUs = os.cpus().length;
const PORT = 3000;
if (cluster.isMaster) {
console.log(`Master ${process.pid} is running`);
// Запускаем workers
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
// Обработка exit события
cluster.on('exit', (worker, code, signal) => {
if (signal) {
console.log(`Worker ${worker.process.pid} was killed by signal: ${signal}`);
} else if (code !== 0) {
console.log(`Worker ${worker.process.pid} exited with error code: ${code}`);
} else {
console.log(`Worker ${worker.process.pid} exited successfully`);
}
// Перезапускаем worker (если это не graceful shutdown)
if (code !== 0 && !worker.exitedAfterDisconnect) {
console.log('Starting a new worker...');
cluster.fork();
}
});
// Graceful shutdown
process.on('SIGTERM', () => {
console.log('Master shutting down gracefully');
for (const id in cluster.workers) {
cluster.workers[id].disconnect();
}
process.exit(0);
});
} else {
// Worker процесс
const server = http.createServer((req, res) => {
// Имитируем обработку
console.log(`[Worker ${process.pid}] Handling request from ${req.url}`);
// Случайно упадем (для тестирования)
if (Math.random() < 0.01) {
throw new Error('Random error in worker');
}
res.writeHead(200);
res.end(`Processed by worker ${process.pid}`);
});
server.listen(PORT);
console.log(`Worker ${process.pid} listening on port ${PORT}`);
// Обработка необработанных ошибок
process.on('uncaughtException', (error) => {
console.error(`[Worker ${process.pid}] Uncaught Exception:`, error);
process.exit(1);
});
}
Распределение нагрузки
Задача: несколько requests обрабатываются разными workers
Request 1 -> Master -> Worker 1 (CPU core 1)
Request 2 -> Master -> Worker 2 (CPU core 2)
Request 3 -> Master -> Worker 3 (CPU core 3)
Request 4 -> Master -> Worker 4 (CPU core 4)
Request 5 -> Master -> Worker 1 (CPU core 1) // Возвращаемся в очередь
Master автоматически распределяет requests между workers через round-robin алгоритм.
Совместное использование портов
Все workers слушают на одном порту (3000), благодаря кластеру:
// Все 8 workers слушают на порту 3000
server.listen(3000); // В worker процессе
// Клиент -> Port 3000 -> Master распределяет -> Worker 1,2,3...
Мастер используется для:
- Получение connection
- Передача в worker через IPC (Inter-Process Communication)
Сравнение: без cluster vs с cluster
Без cluster (1 worker):
Request 1: 100ms
Request 2: 100ms (ждет)
Request 3: 100ms (ждет)
Total: 300ms
С cluster (4 workers):
Request 1 -> Worker 1: 100ms
Request 2 -> Worker 2: 100ms
Request 3 -> Worker 3: 100ms
Total: ~100ms (параллельно)
Сложности с cluster
Проблема 1: Shared state
// ❌ ПЛОХО: state не синхронизируется между workers
let userCount = 0;
app.post('/users', (req, res) => {
userCount++; // Worker 1: userCount = 1
// Worker 2: userCount = 1 (не видит изменение Worker 1)
res.json({ totalUsers: userCount });
});
// ✅ ПРАВИЛЬНО: используем Redis или БД
redis.incr('userCount'); // Синхронизируется для всех workers
Проблема 2: Session хранение
// ❌ ПЛОХО: session только в памяти worker
app.post('/login', (req, res) => {
req.session.userId = 123; // Сохранено в Worker 1
res.json({ ok: true });
});
app.get('/profile', (req, res) => {
// Request может пойти в Worker 2
// req.session.userId будет undefined
});
// ✅ ПРАВИЛЬНО: store session в Redis
const RedisStore = require('connect-redis')(session);
app.use(session({
store: new RedisStore(),
// ...
}));
Альтернативы cluster
1. PM2 (production manager)
# Запускаем приложение в cluster mode
pm2 start app.js -i max
# Автоматически создает worker для каждого ядра
# Автоматически перезапускает упавших workers
# Имеет веб-панель мониторинга
2. Docker + Kubernetes
# Запускаем несколько контейнеров Node.js
# Каждый контейнер — это worker
# Load balancer (nginx, Kubernetes service) распределяет нагрузку
3. Systemd + multiple processes
# Используем systemd для запуска нескольких процессов
# Каждый на разном порту
# nginx перед ними как load balancer
Лучше практики
1. Используй PM2 или Docker, не писал cluster вручную
# Намного проще
pm2 start ecosystem.config.js
2. Если все же пишешь cluster:
const numWorkers = process.env.NODE_CLUSTER_SIZE || os.cpus().length;
// Позволяет переопределить количество workers через env
3. Мониторь процессы
// Логируй worker deaths
cluster.on('exit', (worker) => {
logger.error(`Worker ${worker.process.pid} died`);
metrics.incrementWorkerDeaths();
});
4. Graceful shutdown
process.on('SIGTERM', () => {
// Закрываем новые connections
server.close();
// Даем текущим requests время на завершение
setTimeout(() => process.exit(0), 30000);
});
Практический пример из production
const cluster = require('cluster');
const express = require('express');
const http = require('http');
const os = require('os');
if (cluster.isMaster) {
const numWorkers = parseInt(process.env.WORKERS) || os.cpus().length;
console.log(`Starting ${numWorkers} workers`);
for (let i = 0; i < numWorkers; i++) {
cluster.fork();
}
cluster.on('exit', (worker, code, signal) => {
console.error(`Worker ${worker.process.pid} died (${signal || code}). restarting...`);
cluster.fork();
});
// Graceful reload
process.on('SIGUSR2', () => {
console.log('Reloading workers');
for (const id in cluster.workers) {
cluster.workers[id].kill();
}
});
} else {
const app = express();
app.get('/health', (req, res) => {
res.json({ status: 'ok', pid: process.pid });
});
app.get('/api/data', async (req, res) => {
// Обработка requests
res.json({ data: 'from worker ' + process.pid });
});
const server = http.createServer(app);
server.listen(3000);
console.log(`Worker ${process.pid} listening on port 3000`);
// Graceful shutdown
process.on('SIGTERM', () => {
server.close(() => process.exit(0));
});
}
Итог
Cluster — это:- Использование всех ядер процессора
- Автоматический restart упавших workers
- Простое распределение нагрузки
- Для production приложений ОБЯЗАТЕЛЬНО
Но в реальности используй PM2 — это проще и надежнее.