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 перезапускает упавший воркер, что даёт отказоустойчивость: падение одного не роняет сервис.
- 1 Главный процесс (primary)Порождает воркеры по числу ядер
- 2 N воркеров на N ядрахКаждый — копия сервера на том же порту
- 3 Запрос приходитГлавный распределяет его свободному воркеру
- 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.