Node.js обязана своей исключительной пропускной способностью и параллелизмом обманчиво простому дизайну: единственному потоку выполнения, управляющему неблокирующими асинхронными операциями. В сердце этого среды выполнения находится цикл событий— непрерывный цикл планирования, реализованный на C++ с помощью libuv , который управляет таймерами, системными событиями ввода-вывода, микрозадачами и передачей задач в пул потоков. Понимание того, как цикл событий координирует каждую фазу выполнения, имеет фундаментальное значение для создания низколатентных, масштабируемых сервисов Node.js.
Архитектура: V8, libuv и парадигма одного потока
Когда разработчик говорит, что Node.js «однопоточна», он имеет в виду конкретно стек вызовов JavaScript. Код вашего приложения выполняется в одном главном потоке, что исключает гонки данных, сложные мьютексы и опасности синхронизации потоков. Однако лежащая в основе среда выполнения является сильно многопоточной:
- Движок V8: движок JavaScript с открытым исходным кодом от Google синтаксически анализирует код, компилирует функции в машинные инструкции, управляет кучей памяти и организует сборку мусора.
- libuv: высокопроизводительная кроссплатформенная библиотека асинхронного ввода-вывода, написанная на C. Она предоставляет цикл событий, кроссплатформенные механизмы опроса (
epollв Linux,kqueueв macOS/BSD,IOCPв Windows) и пул рабочих потоков для синхронных операционных системных операций. - Ядро операционной системы: современные ядра поддерживают неблокирующие сетевые сокеты и интерфейсы уведомлений о событиях, которые обрабатывают тысячи открытых соединений одновременно без выделенных потоков ядра.
Операции сетевого ввода-вывода (HTTP-запросы, TCP-потоки, WebSockets) не расходуют рабочие потоки. Они регистрируются непосредственно в слое неблокирующего демультиплексирования ядра операционной системы (таком как epoll) и будят цикл событий только тогда, когда в буферах сокетов появляются входящие данные.
6 фаз жизненного цикла цикла событий
Каждая полная итерация цикла событий называется тактом(tick). На каждом такте libuv последовательно обрабатывает определенные очереди обратных вызовов FIFO в рамках шести различных фаз:
| Фаза | Очередь и обязанности | Тип��чные операции |
|---|---|---|
| 1. Таймеры | Выполняет обратные вызовы, запланированные истекшими таймерами пороговых значений. | setTimeout(), setInterval() |
| 2. Отложенные обратные вызовы | Выполняет отложенные обратные вызовы ввода-вывода из предыдущего цикла. | Обработчики ошибок TCP-сокетов (ECONNREFUSED) |
| 3. Ожидание, Подготовка | Внутреннее обслуживание подсистемы. | Синхронизация внутреннего состояния libuv |
| 4. Опрос | Получает новые события ввода-вывода, выполняет обратные вызовы ввода-вывода и вычисляет тайм-аут сна. | Входящие HTTP-запросы, чтение из базы данных, файловые буферы |
| 5. Проверка | Выполняет обратные вызовы, зарегистрированные специально для запуска сразу после опроса ввода-вывода. | setImmediate() |
| 6. Обратные вызовы закрытия | Выполняет очистку ресурсов и окончательные хуки завершения работы. | socket.on('close'), process.on('exit') |
Фаза опроса и вычисление блокирующего сна
The Фаза опроса — это то место, где Node.js проводит подавляющую часть своей операционной жизни. При входе в фазу опроса:
- Если очередь опроса содержит скрипты для обработки, цикл событий выполняет их синхронно до тех пор, пока очередь не опустеет или не будет достигнут системный лимит.
- Если очередь опроса пуста, цикл проверяет, ждут ли
setImmediate()обратные вызовы. Если да, он немедленно переходит к фазе проверки. - Если
setImmediate()скрипты отсутствуют, цикл проверяет минимальную кучу таймеров. Если таймер истек, он возвращается к фазе таймеров. В противном случае он переводит поток в режим сна в вызове ожидания ОС (например,epoll_wait) на точную оставшуюся длительность до срабатыван��я ближайшего таймера.
Микрозадачи: process.nextTick против Promise.then()
Очереди микрозадач находятся вне стандартного шестифазного цикла libuv и обладают более высоким приоритетом выполнения. Всякий раз, когда стек вызовов JavaScript завершает выполнение операции или при переходе между фазами цикла событий, Node.js полностью опустошает свои очереди микрозадач перед продолжением.
| Тип микрозадачи | Уровень приоритета | Гарантия выполнения |
|---|---|---|
process.nextTick() |
Наивысший (очередь тактов) | Выполняется сразу после завершения текущей операции, перед любыми другими микрозадачами или переходом фаз. |
Promise.then() / async await |
Высокий (очередь Promise) | Выполняется сразу после всех ожидающих process.nextTick() обратные вызовы полностью обработаны. |
setImmediate() |
Стандартный (Фаза Check) | Выполняется во время фазы Check после опроса ввода-вывода, позволяя событиям ввода-вывода обрабатываться в первую очередь. |
Предупреждение о голодании цикла событий: Поскольку рекурсивные process.nextTick() вызовы полностью обрабатываются до того, как цикл событий сможет продвинуться вперед, бесконечная рекурсия nextTick заморозит опрос ввода-вывода и вызовет голодание всего сервера.
Пул рабочих потоков libuv
Некоторые операции ОС не поддерживают неблокирующие асинхронные интерфейсы во всех операционных системах. Чтобы предотвратить блокировку основного потока JavaScript во время этих операций, libuv поддерживает фоновый пул рабочих потоков C++.
Операции, делегируемые пулу потоков libuv, включают:
- Модуль файловой системы (
fs):fs.readFile(),fs.writeFile(),fs.stat()и операции с директориями. - Модуль криптографии (
crypto): ресурсоемкое хэширование, генерация ключей и шифры (crypto.pbkdf2(),crypto.scrypt(),crypto.randomBytes()). - Модуль сжатия (
zlib): потоковые преобразования Gzip, Brotli и Deflate. - DNS-разрешение:
dns.lookup()(которое использует синхронную библиотеку Cgetaddrinfo(3)).
Настройка UV_THREADPOOL_SIZE
По умолчанию libuv выделяет пул потоков размером в 4 рабочих потока. На серверах с высокой пропускной способностью, выполняющих параллельный дисковый ввод-вывод, сжатие или хэширование паролей, четыре потока могут легко перегрузиться, что приведет к постановке операций в очередь и появлению искусственных пиков задержки.
Вы можете увеличить размер пула потоков до 1024 потоков перед запуском процесса Node:
export UV_THREADPOOL_SIZE=16 && node server.js
Мониторинг задержки цикла событий в продакшене
Когда синхронное выполнение в главном потоке занимает слишком много времени (например, синтаксический анализ строки JSON размером МБ или выполнение неиндексированного регулярного выражения), цикл событий блокируется и не может продвигаться дальше. Эта задержка измеряется как задержка цикла событий.
В Node.js 12+ вы можете измерять задержку цикла событий с высоким разрешением нативно, используя perf_hooks:
const { monitorEventLoopDelay } = require('perf_hooks');
const histogram = monitorEventLoopDelay({ resolution: 10 });
histogram.enable();
setInterval(() => {
const p99 = (histogram.percentile(99) / 1e6).toFixed(2);
const max = (histogram.max / 1e6).toFixed(2);
console.log(`Event Loop Lag — p99: ${p99}ms | max: ${max}ms`);
histogram.reset();
}, 5000);
Практические стратегии предотвращения блокировки цикла событий
- Перенос тяжелых вычислений в рабочие потоки: Используйте встроенный модуль
worker_threadsNode.js или отдельные микросервисные процессы для задач, требующих высокой производительности CPU, таких как обработка изображений, компиляция PDF или сложная трансформация данных. - Потоковая передача больших полезных данных: Избегайте буферизации огромных JSON-файлов в памяти с помощью
fs.readFileSync(). Вместо этого передавайте фрагменты данных с помощьюfs.createReadStream()и потоковых парсеров JSON. - Разбиение длинных циклов с помощью setImmediate: При обработке большого набора элементов разделите массив на более мелкие части и планируйте последующие итерации с помощью
setImmediate(), чтобы позволить входящим событиям ввода-вывода обрабатываться между частями. - Безопасные регулярные выражения: Защититесь от атаки ReDoS (отказ в обслуживании регулярных выражений), избегая катастрофического возврата (backtracking) в вложенных квантификаторах.
Развертывайте приложения Node.js и микросервисы на оптимизированной инфраструктуре VPS, настроенной с использованием кластеров процессов PM2, граничного кэширования обратного прокси-сервера Nginx и автоматического SSL.
- Ознакомьтесь с тарифными планами VPS хостинга Node.js — Скорость «железа» и выделенные виртуальные процессоры (vCPU)
- Архитектура платформы multiDomainCMS — Время отклика SSR менее мс
- Запросить индивидуальный расчет инфраструктуры — Бесплатный подбор конфигурации и помощь в миграции