Как NodeJS обрабатывает тысячи запросов, будучи однопоточным?

Привет, прошло много времени с тех пор, как я в последний раз писал пост на странице своего блога. Эти дни были тяжелыми для меня, потому что у меня было много дел, о которых нужно было беспокоиться. Но теперь я вернулся с новым постом. Обратите внимание, что это только мое мнение/точка зрения по этому поводу (конечно, я получил некоторые знания от друзей, блогов)

1. Почему однопоточная, а не многопоточная

Многие языки используют многопоточные модели, такие как PHP, Java, C#, но JS все еще предпочитает оставаться с однопоточной моделью цикла событий, потому что она имеет некоторые преимущества.

  • Первое, что необходимо учесть, это то, что Javascript был рожден с целью создания скриптового языка для front-end разработки. Его цель — создать быстрый, простой в использовании язык, который бы подошел для фронтенд-разработки. Работа с одним потоком намного проще, чем с несколькими потоками, где вам придется столкнуться с проблемами архитектуры проектирования (поскольку потоки используют одни и те же ресурсы), тупиками, условиями гонки и многими другими вещами, которые могут свести вас с ума.
  • Во-вторых, при многопоточной модели для каждого запроса вы будете создавать новый поток для его обработки. Проблема здесь в том, что количество потоков, которые вы можете создать, зависит от объема оперативной памяти сервера. В однопоточной модели вам нужно беспокоиться о процессоре, а не об оперативной памяти. Но с учетом скорости современных CPU, которые могут обрабатывать миллионы операций в секунду, для приложений, которые не требуют большого количества запросов с интенсивным использованием CPU, использование однопоточной модели является лучшим выбором (Мы поговорим о причине, по которой нам нужно заботиться о запросах с интенсивным использованием CPU, в следующих разделах, где мы обсудим, как NodeJS обрабатывает запрос).

2. Понимание процесса обработки запроса в NodeJS

a. Некоторые основные определения

Прежде чем перейти к основной части этого раздела, давайте рассмотрим несколько основных определений, чтобы, когда мы перейдем к следующей части, было легче понять секрет, лежащий в основе NodeJS.

NodeJS построен на базе движка V8 Engine и LibUV. В этой статье нас больше всего интересует LivUB. Он написан на языке C, содержит цикл событий, пул потоков (который обеспечивает больше потоков, которые могут быть использованы NodeJS) и очередь событий. Количество потоков в пуле потоков в LibUV по умолчанию равно 4. Таким образом, в основном, NodeJS будет иметь 1 основной поток и 4 под-потока (я не знаю, можно ли их назвать «под-потоками», но просто имейте в виду, что NodeJS не ходит только с 1 потоком).

Теперь перейдем к одному из самых больших споров всех времен среди разработчиков Javascript (а также некоторых других сообществ разработчиков). Это: Является ли NodeJS однопоточным?

  • Одним из доказательств того, что NodeJS не является однопоточным, является следующее: NodeJS построен поверх V8 Engine и LibUV, LibUV имеет пул потоков, который содержит некоторые другие потоки, которые NodeJS может использовать, так что, по сути, NodeJS имеет более одного потока, поэтому не верно, что NodeJS однопоточный. В этом споре я на стороне тех, кто считает, что NodeJS однопоточный. Позвольте мне объяснить почему: Да, это правда, что NodeJS может воспользоваться преимуществами использования потоков, предоставляемых LibUV. Но помните, что ключевое слово «однопоточный» означает возможность выполнения одного и того же фрагмента кода более чем 1 раз в одно и то же время. Например, если у вас есть цикл, который выводит в консоль слово, например: «Hello world». В NodeJS, в пределах одного экземпляра приложения NodeJS, не может быть одновременно запущено 2 цикла, вы можете проверить это сами.

