Node.js Internals

Cluster: Масштабирование Node.js

Твой Node.js сервер работает на одном ядре, а остальные 7 ядер твоего MacBook Pro просто смотрят. Пора это исправить.

  • **Netflix:** Используют PM2 cluster mode для горизонтального масштабирования Node.js API. Каждый сервер запускает 8-16 workers (по числу ядер).
  • **PayPal:** Мигрировали с Java на Node.js + Cluster и получили 2x прирост производительности при том же железе.
  • **LinkedIn:** Используют Worker Threads для CPU-интенсивных задач (парсинг резюме, обработка изображений) внутри основного сервера.

Intro

Node.js работает в одном потоке. Это значит, что на 8-ядерном процессоре используется только 1 ядро, а остальные 7 простаивают.

**Cluster** - это встроенный модуль Node.js, который позволяет создавать несколько рабочих процессов (workers), каждый из которых работает на отдельном ядре CPU. Все они слушают один и тот же порт, а операционная система распределяет входящие соединения между ними.

**Ключевая идея:** Cluster создаёт несколько копий приложения (по одной на каждое ядро CPU), и master-процесс распределяет нагрузку между ними. Это горизонтальное масштабирование на уровне одной машины.

Классическая аналогия: касса в магазине. Если работает один кассир - образуется очередь. Если открыть 4 кассы (по числу ядер) - пропускная способность вырастает в 4 раза.

При запуске этого кода создаётся 1 master-процесс и N workers (где N = количество ядер). Каждый worker - это отдельный процесс Node.js с собственной памятью и Event Loop.

Что произойдёт, если на 4-ядерном CPU запустить cluster.fork() 8 раз?

Single Thread Problem

Node.js отлично справляется с I/O-bound задачами (сеть, файлы, БД), но **CPU-bound** задачи (хеширование, шифрование, вычисления) блокируют Event Loop.

Пример: сервер хеширует пароли с bcrypt. Каждый запрос занимает 100ms CPU. Если в секунду приходит 20 запросов - Event Loop заблокирован на 2 секунды. Все остальные запросы встают в очередь.

**Проблема:** Один медленный запрос блокирует весь сервер. Latency растёт линейно с нагрузкой: 1 запрос = 100ms, 10 запросов = 1 секунда, 100 запросов = 10 секунд.

Видишь? Request 2 и 3 ждут, пока Request 1 завершит CPU-работу. Event Loop заблокирован. Это называется **head-of-line blocking**.

**Решения:** 1. **Cluster** - запустить несколько процессов Node.js (по одному на ядро) 2. **Worker Threads** - вынести CPU-задачу в отдельный поток 3. **Offload** - отдать работу в отдельный сервис (микросервисы)

Почему async/await не спасает от блокировки Event Loop при CPU-bound задачах?

Cluster Module

**Cluster module** позволяет создать несколько копий приложения (workers), каждая из которых работает в отдельном процессе. Master-процесс управляет workers и распределяет входящие соединения.

Ключевое свойство: все workers слушают один и тот же порт. Это возможно благодаря механизму **SO_REUSEPORT** в ядре Linux и round-robin балансировке в Node.js.

**Round-robin balancing:** Master-процесс принимает соединения и распределяет их между workers по кругу. Worker 1 → Worker 2 → Worker 3 → Worker 1... Это обеспечивает равномерную нагрузку.

Теперь запросы обрабатываются параллельно! На 4-ядерном CPU пропускная способность вырастает в ~4 раза.

**Важно:** Каждый worker - это отдельный процесс с собственной памятью. Они **не разделяют состояние**. Если хранишь данные в памяти (кеш, сессии) - они будут разные в каждом worker'е.

**Решения для shared state:** - Redis/Memcached для кеша - Sticky sessions (запросы от одного клиента всегда на один worker) - Shared database для сессий

Что произойдёт, если в worker'е изменить глобальную переменную let counter = 0?

Worker Threads

**Worker Threads** - это настоящие потоки внутри одного процесса Node.js. В отличие от Cluster (где каждый worker - это отдельный процесс), Worker Threads разделяют память через **SharedArrayBuffer**.

**Когда использовать:** - CPU-bound задачи внутри одного запроса (хеширование, шифрование, парсинг большого JSON) - Параллельные вычисления (обработка изображений, математика) - Не нужно запускать несколько копий всего сервера

**Cluster vs Worker Threads:** - **Cluster:** Несколько процессов, каждый со своим Event Loop. Используй для масштабирования всего сервера. - **Worker Threads:** Несколько потоков внутри одного процесса. Используй для CPU-задач внутри одного запроса.

**Проблема:** Создавать новый поток на каждый запрос - дорого. Лучше использовать **Worker Pool** (пул потоков, которые переиспользуются).

