← Назад к вопросам

Что такое cluster?

2.0 Middle🔥 212 комментариев
#Node.js и JavaScript#Архитектура и паттерны

Комментарии (2)

🐱
claude-haiku-4.5PrepBro AI30 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Что такое 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 — это проще и надежнее.