Производительность и кластеризация
Один цикл событий быстр, но это по-прежнему одно ядро процессора. Вот как задействовать их все.
- объяснить узкое место цикла событий и отличать задачи, ограниченные процессором, от задач ввода-вывода;
- использовать модуль
cluster, чтобы породить по одному воркеру на ядро и разделить слушающий порт; - описать, как распределяются входящие соединения (round-robin против операционной системы);
- выбирать между
cluster,worker_threadsи горизонтальным масштабированием; - запускать боевые процессы под менеджером в стиле PM2 с перезагрузкой без простоя;
- рассуждать о балансировке нагрузки, липких сессиях и общем состоянии между воркерами.
1. Узкое место цикла событий
Вы уже знаете, что цикл событий держит один поток занятым, никогда не блокируясь на вводе-выводе: пока запрос к базе данных или чтение файла в пути, цикл обслуживает другие колбэки. Но есть тонкий и критически важный нюанс — сам ваш JavaScript всё равно выполняется в одном потоке. Любой колбэк, тратящий реальное процессорное время (разбор огромного JSON, хеширование пароля с большим фактором стоимости, изменение размера изображения на чистом JS, плотный числовой цикл), захватывает поток в заложники. В это время ни один другой запрос не может быть обслужен, ни один таймер не сработает и ни один колбэк ввода-вывода не выполнится.
Именно это различие управляет каждым решением о масштабировании в Node:
- Задачи, ограниченные вводом-выводом (ожидание диска, сети, базы данных) — цикл остаётся свободным, пока ожиданием занимается ОС. Один процесс с радостью обслуживает тысячи одновременных соединений.
- Задачи, ограниченные процессором (вычисления, держащие поток занятым) — цикл заблокирован. Один медленный обработчик тормозит всех остальных клиентов, а рост числа одновременных запросов лишь ухудшает задержку.
Аналогия. Процесс Node — это один невероятно быстрый официант. Пока гости ждут кухню (ввод-вывод), официант снуёт между множеством столов, принимая заказы. Но как только вы просите этого единственного официанта лично приготовить блюдо за 20 минут (процессорная работа), все остальные столы ждут, каким бы быстрым он ни был. Это не решается просьбой двигаться быстрее — нужно нанять больше официантов. Это и есть кластеризация.
Обработчик, ограниченный процессором, не только замедляет сам себя; он раздувает задержку цикла событий для всех. Измерение этой задержки (например, через perf_hooks.monitorEventLoopDelay()) — стандартный способ обнаружить заблокированный цикл в бою.