Cluster в Node.js: используем все ядра процессора

Содержание

О проверке кода. Примеры не запускались в этой сессии (по договорённости — фокус на контенте, а не прогонах). Говорим прямо, а не ставим бейдж «проверено».

Современный сервер — это 4, 8, 16 ядер, но обычный процесс Node.js использует лишь одно. Модуль cluster заставляет приложение работать на всех ядрах. Разберём, как и когда это нужно.

Проблема: одно ядро из многих

Event loop Node — однопоточный, а значит, один процесс загружает только одно ядро процессора. На 8-ядерном сервере это буквально означает, что 7 ядер простаивают, пока одно тащит всю нагрузку. Для сервера под трафиком — расточительство.

Cluster решает это, запуская несколько процессов-воркеров — по числу ядер — и распределяя входящие соединения между ними. Восемь воркеров на восьми ядрах обслужат кратно больше запросов, чем один.

Базовый пример

const cluster = require('node:cluster');
const os = require('node:os');
const http = require('node:http');

if (cluster.isPrimary) {
  // главный процесс: порождает воркеры по числу ядер
  const cpus = os.cpus().length;
  console.log(`Запускаю ${cpus} воркеров`);

  for (let i = 0; i < cpus; i++) {
    cluster.fork();                     // создать воркер
  }

  cluster.on('exit', (worker) => {
    console.log(`Воркер ${worker.process.pid} упал, перезапускаю`);
    cluster.fork();                     // заменить упавший
  });
} else {
  // воркеры: каждый — обычный сервер на том же порту
  http.createServer((req, res) => {
    res.end(`Ответил воркер ${process.pid}`);
  }).listen(3000);
}

Главный процесс (isPrimary) порождает воркеры через fork, а воркеры запускают обычный сервер. Все воркеры слушают один порт — cluster сам распределяет между ними входящие соединения. Обработчик exit перезапускает упавший воркер, что даёт отказоустойчивость: падение одного не роняет сервис.

Как cluster распределяет запросы
  1. 1 Главный процесс (primary)Порождает воркеры по числу ядер
  2. 2 N воркеров на N ядрахКаждый — копия сервера на том же порту
  3. 3 Запрос приходитГлавный распределяет его свободному воркеру
  4. 4 Воркер упал → перезапускСервис продолжает работать

Сколько воркеров

const os = require('node:os');
os.cpus().length;   // число логических ядер — типичное число воркеров

Обычно запускают воркеров по числу ядер процессора. Больше воркеров, чем ядер, не ускоряет — только добавляет накладные расходы на переключение процессов. Точное оптимальное число подбирают нагрузочным тестированием под конкретное приложение, но os.cpus().length — разумная отправная точка.

Ловушка: воркеры не делят память

// ❌ так НЕЛЬЗЯ при cluster — состояние в памяти процесса
let sessions = {};
app.post('/login', (req, res) => {
  sessions[userId] = data;   // увидит ТОЛЬКО этот воркер!
});
// следующий запрос того же юзера может попасть на ДРУГОЙ воркер — сессии там нет

Каждый воркер — отдельный процесс со своей памятью. Состояние, положенное в память одного воркера, не видят остальные. А запросы одного пользователя распределяются между разными воркерами — значит, хранить в памяти процесса нельзя:

  • сессии, кэш, счётчики — выносят в Redis или базу данных, общие для всех воркеров;
  • загруженные в память данные дублируются в каждом воркере — учитывайте расход памяти.

Это фундаментальное следствие процессной модели, и на нём спотыкаются, добавляя cluster к приложению, которое хранило состояние в памяти.

Cluster против worker threads

Повторим ключевое различие (подробнее — в статье про потоки):

  • cluster — несколько процессов для распределения запросов по ядрам. Про масштаб сервера под трафиком;
  • worker threadsпотоки внутри одного процесса для параллельных вычислений. Про ускорение тяжёлой CPU-задачи.

Грубо: cluster отвечает на «много клиентов», threads — на «тяжёлая математика». Инструменты не взаимозаменяемы и иногда работают вместе.

В продакшене: PM2

Писать cluster руками поучительно, но в реальном проде чаще берут менеджер процессов PM2:

pm2 start app.js -i max    # -i max = воркеры по числу ядер, автоматически

PM2 сам поднимает воркеры по числу ядер, перезапускает упавшие, следит за нагрузкой и памятью, даёт бесперебойную перезагрузку при деплое и мониторинг. Он избавляет от ручного кода cluster и добавляет то, что вы всё равно написали бы сами. Правило то же, что и везде: готовый инструмент под задачу, если он делает нужное надёжнее. Понимать cluster полезно, чтобы знать, что PM2 делает под капотом.

Когда это нужно

  • сервер под заметным трафиком — задействовать все ядра;
  • отказоустойчивость — падение воркера не роняет сервис;
  • бесперебойный деплой — перезапуск воркеров по одному.

Когда не нужно: маленький сервис, скрипт, приложение с малой нагрузкой — там один процесс проще и достаточно. Cluster (или PM2) добавляют, когда одного ядра перестаёт хватать.


Смежные темы: worker threads — потоки против процессов; event loop — почему одно ядро; graceful shutdown — корректный перезапуск; Redis для общего состояния. Полный список — в уроках Node.js.

Частые вопросы

Зачем нужен модуль cluster?
Один процесс Node использует лишь одно ядро процессора. Cluster запускает несколько процессов-воркеров, по числу ядер, и распределяет входящие запросы между ними. Это позволяет сервору обслуживать больше запросов, задействуя весь процессор.
Чем cluster отличается от worker threads?
Cluster создаёт отдельные процессы с собственной памятью для распределения запросов между ядрами. Worker threads — потоки внутри одного процесса для параллельных вычислений. Cluster масштабирует сервер, threads ускоряют тяжёлую задачу.
Сколько воркеров запускать?
Обычно по числу ядер процессора, которое даёт os.cpus().length. Больше воркеров, чем ядер, не ускоряет, а добавляет накладные расходы на переключение. Точное число подбирают нагрузочным тестированием под конкретное приложение.
Делят ли воркеры общую память?
Нет, каждый воркер — отдельный процесс со своей памятью. Поэтому состояние вроде сессий или кэша нельзя держать в памяти процесса — его не увидят другие воркеры. Общее состояние выносят в Redis или базу данных.
Нужно ли писать cluster вручную?
В продакшене чаще берут менеджер процессов PM2, который сам поднимает воркеры по числу ядер, перезапускает упавшие и следит за нагрузкой. Ручной cluster полезно понимать, но PM2 избавляет от рутины и добавляет мониторинг.