**SharedArrayBuffer** позволяет разделять память между потоками. Это опасно (race conditions), но иногда необходимо для производительности.

В чём главное отличие Worker Threads от Cluster?

IPC (Inter-Process Communication)

**IPC (Inter-Process Communication)** - это способ обмена данными между процессами. В Node.js для этого используется встроенный механизм **message passing**.

В Cluster master-процесс общается с workers через **process.send()** и **process.on('message')**. Это работает через Unix Domain Sockets (на Linux/Mac) или Named Pipes (на Windows).

**Важно:** IPC в Node.js основан на сериализации JSON. Это означает: - Можно передавать объекты, массивы, примитивы - Нельзя передавать функции, классы, циклические ссылки - Большие данные (>1MB) передавать медленно

**Практический пример:** Graceful shutdown. Master отправляет сигнал workers, чтобы они закончили текущие запросы и завершились.

**IPC для координации:** Master может собирать статистику от workers (количество запросов, использование памяти) и принимать решения о перезапуске.

**Ограничения IPC:** - Сериализация/десериализация занимает CPU - Передача больших объектов (>10MB) медленная - Для shared state лучше использовать Redis/Memcached

Что произойдёт, если отправить через process.send() объект с циклической ссылкой?

Load Balancing

**Load Balancing** - это распределение нагрузки между workers. Node.js использует **round-robin** по умолчанию, но есть и другие стратегии.

**Встроенные стратегии в Node.js Cluster:** 1. **Round-robin (по умолчанию на Linux/Mac):** Запросы распределяются по кругу. Worker 1 → Worker 2 → Worker 3 → Worker 1... 2. **OS balancing (по умолчанию на Windows):** Операционная система сама решает, какому worker'у отдать соединение.

**Round-robin vs OS balancing:** - Round-robin гарантирует равномерное распределение, но добавляет overhead (master принимает все соединения). - OS balancing быстрее (workers сами принимают соединения), но может быть неравномерным.

**PM2 cluster mode:** PM2 - это production process manager для Node.js. Он автоматически создаёт cluster и управляет workers.

**Nginx как внешний балансировщик:** При нескольких серверах (не только workers на одной машине) лучше использовать Nginx для распределения нагрузки между ними.

**Sticky Sessions:** Если приложение использует in-memory сессии, нужно, чтобы запросы от одного клиента всегда попадали на один и тот же worker.

**Zero-downtime deployment:** 1. Master запускает новые workers с новым кодом 2. Отправляет старым workers сигнал на завершение (graceful shutdown) 3. Старые workers завершаются после обработки текущих запросов 4. Новые workers обрабатывают все новые запросы

Cluster автоматически делает приложение быстрее в 4 раза на 4-ядерном CPU

Cluster помогает только если есть CPU-bound задачи или высокая параллельная нагрузка. Для I/O-bound задач прирост минимальный

Node.js уже эффективно использует Event Loop для I/O. Cluster помогает распараллелить CPU-работу или обрабатывать больше одновременных соединений (например, 10000 websockets на 1 worker vs 40000 на 4 workers).

Зачем нужны sticky sessions при использовании Cluster?

Ключевые идеи

  • **Cluster** создаёт несколько процессов Node.js (по одному на ядро), каждый с собственным Event Loop. Используй для масштабирования всего сервера.
  • **Worker Threads** создают несколько потоков внутри одного процесса. Используй для CPU-bound задач (хеширование, вычисления).
  • **IPC** (process.send/on('message')) - это обмен данными между процессами через JSON. Для большого shared state используй Redis.
  • **Load balancing:** Round-robin (по умолчанию) vs OS balancing. В продакшене используй PM2 или Nginx для управления workers.
  • **Zero-downtime deployment:** PM2 автоматически делает graceful reload (старые workers завершаются, новые запускаются).

Связанные темы

Cluster - это часть экосистемы масштабирования Node.js:

  • Event Loop — Cluster создаёт несколько Event Loop (по одному на worker). Это помогает распараллелить CPU-bound задачи.
  • Streams — Worker Threads могут обрабатывать streams параллельно (например, сжатие файлов).
  • Memory Management — Каждый worker имеет собственную heap. Нужно мониторить использование памяти и перезапускать workers при утечках.

Вопросы для размышления

  • Когда лучше использовать Cluster, а когда Worker Threads? Приведи пример задачи для каждого.
  • Как реализовать zero-downtime deployment без PM2? Опиши алгоритм с использованием IPC.
  • Почему sticky sessions - это anti-pattern? Как избежать их использования?

Связанные уроки

  • os-02-processes
Cluster: Масштабирование Node.js

0

1

Войти