Урок по треку Node.js и бэкенд 7 мин

Кластер, воркеры и масштабирование Node.js

cluster, worker_threads, горизонтальное масштабирование, stateless-сервисы и балансировка.

Один процесс Node выполняет JavaScript на одном ядре. Сервер с 8 ядрами при одном процессе использует для JS одно. Ещё одна проблема: тяжёлое вычисление блокирует event loop для всех. Решения разные, и их часто путают.

Три инструмента

ЗадачаИнструментЧто это
Загрузить все ядра одной машины под HTTPcluster или несколько процессовотдельные процессы с общим портом
Вынести CPU-работу из event loopworker_threadsпотоки внутри одного процесса
Выдержать нагрузку больше одной машиныгоризонтальное масштабированиемного экземпляров за балансировщиком

cluster

import cluster from 'node:cluster';
import { availableParallelism } from 'node:os';
import http from 'node:http';

if (cluster.isPrimary) {
  for (let i = 0; i < availableParallelism(); i++) cluster.fork();
  cluster.on('exit', () => cluster.fork()); // упал: поднимаем новый
} else {
  http.createServer(handler).listen(3000);
}

Главный процесс запускает воркеры, все слушают один порт. Входящие соединения главный процесс раздаёт по кругу (round-robin, по умолчанию везде, кроме Windows). Каждый воркер это отдельный процесс со своей памятью: переменная-счётчик или кеш в памяти у каждого свой.

PM2 в режиме cluster делает то же самое и добавляет перезапуски и деплой без простоя. В Kubernetes и Docker обычно не используют cluster вовсе: запускают один процесс на контейнер и масштабируют числом реплик, а балансирует сама платформа.

worker_threads

import { Worker } from 'node:worker_threads';

function hashInWorker(data) {
  return new Promise((resolve, reject) => {
    const w = new Worker(new URL('./hash-worker.js', import.meta.url), { workerData: data });
    w.once('message', resolve);
    w.once('error', reject);
  });
}

Воркер это отдельный поток со своим экземпляром V8, своим event loop и своей кучей. Данные передаются сообщениями (structured clone, то есть копией). Без копирования можно передать ArrayBuffer через transfer list или разделить SharedArrayBuffer с синхронизацией через Atomics.

Когда нужны воркеры: ресайз картинок, парсинг огромных файлов, тяжёлая криптография, сжатие, вычисления. Когда не нужны: ввод-вывод. HTTP-запросы и запросы в базу и так не блокируют цикл, воркер их не ускорит.

Создание воркера стоит миллисекунды и память, поэтому в проде держат пул воркеров (например, библиотека piscina), а не создают по одному на задачу.

Горизонтальное масштабирование

Чтобы добавлять экземпляры без проблем, сервис должен быть stateless:

  • сессии в Redis или в подписанном токене, а не в памяти процесса;
  • файлы в объектном хранилище (S3), а не на локальном диске;
  • кеш общий (Redis) или такой, что расхождение между экземплярами не страшно;
  • фоновые задачи в очереди, а не в setInterval каждого экземпляра (иначе крон выполнится N раз).

WebSocket-соединения живут на конкретном экземпляре. Чтобы сообщение дошло до пользователя, подключённого к другому экземпляру, используют общую шину (Redis pub/sub). Sticky sessions нужны, если протокол делает несколько HTTP-запросов в рамках одной сессии, например long polling в Socket.IO.

Память

Размер кучи V8 ограничен. Для больших процессов его поднимают флагом --max-old-space-size=4096 (в МБ). В контейнере значение ставят заметно ниже лимита памяти контейнера: кроме кучи процессу нужны буферы, стек и нативная память.

Частые ошибки

  • Ждать, что worker_threads ускорят запросы к базе.
  • Хранить сессии в памяти процесса и потом включить cluster: пользователей начнёт «разлогинивать».
  • Создавать новый воркер на каждый запрос.
  • Запускать cluster внутри контейнера и одновременно масштабировать репликами: сложно ограничить ресурсы.

Что спросят на собесе

  • Чем cluster отличается от worker_threads.
  • Как Node использует несколько ядер.
  • Что значит stateless-сервис и зачем он нужен.
  • Как масштабировать WebSocket на несколько экземпляров.
  • Когда воркеры не помогут.

Закрепите за 5 минут

После урока в тренажёре 3-5 проверочных вопросов, дальше модуль уходит в повторения.