Node.js является событийно-ориентированной системой. Другими словами, все, что происходит в ноде, является реакцией на события и события вызывают каскад колбеков. Этот механизм работает на основе библиотеки libuv и называется циклом событий (event loop).
Есть популярное мнение, что цикл событий является одним из самых «недопонимаемых» принципов платформы (Node.js).
Общие заблуждения
Как я уже упоминал, в цикл событий Node.js построен на базе библиотеки libuv. В одном из своих выступлений, Bert Belder, один из разработчиков libuv, при помощи поиска картинок известной поисковой системы, показал, на сколько разные подходы используют люди для того, чтобы изобразить своё понимание (далеко не всегда правильное) механизмов работы цикла событий.
Рассмотрим самые популярные заблуждения.
Заблуждение 1: Цикл событий работает в отдельном потоке
Заблуждение
Существует главный поток, исполняющий пользовательский javascript код и существует другой поток, в котором работает цикл событий. Каждый раз, когда выполняется асинхронная операция, главный поток передает управление в поток цикла событий, а по завершению операции, цикл событий передает сообщение о необходимости выполнить колбек в главный поток.
Реальность
Существует только один поток, который выполняет пользовательский код javascript и цикл событий. Выполнение колбеков (весь пользовательский код, по сути, является колбеком) инициируется циклом событий. Ниже затронем этот вопрос чуть подробнее.
Заблуждение 2: Вся асинхронная работа выполняется пулом потоков
Заблуждение
Асинхронные операции, такие как работа с фаловой системой, осуществление исходящих HTTP-запросов или запросы к базам данных, всегда выполняются в пуле потоков, контролируемых библиотекой libuv.
Реальность
Действительно, по умолчанию, libuv создает пул, состоящий из четырех потоков, которым могут обрабатываться асинхронные вызовы. Но современные операционные системы предоставляют асинхронные интерфейсы для большинства задач ввода-вывода, например AOI в Linux. Там, где это возможно, libuv использует доступные асинхронные интерфейсы, избегая использования пула потоков. Похожая ситуация сложилась и с различным сторонним софтом, например базами данных. В данном случае, авторы драйверов используют асинхронные интерфейсы охотнее, чем пулы потоков. В общем, пулы потоков используются для асинхронных взаимодействий только в самых крайних случаях.
Заблуждение 3: Цикл событий — это что-то вроде стека или очереди
Заблуждение
Цикл событий содержит FIFO очередь из асинхронных заданий и, по завершению задания, вызывает его колбек.
Реальность
Цикл событий содержит структуры, похожие на очереди, но он не обрабатывает весь стек последовательно. Цикл событий представляет собой процесс, состоящий из этапов (групп задач), которые выполняются по очереди.
Подробнее про этапы цикла событий
Для полного понимания цикла событий, мы должны понять, какие задачи выполняются на каком этапе. В графическом виде работу цикла событий можно представить так:
Рассмотрим этапы чуть подробнее. Полное описание можно найти на официальном сайте.
Timers
На данном этапе выполняется код, инициированный через setTimeout() или setInterval().
IO Callbacks
Здесь выполняются почти все колбеки. Как было сказано ранее, почти весь код в Node.js — колбеки (например входящий http запрос вызывает каскад колбеков), а значит, почти весь пользовательский код выполняется на этом этапе.
IO Polling
Опрос новых событий, которые будут обработаны в следующем проходе цикла.
Set Immediate
Выполняет функции, зарегистрированные через setImmediate().
Close
На данном этапе выполняются все колбеки для событий on(‘close’).
Мониторинг цикла событий
Мы значем, что все в node.js приложении выполняется через цикл событий. А значит, если мы сможем собрать его мертирики, мы сможем достоверно знать общее состояние и производительность приложения.
К сожалению, нет единого API для получения метрик цикла событий, поэтому каждая утилита представляет свой набор метрик. Давайте подумаем, что мы можем получить.
Частота тиков
Количество тиков в единицу времени.
Длительность тика
Время выполнения одного тика.
Дальнейшие результаты получены при помощи нативного модуля мониторинга от Dynatrace.
Метрики частоты и длительности тика в действии
На первый взгляд, результаты тестирования могут быть очень даже удивительными.
В следующем сценарии, использовано приложение на express.js, которое осуществляет вызов на другой http сервер.
Четыре сценария тестирования:
1. Простой
Нет входящих запросов.
2. ab -c 5
5 конкурентных запросов одновременно
3. ab -c 10
10 конкурентных запросов одновременно
4. ab -c 10 (намеренная задержка ответа)
Http сервер возвращает ответ через с задержкой в 1 секунду. Такое поведение должно вызвать накопление очереди запросов, ожидающих ответа.
На графике мы можем видеть интересные результаты: частота и длительность тиков цикла событий динамически подстраивается под выполняемые операции.
Если приложение простаивает, то есть в очереди нет ожидающих выполнения задач (таймеров колбеков и так далее), цикл событий начинает работать на минимальной частоте, в ожидании входящих событий.
Также мы видим, что метрики под одной и тоже входящей нагрузкой не одинаковы, цикл событий адаптирует частоту тиков к реальной нагрузке.
Судя по всему, лучшая производительность приложения достигается на 5 одновременных подключениях.
Не смотря на всю информативность этих данных, мы по прежнему не можем знать, какой этап занял сколько времени в каждом тике. Добавим еще пару значений для мониторинга.
Задержки обработки
Это значение показывает, как долго асинхронная задача ожидала выполнения пулом потоков.
Высокое значение этой метрики говорит о занятости или полном заполнении пула потоков.
Для наглядности, можно использовать приложение, обрабатывающее изображения при помощи модуля Sharp. Обработка изображений — довольно сложная операция и Sharp будет использовать пул потоков.
Запустив снова утилиту Apache bench с пятью одновременными подключениями, можно увидеть, что время ожидания обработки вырастает практически с нуля до 8-10 мс в среднем и превышает 20 мс в пиках.
Задержки цикла событий
Данная метрика отражает время задержки выполнения задачи, созданной при помощи setTimeout().
Высокое значение этой метрики означает, что цикл событий занят обработкой колбеков.
Для демонстрации, можно использовать приложение, вычисляющее числа Фибоначчи не самым оптимальным способом.При помощи все той же утилиты ab и пяти одновременных подключений, можно увидеть, что из-за заполнения очереди обработки колбеков, значительно вырастает время задержки цикла событий.
Все четыре метрики позволяют лучше понять, как работает node.js под капотом.
Настройка цикла событий
Конечно, метрики, сами по себе, не помогут решить проблему производительности приложения. Нужны конкретные действия, основанные на знании значений метрик. Несколько советов, что делать, когда цикл событий кажется перегруженным.
Использование всех доступных ядер CPU
Приложение Node.js выполняется в одном потоке. В условиях современных многоядерных процессоров, это означает, что нагрузка не распределяется на все доступные ядра и они простаивают.Использование модуля cluster позволяет node легко создавать дочерние процессы на каждое доступное ядро. Каждый дочерний процесс имеет свой цикл событий и родительский процесс прозрачно распределяет нагрузку между всеми дочерними процессами.
Настройка пула потоков
Как уже говорилось выше, libuv создает пул из 4 потоков. Значение по умолчанию может быть переопределено переменной окружения UV_THREADPOOL_SIZE.
С одной стороны, увеличение пула может помочь решить проблему при работе с вводом-выводом, с другой стороны, это приведет к большему расходу оперативной памяти и/или процессорного времени. Рекомендую подходить к этой настройке с осторожностью.
Делегирование специфических задач
Если Node.js покажется вам не самой подходящей платформой для определенного рода вычислений, можно просто вынести конкретный блок задач в отдельный сервис, написанный на более подходящем языке.
Подведем итоги
- Цикл событий — это основа приложения Node.js.
- Его работа часто недопонимается — на самом деле, это набор этапов, на каждом из которых выполняются задачи разных видов.
- На данный момент нет встроенного механизма получения метрик цикла событий, но есть много различных сторонних решений.
- Метрики могут помочь понять, где находятся узкие для производительности места.