Высоконагруженные приложения на 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 метрики нагруженности

  1. Throughput — запросов в секунду
  2. Latency — время ответа на запрос (p50, p95, p99)
  3. 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 pathlodash.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, базы данных; автоматное программирование для надёжности

См. также