Высоконагруженные приложения на Node.js
Масштабируемость нельзя добавить потом — она закладывается архитектурой. Один процесс Node может обслужить 20–50k req/s, кластер — 500k, распределённая система — 10M+.
Пороги нагрузки
1 процесс Node 20–50k req/s (без I/O, чистый JS)
1 процесс с БД 5–15k req/s
Cluster на сервере 8 ядер 100–300k req/s
Кластер из 20 серверов ~10M соединений
После каждого порога предыдущая архитектура идёт в утиль. Не пытайся масштабировать монолит до 1M — нужно переписывать.
3 метрики нагруженности
- Throughput — запросов в секунду
- Latency — время ответа на запрос (p50, p95, p99)
- Concurrency — одновременных соединений
Что влияет на производительность
- Архитектура — главный фактор; правильная декомпозиция даёт x10
- Структуры данных важнее алгоритмов: подобранный Map/Set ускоряет больше чем рефакторинг цикла
- Зависимости приводят к lock-in: чем больше npm-пакетов, тем меньше контроль над производительностью
- V8 оптимизирует непредсказуемо: hidden classes, monomorphic vs polymorphic функции
- Скрытые классы: не меняй форму объекта в hot path, всё инициализируй сразу
Stateful vs Stateless
Stateless:
+ горизонтальное масштабирование тривиально
+ рестарт безопасен
− каждый запрос пересоздаёт контекст (БД-коннект, сессия)
− кэш во внешнем хранилище (Redis) → +latency
Stateful (Node-стиль):
+ состояние в памяти процесса = 0 latency
+ долгоживущие соединения с БД
− масштабирование требует sticky session или CRDT-репликации
− рестарт = потеря состояния → нужен graceful + warm start
Архитектурные приёмы
- Веб-сервер внутри приложения — не приложение внутри сервера (как PHP/Apache)
- Упреждающее чтение — заранее подгрузить в кэш то что понадобится
- Lazy запись — батчить запись в БД, сбрасывать пачкой
- Балансировка на клиенте — клиент знает список серверов, выбирает по хэшу userId
- Хэш из IP как защита от DoS — атаки попадают в один процесс, не валят кластер
- Свой бинарный протокол поверх WebSocket — без HTTP overhead на каждое сообщение
Подводные камни
- Зависимости в hot path —
lodash.cloneDeepв обработчике = тормоза - Процесс должен жить месяцами — утечки памяти убивают (даже маленькие при rps=10k растут быстро)
- Интерактивные сессии нельзя рестартить — WebSocket-клиент потеряет состояние; нужен graceful shutdown с миграцией
- Преждевременная оптимизация — без profiling и метрик улучшения вслепую делают только хуже
- Cluster — узкое место на 40+ ядрах — общая шина IPC становится bottleneck, нужны несколько процессов на 4–8 ядер каждый
🎓 Источники
- 🎓 [Высоконагруженные распределенные приложения на Node.js] · 2018-10-30 · YouTube · [Marp](../../../Documents/TimurShemsedinov/2018-10-30 — Высоконагруженные распределенные приложения на Node.js (7tfZDABPvVs).md)
- Тезисы: пороги нагрузки 50k → 500k → 10M, три метрики, архитектура определяет предел, отсутствие I/O быстрее чем async I/O, структуры данных важнее алгоритмов, эволюция стека Metarhia 2012→2016 (от cluster+nginx+mongo до in-memory + собственный протокол), мультиплексация стримов
- Цитата: «Масштабируемость нельзя добавить потом — она закладывается с первой строки»
- 🎓 [Введение в Node js и серверный JavaScript] · 2019-11-16 · YouTube
- Тезисы: 20–50k req/s одним процессом, ~1.5GB лимит V8, scaled через cluster по числу ядер
- 🎓 [Вступительная лекция Node.js и Back-end (КПИ)] · 2021-09-03 · YouTube
- Тезисы: минимум для бэкенда — асинхронность, ООП, GRASP, базы данных; автоматное программирование для надёжности