Как Node.js обрабатывает большое количество одновременных запросов?

Ответ

Node.js справляется с высокой нагрузкой благодаря своей однопоточной, неблокирующей архитектуре на основе событийного цикла (Event Loop). Вместо создания отдельного потока для каждого соединения (как в традиционных серверах), он использует асинхронный ввод-вывод и обратные вызовы (callbacks), что позволяет одному процессу эффективно обслуживать тысячи одновременных подключений.

Ключевые компоненты архитектуры:

  1. Event Loop (libuv): Обрабатывает асинхронные операции (сетевые запросы, чтение файлов). Пока одна операция ожидает ответа (например, от БД), цикл может обрабатывать другие запросы.
  2. Worker Pool: Для блокирующих, ресурсоемких задач (например, хэширование паролей, работа с файловой системой) используется пул рабочих потоков, чтобы не блокировать основной цикл событий.
  3. Кластеризация (Cluster module): Для использования всех ядер процессора можно запустить несколько процессов Node.js. Главный процесс (master) распределяет входящие соединения между рабочими процессами (workers).

Пример кластеризации:

const cluster = require('cluster');
const os = require('os');
const http = require('http');

if (cluster.isPrimary) {
  console.log(`Primary ${process.pid} is running`);
  // Запускаем worker'ов по количеству CPU
  for (let i = 0; i < os.cpus().length; i++) {
    cluster.fork();
  }
} else {
  // Workers создают HTTP-сервер
  http.createServer((req, res) => {
    res.writeHead(200);
    res.end('Hello from worker ' + process.pid);
  }).listen(8000);
  console.log(`Worker ${process.pid} started`);
}

Дополнительные стратегии для масштабирования:

  • Балансировщик нагрузки (Nginx): Распределение запросов между несколькими экземплярами Node.js.
  • Кеширование (Redis/Memcached): Хранение часто запрашиваемых данных в памяти для снижения нагрузки на БД.
  • Оптимизация асинхронного кода: Использование async/await и избегание блокирующих синхронных операций в основном потоке.

Ответ 18+ 🔞

А, слушай, про Node.js и его нагрузку! Ну это же классика, ёпта. Все думают: «Однопоточный? Да он же сдохнет от ста пользователей!» А он, хитрая жопа, берёт не количеством потоков, а хитростью. Представь бармена в адской забегаловке. Плохой бармен — один клиент заказал сложный коктейль, и он на полчаса упоролся его мешать, а очередь до улицы. А наш Node.js — он принял заказ, крикнул на кухню «Эй, готовьте!», и сразу к следующему. Пока кухня там колдует, он уже десятерым счет выставил. Вот и вся магия — однопоточный, неблокирующий Event Loop.

Из чего эта мартышлюшка состоит:

  1. Event Loop (это libuv): Главный по асинхронщине. Сетевой запрос, чтение файла — он не ждёт, как дурак, а ставит дело на паузу и идёт дальше. Ответ пришёл — он тебе колбэк впарит. Красота!
  2. Worker Pool: Ну а если задача тяжёлая, прям овердохуища ресурсов жрёт (типа пароль захешировать или гигабайтный файл прочитать)? Тогда её в сторонку, в пул рабочих потоков скидываем, чтобы основной цикл не встал колом. Умно, блядь.
  3. Кластеризация (Cluster module): А если одного процесса мало? Процессор-то многоядерный! Берём и запускаем клонов, как в «Звёздных войнах». Один главный процесс (master) раздаёт входящие запросы рабочим (workers), и они все дружно вротберунчик нагрузку принимают.

Вот смотри, как клоны плодить (код не трогаю, он святой):

const cluster = require('cluster');
const os = require('os');
const http = require('http');

if (cluster.isPrimary) {
  console.log(`Primary ${process.pid} is running`);
  // Запускаем worker'ов по количеству CPU
  for (let i = 0; i < os.cpus().length; i++) {
    cluster.fork();
  }
} else {
  // Workers создают HTTP-сервер
  http.createServer((req, res) => {
    res.writeHead(200);
    res.end('Hello from worker ' + process.pid);
  }).listen(8000);
  console.log(`Worker ${process.pid} started`);
}

Ну и куда же без фишек для совсем уж бешеных масштабов:

  • Балансировщик (Nginx): Ставим перед нашей оравой Node.js-процессов. Он как умный швейцар — запросы по рабочим равномерно раскидывает, чтобы никто не простаивал и не захлёбывался.
  • Кеширование (Redis): Чтобы по каждому чиху в базу не лезть. Частые данные — раз! — и в оперативку. Скорость — ни хуя себе.
  • Пиши код нормально: Используй async/await, не делай в основном потоке дикие синхронные вещи (типа readFileSync на гигабайтнике), и будет тебе счастье. А то доверия ебать ноль к такому коду.