Задача, связанная с процессором, и задача, связанная с IO

  • I/O = задача ввода/вывода, например: чтение некоторых строк из базы данных и их возврат, подсчет количества строк в файле, …
  • CPU: Задачи, которые должен выполнять ваш процессор. Например: вычисление операции: 1+1 = 2, обработка изображений.
  • Поэтому помните, что задача ввода-вывода — это то, с чем CPU не может справиться сам, но для этого должна быть задействована подсистема ввода-вывода.
  • Задача CPU должна выполняться в основном потоке NodeJS, в то время как задача ввода/вывода может выполняться в потоках в пуле потоков, предоставляемом LibUV. Именно по этой причине NodeJS не подходит для приложений с интенсивными задачами CPU. В ближайшие несколько минут мы расскажем об этом подробнее.

Блокирующий ввод/вывод против неблокирующего ввода/вывода:

  • Блокирующий ввод-вывод: Запросы, связанные с операциями ввода-вывода, блокируют поток/процесс до тех пор, пока они не будут выполнены.
  • Неблокирующий ввод/вывод: Запрос, связанный с операцией ввода/вывода, будет помещен в очередь и выполнен позже (если он не готов, если готов, то возвращается сразу).

b. Как NodeJS обрабатывает запрос

Итак, с определениями покончено. Теперь перейдем к тому, что вас больше всего интересует.

Вот как NodeJS обрабатывает запрос.

Когда приходит запрос, он сначала попадает в очередь, называемую очередью событий.

Цикл событий подхватывает первый элемент в очереди событий и начинает его обрабатывать. Здесь есть два случая:

  • Если запрос связан только с неблокирующей задачей ввода-вывода/задачей процессора (выполнить некоторые вычисления, без необходимости вызова других подсистем ввода-вывода) (в приведенном выше примере это работа по вычислению счета заказа), цикл событий поместит его в стек вызовов, и NodeJS выполнит его сразу же в основном потоке, Здесь возникает первое узкое место NodeJS, когда NodeJS обрабатывает эту задачу в главном потоке, цикл событий не будет забирать новую задачу из очереди событий, все остальные задачи должны ждать завершения задачи обработки, что приводит к тому, что время отклика будет чрезвычайно медленным. Именно по этой причине NodeJS не подходит для задач с интенсивным процессором, задача процессора может быть выполнена только в главном потоке, когда главный поток занят, он не может делать ничего другого.
  • Если это не блокирующая задача ввода-вывода, событие переместит это задание в другую очередь, задания в этой очереди ожидают выполнения потоками в пуле потоков, предоставляемом LibUV. Обратите внимание, что по умолчанию LibUV предоставляет только 4 дополнительных потока для обработки блокирующих заданий ввода-вывода. Это означает, что когда эти 4 потока заняты обработкой задач. Каждая 5-я задача, поступающая в поток, должна ждать (в очереди, о которой я говорил выше). Но имейте в виду, что количество потоков, предоставляемых LibUV, может быть изменено через настройки (но, насколько я знаю, 4 достаточно для обработки большинства запросов), если количество запросов слишком велико, тогда мы должны подумать о вертикальном масштабировании (создать еще один экземпляр приложения) вместо увеличения количества потоков LibUV.
    • Кто-то может удивиться, как это возможно, что NodeJS приложение может обрабатывать тысячи запросов, в то время как у него всего 5 потоков. Дело здесь в «обратном вызове». Когда поток (A) в пуле потоков получает задание, он будет взаимодействовать с другими подсистемами ввода-вывода, такими как база данных (например). Обратите внимание, что база данных также предоставляет некоторые потоки (B) сама, так что поток A просто приходит и говорит: «Эй, база данных, мне нужно получить некоторые данные в базе данных, но я не буду ждать, пока ты найдешь и вернешь их мне, вместо этого я оставлю здесь «обратный вызов», вызови его, когда ты закончишь свою работу, я вернусь в пул и возьму другое задание». Как только база данных выполнит свою работу, она вызовет функцию обратного вызова и отправит ответ обратно в очередь обратных вызовов, а когда стек вызовов опустеет, цикл событий будет выбирать из очереди обратных вызовов в стек вызовов, чтобы продолжить обработку запроса.

Ниже приведена простая диаграмма, визуализирующая, как NodeJS обрабатывает запрос

Источник: https://dattp.github.io/2020-04-10-event-loop-in-nodejs/

