Когда нужны «Воркеры» (Worker threads) в NodeJS? Почему тормозит NodeJS?
Лучше спрашивать не «Зачем нужны«, а «Когда нужны» Воркеры. NodeJS — это однопоточная среда. То есть на один процесс приходится один поток. И в какой-то внезапный момент времени этот поток может забиться и перестать быстро работать.
Вот ты разработчик. Пишешь, пишешь себе код потихоньку и тут на тебе, производительность падает. Вроде и процессор мощный. Вроде и памяти предостаточно. Но после написания дополнительного функционала в приложении, всё люто начинает тормозить. Что меняется? Может ты чего-то не знаешь?
Чем забивается поток в NodeJS?
Самую большую нагрузку на поток несут «долгие синхронные вычисления«. Это когда ты пытаешься что-то сложно вычислять — хэш-функции, криптография, сложные регулярные выражения по огромному массиву строк, циклы в циклах и ещё раз в циклах, лютая высшая математика и тому подобное.
Так вот вся проблема начинается тогда, когда «цикл событий» не может пробиться в это «долгое синхронное вычисление«. И что происходит тогда? Очень быстро ничинают переполняться очереди «макро/микро задач». Начинает сильно возрастать «задержка шага цикла событий«.
Этот параметр называется «Event Loop Latency p95» и «Event Loop Latency«. Если ты работаешь с менеджером процессов PM2, то значение в более 100 миллисекунд будет говорить о проблеме в разработке. Чем больше это число, тем «тупее» будет работать NodeJS. Важно отметить, что работать будет (не отвалится), но медленно.
Если подвести итог этой проблемы, то на один поток NodeJS выделяет какой-то минимум системных ресурсов, чтобы решать задачи среды выполнения кода. Короче говоря, процессор не работает на максимальных возможностях.
Простое объяснение многопоточности в NodeJS
Если по-простому рассказывать про поток, то представьте себе дорогу по которой едут автомобили. Если вы живёте в крупном городе, то я рекомендую обратить внимание на КАД (Кольцевую Автомобильную Дорогу). Это такая дорога, по которой в любое время дня и ночи проезжают автомобили.
Давайте будем говорить только об одном направлении движения. Теперь я задам вам вопрос — Какова скорость потока автомобилей по этой дороге? 140? 90? 50? Не парьтесь. Правильного ответа тут нет, если мы не учитываем время, в которое делаем расчёт скорости потока автомобилей.
Согласитесь, что в будние дни в 12 часов дня количество автомобилей на дороге будет больше, чем в 12 часов ночи этого же дня.
А теперь представьте себе ситуацию: лето, пятница, вечер, хорошая солнечная погода, шашлыки, банька! Все валят из города по своим пригородным дачам и садоводствам. Если вы на авто, то выезд из города в это время создаст для вас проблему. Вы встанете в пробку в 98% подобных случаев. Ваша скорость потока будет 1 километр в час.
Теперь посмотрим на это со стороны. Вроде у вас мощный автомобиль, который легко разгонится до 120. Вроде есть дорога, которая НЕ ВСЕГДА в пробке. Но вы не едете. И причём не только вы. Все остальные также «тормозят». Что происходит? Может проблема в количестве участников дорожного движения?
Думаю вы догадались, что вся проблема в дороге, которая просто не справляется с таким потоком автомобилей. Какое решение проблемы вам кажется самым верным в данной ситуации? Оно первым придёт вам на ум.
Решение: ВАМ НУЖНА ЕЩЁ ОДНА ДОРОГА !!!
Часть автомобилей поедут по одной дороге, а часть поедет по другой. И тогда все будут «ехать», а не «стоять».
Надеюсь, аналогия понятна. Теперь перейдём обратно на уровень языка программирования JavaScript (NodeJS).
- Поток — это дорога с одной полосой в одном направлении. (у процессора есть только вход и выход, и каждый такт переносит электроны от входа к выходу)
- Сложная, долгая синхронная функция — это медленный гружёный автомобиль.
- Задержка цикла событий — это время, за которое этот участок дороги проезжает наш медленный автомобиль.
Зачем нужны «Воркеры» (Worker threads) в NodeJS?
Теперь мы можем дать ответ. Воркеры (Worker threads) в NodeJS нужны для того, чтобы вынести в отдельный поток долгое синхронное вычисление какой-то сложной функции.
Воркер знает о существовании основного потока. Воркер может принять набор данных, выполнить расчёт и вернуть результат вычисления обратно в основной поток.
Основному потоку останется только получить готовый результат и положить его в нужное место.
Согласитесь, что идти в магазин и собирать продукты это не одно и тоже, что черпать борщ из тарелки себе в рот. Поход в магазин и приготовление борща — это более затратная процедура, чем поедание готовой жижи. Кто-то ходит в магазин, кто-то готовит борщ, а кто-то есть. Вот такой алгоритм.
Почему поток в NodeJS ограничен в ресурсах?
Это просто чей-то расчёт. Просто разработчики NodeJS договорились, что один поток должен «откусывать» от доступного ему процессора маленький кусочек тактовой частоты. Почему так? Я не знаю. Нужно икать информацию.
Скорее всего для большинства стандартных задач, одного потока более чем достаточно.
Многопоточность и Асинхронность в NodeJS
Никогда не связывайте эти два понятия в одно. Это разные вещи!!!
Асинхронность — это когда вы хотите на некоторое время съехать с дороги на обочину и постоять там неопределённое (или определённое) время. От того что вы съехали на обочину, дорога не поменялась. Вы всё равно захотите однажды вернуться на эту же дорогу. В момент вашего возврата обратно в поток, вы ещё сильнее замедлите его работу так как вам должны будут уступить место на дороге. Так вот если таких «обочечников» много, то дорожная ситуация от съезда/заезда на обочину не изменится. Если на дороге «пробка», то обочина тоже в «пробке».
Многопоточность — это несколько отдельных дорог. Чем больше дорог, темы выше вероятность, что всем автомобилям их будет хватать. Если всем авто хватает дорог, то не имеет значения на какой дороге вы съехали/заехали с обочины.
Долгая синхронная функция в NodeJS
Теперь рассмотрим пример долгой синхронной функции на JavaScript (NodeJS). По сути нам нужен цикл, который выполняет большое количество итераций с какой-то сложной математической операцией — например, получение квадратного корня числа.
function f_sync_long(obj = {}){ // Фиксируем время начала выполнения функции obj.start_time = Date.now(); // Прописываем ключ для объекта console.log(`Старт в: ${obj.start_time}`); // Получаем случайное количество итераций для цикла // Имитируем разный объём данных для синхронной обработки obj.iterations = Math.ceil(10**9 * Math.random()); // Накапливаем вычисления в переменную obj.total = 0; // Запускаем цикл for(let x = 0; x < obj.iterations; x++){ // Условная дополнительная нагрузка на функцию через вычисление корней // для каждой пятой итерации if(!(x%5)){ obj.total += Math.sqrt(x*10); }; // Постоянная нагрузка для каждой итерации цикла obj.total += Math.sqrt(x*10); }; obj.delay_time = Date.now() - obj.start_time; obj.end_time = Date.now(); console.log(`Финиш в: ${obj.end_time}`); console.log(`Затрачено времени: ${obj.delay_time}`); // Возвращаем созданный объект return obj; };
Каждый вызов данной функции в консоли браузера будет вызывать разные задержки выполнения.
Количество итераций варьируется в пределах одного миллиарда. Это влияет на итоговое время полного заверения работы функции.
На скриншоте можно увидеть задержки в 2,8 секунд, 4,5 секунды и 1 секунда. Это очень долго.
Пока в одном потоке выполняется данная функция, то другие функции не смогут пробиться в данный поток для вычисления. Это значит, что любой асинхронный код тупо не будет выполнен вовремя при достижении таймеров или иных событий.
Такое долгое вычисление нужно выносить в отдельный поток и высчитывать отдельной частотой процессора.
Информационные ссылки
Стандарт NodeJS — https://nodejs.org/api/worker_threads.html
Цикл событий JavaScript — https://html.spec.whatwg.org/multipage/webappapis.html#event-loops
Цикл событий NodeJS — https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick
Дэвид Хеттлер — https://davidhettler.net/blog/event-loop-lag/