Последующие вопросы:

У вас, ребята, могут быть некоторые сомнения по этому поводу (как и у меня), поэтому я перечислю некоторые вопросы здесь, надеюсь, что они (частично) соответствуют вашим опасениям.

❓ Хорошо, я понимаю, что вы сказали в (1). Но я совсем не понимаю, что произойдет после этого, он отправит ответ обратно в стек вызовов и что потом?

→ Да, верно, имейте в виду, что ваша функция, которая обрабатывает запрос, обычно не простая, она может быть связана с множеством обращений к базе данных или любой другой блокирующей задачей ввода-вывода. Например

const handleRequest = async (input: InputDTO) => {
    const dataA = await dbConnection.get(A);
  const dataB = await dbConnection.get(B);
// process the output which combining dataA and dataB or some other stuff associated with those datas.
}
Вход в полноэкранный режим Выйти из полноэкранного режима

Поэтому NodeJS разобьет вашу функцию handleRequests на несколько более мелких задач. Например, получение данныхА — это одна задача, получение данныхВ — другая задача, и, наконец, обработка вывода — еще одна задача. Каждая задача отдельно помещается в очередь событий и обрабатывается отдельно, (конечно, это должно быть в том порядке, в котором вы пишете в своем коде).

  • Небольшое замечание в этом примере: всякий раз, когда вы вызываете ключевое слово «await» в асинхронной функции, то, что идет после ключевого слова await, будет автоматически обернуто в Promise. и NodeJS прекратит выполнение вашей функции в тот момент, когда она встретит ключевое слово await, она продолжит обработку этой функции, когда получит ответ от этого Promise.

Когда данныеА будут возвращены из обратного вызова, NodeJS продолжит получать данныеВ из базы данных, цикл событий подхватит их (если они находятся на вершине стека вызовов) и обработает их точно так же, как обрабатывал данныеА. И так будет продолжаться до тех пор, пока не будет достигнут конец функции обработки запроса.

❓ Каков приоритет в случае, если стек вызовов пуст, а у нас одновременно готовы задачи из очереди событий и очереди обратного вызова.

  • На мой взгляд, сначала будет выполняться задача из очереди обратного вызова, поскольку задача из очереди обратного вызова имеет отношение к предыдущему запросу, лучше отправить ответ на предыдущий запрос, а не обрабатывать новый, если они оба готовы. Но это только мое предположение. Возможно, NodeJS не следует этому правилу.

❓ Я думаю, что очередь обратного вызова содержит несколько меньших типов очередей, не так ли?

  • Вы правы, когда я говорю об очереди обратного вызова, я просто хочу сказать о высоком уровне обработки задач в NodeJS. Действительно, у нас есть более 1 очереди обратного вызова. К ним относятся: 1) очередь NextTick 2) очередь микрозадач 3) очередь таймеров 4) очередь обратных вызовов IO (запросы, операции с файлами, операции с db) 5) очередь опроса IO 6) очередь фазы проверки или очередь SetImmediate 7) очередь закрытия обработчиков. Цикл событий будет постоянно проверять эти очереди обратных вызовов в порядке их следования, и если какая-либо задача в этой очереди будет выполнена, он переместит ее обратно в стек вызовов для продолжения процесса (конечно, в случае, если стек вызовов пуст).

3. Резюме

Итак, это все, чем я хочу поделиться с вами сегодня. Обратите внимание, что все они основаны на моем понимании и некоторых статьях, которые я прочитал в интернете, они могут быть не верны на 100% (а может быть и совсем неверны. Если в этой статье что-то не так. Пожалуйста, не стесняйтесь оставить комментарий, и я пойду, чтобы обсудить больше с вами, ребята. Спасибо и до свидания

Ссылки:

https://viblo.asia/p/ben-trong-nodejs-2-thu-vien-libuv-RQqKLRMOl7z

https://dattp.github.io/2020-04-10-event-loop-in-nodejs/

https://www.digitalocean.com/community/tutorials/node-js-architecture-single-threaded-event-loop

Оцените статью
devanswers.ru
Добавить комментарий