Я понимаю, что Node.js использует однопоточную обработку и цикл обработки событий для обработки запросов, обрабатываемых только по одному за раз (что не является блокирующим). Но все же, как это работает, допустим, 10000 одновременных запросов. Цикл событий обработает все запросы? Разве это не займет много времени?

Не могу (пока) понять, как он может быть быстрее многопоточного веб-сервера. Я понимаю, что многопоточный веб-сервер будет дороже по ресурсам (памяти, ЦП), но не будет ли он все равно быстрее? Я, наверное, ошибаюсь; пожалуйста, объясните, как этот однопоточный процесс выполняется быстрее при большом количестве запросов и что он обычно делает (на высоком уровне) при обслуживании большого количества запросов, например 10 000.

А также, будет ли эта однопоточная система хорошо масштабироваться при таком большом количестве? Имейте в виду, что я только начинаю изучать Node.js.

g_b

Ответов: 8

Ответы (8)

Если вам нужно задать этот вопрос, то вы, вероятно, не знакомы с тем, что делает большинство веб-приложений / сервисов. Вы, наверное, думаете, что все программы делают это:

user do an action
       │
       v
 application start processing action
   └──> loop ...
          └──> busy processing
 end loop
   └──> send result to user

Однако веб-приложения, да и вообще любое приложение с базой данных в качестве серверной части, работают иначе. Веб-приложения делают это:

user do an action
       │
       v
 application start processing action
   └──> make database request
          └──> do nothing until request completes
 request complete
   └──> send result to user

В этом сценарии программное обеспечение тратит большую часть своего рабочего времени, используя 0% процессорного времени, ожидая возврата базы данных.

Многопоточное сетевое приложение:

Многопоточные сетевые приложения обрабатывают указанную выше рабочую нагрузку следующим образом:

request ──> spawn thread
              └──> wait for database request
                     └──> answer request
request ──> spawn thread
              └──> wait for database request
                     └──> answer request
request ──> spawn thread
              └──> wait for database request
                     └──> answer request

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

Однопоточный цикл обработки событий

Поскольку мы проводим большую часть нашего времени, используя 0% ЦП, почему бы не запустить какой-нибудь код, когда мы не используем ЦП? Таким образом, каждый запрос по-прежнему будет получать такое же количество процессорного времени, что и многопоточные приложения, но нам не нужно запускать поток. Итак, делаем это:

request ──> make database request
request ──> make database request
request ──> make database request
database request complete ──> send response
database request complete ──> send response
database request complete ──> send response

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

Главное преимущество здесь в том, что нам не нужно создавать новый поток, поэтому нам не нужно делать много-много malloc, которые могли бы замедлить нас.

Магия, невидимая нарезка

Кажется загадочным, как оба вышеперечисленных подхода позволяют выполнять рабочую нагрузку «параллельно»? Ответ заключается в том, что база данных многопоточна. Таким образом, наше однопоточное приложение фактически использует многопоточное поведение другого процесса: базы данных.

Если однопоточный подход не работает

Однопотоковое приложение выйдет из строя, если вам нужно выполнить много вычислений ЦП перед возвратом данных. Я не имею в виду цикл for, обрабатывающий результат базы данных. Это все еще в основном O (n). Я имею в виду такие вещи, как преобразование Фурье (например, кодирование в формате mp3), трассировка лучей (3D-рендеринг) и т. Д.

Другой недостаток однопоточных приложений заключается в том, что они используют только одно ядро ​​ЦП. Поэтому, если у вас есть четырехъядерный сервер (что не редкость в наши дни), вы не используете остальные 3 ядра.

Если многопоточный подход не работает

Многопоточное приложение выйдет из строя, если вам нужно выделить много оперативной памяти для каждого потока. Во-первых, само использование ОЗУ означает, что вы не можете обрабатывать столько запросов, сколько однопоточное приложение. Хуже того, malloc работает медленно. Распределение большого количества объектов (что является обычным для современных веб-фреймворков) означает, что мы потенциально можем работать медленнее, чем однопоточные приложения. Здесь обычно побеждает node.js.

Один из вариантов использования, который в конечном итоге ухудшает многопоточность, - это когда вам нужно запустить другой язык сценариев в своем потоке. Сначала вам обычно нужно переназначить всю среду выполнения для этого языка, затем вам нужно переназначить переменные, используемые вашим скриптом.

Итак, если вы пишете сетевые приложения на C, go или java, накладные расходы на многопоточность обычно не будут такими уж большими. Если вы пишете веб-сервер C для обслуживания PHP или Ruby, тогда очень легко написать более быстрый сервер на javascript, Ruby или Python.

Гибридный подход

Некоторые веб-серверы используют гибридный подход. Например, Nginx и Apache2 реализуют свой код сетевой обработки как пул потоков для циклов событий. Каждый поток запускает цикл обработки событий, одновременно обрабатывая запросы в однопоточном режиме, но запросы с балансировкой нагрузки между несколькими потоками.

Некоторые однопоточные архитектуры также используют гибридный подход. Вместо запуска нескольких потоков из одного процесса вы можете запускать несколько приложений - например, 4 сервера node.js на четырехъядерном компьютере. Затем вы используете балансировщик нагрузки для распределения нагрузки между процессами.

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

Дополнение к ответу слебетмана: Когда вы говорите, что Node.JS может обрабатывать 10000 одновременных запросов, они по сути являются неблокирующими запросами, т.е. эти запросы в основном относятся к запросу к базе данных.

Внутри цикл событий из Node.JS обрабатывает пул потоков, где каждый поток обрабатывает неблокирующий запроси цикл событий продолжает прослушивать дополнительные запросы после делегирования работы одному из потоков пула потоков . Когда один из потоков завершает работу, он отправляет сигнал в цикл событий , что он завершен, он же обратный вызов.Цикл событий затем обработайте этот обратный вызов и отправьте ответ обратно.

Поскольку вы новичок в NodeJS, прочтите подробнее о nextTick, чтобы понять, как цикл событий работает внутри. Читайте блоги на http://javascriptissexy.com, они мне очень помогли, когда я начал с JavaScript / NodeJS.

Этапы обработки модели однопоточного цикла событий:

  • Clients Send request to Web Server.

  • Node JS Web Server internally maintains a Limited Thread pool to provide services to the Client Requests.

  • Node JS Web Server receives those requests and places them into a Queue. It is known as “Event Queue”.

  • Node JS Web Server internally has a Component, known as “Event Loop”. Why it got this name is that it uses indefinite loop to receive requests and process them.

  • Event Loop uses Single Thread only. It is main heart of Node JS Platform Processing Model.

  • Event Loop checks any Client Request is placed in Event Queue. If not then wait for incoming requests for indefinitely.

  • If yes, then pick up one Client Request from Event Queue

    1. Starts process that Client Request
    2. If that Client Request Does Not requires any Blocking IO Operations, then process everything, prepare response and send it back to client.
    3. If that Client Request requires some Blocking IO Operations like interacting with Database, File System, External Services then it will follow different approach
  • Checks Threads availability from Internal Thread Pool
  • Picks up one Thread and assign this Client Request to that thread.
  • That Thread is responsible for taking that request, process it, perform Blocking IO operations, prepare response and send it back to the Event Loop

    very nicely explained by @Rambabu Posa for more explanation go throw this Link

Я понимаю, что Node.js использует однопоточность и цикл событий для обрабатывать запросы обрабатываются только по одному (что не является блокирующим).

Я мог неправильно понять то, что вы здесь сказали, но «по одному» звучит так, будто вы не полностью понимаете архитектуру, основанную на событиях.

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

Например: вы получаете соединение от клиента, вы его принимаете, вы читаете заголовки запроса (в случае http), затем вы начинаете действовать по запросу. Вы можете прочитать тело запроса и, как правило, в конечном итоге отправляете некоторые данные обратно клиенту (это преднамеренное упрощение процедуры, чтобы продемонстрировать суть).

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

Когда состояние объекта ввода-вывода (например, сетевого соединения) изменяется так, что ему требуется обработка (например, данные получены в сокете, сокет становится доступным для записи и т. Д.), Основной JS-поток Node.js просыпается со списком товаров, которые необходимо обработать.

Он находит соответствующую структуру данных и генерирует какое-то событие в этой структуре, которое вызывает запуск обратных вызовов, которые обрабатывают входящие данные или записывают больше данных в сокет и т. Д. После того, как все объекты ввода-вывода нуждаются в обработки, основной поток JS Node.js будет снова ждать, пока не сообщит, что доступны дополнительные данные (или какая-то другая операция завершена или истекло время ожидания).

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

Важным моментом является то, что обработка разных запросов чередуется, он не обрабатывает один запрос от начала до конца, а затем переходит к следующему.

На мой взгляд, основным преимуществом этого является медленный запрос (например, вы пытаетесь отправить 1 МБ данных ответа на устройство мобильного телефона через соединение для передачи данных 2G, или вы выполняете очень медленный запрос к базе данных ) не будет блокировать более быстрые.

В обычном многопоточном веб-сервере у вас обычно будет поток для каждого обрабатываемого запроса, и он будет обрабатывать ТОЛЬКО этот запрос, пока он не будет завершен. Что произойдет, если у вас будет много медленных запросов? В итоге вы получаете множество ваших потоков, связанных с обработкой этих запросов, а другие запросы (которые могут быть очень простыми запросами, которые можно обрабатывать очень быстро) помещаются в очередь за ними.

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

• 100001

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

В то время как неблокирующая однопоточная система наилучшим образом использует свою однопоточную систему.

См. Схему ниже: enter image description here enter image description here

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

Внутренний пул потоков в nodeJs по умолчанию имеет всего 4 потока. и это не похоже на то, что весь запрос прикреплен к новому потоку из пула потоков, все выполнение запроса происходит так же, как любой обычный запрос (без какой-либо блокирующей задачи), просто всякий раз, когда запрос имеет долгую или тяжелую операцию, такую ​​как db при вызове, файловой операции или http-запросе задача ставится в очередь во внутренний пул потоков, предоставляемый libuv. И поскольку nodeJs предоставляет 4 потока во внутреннем пуле потоков по умолчанию, каждый 5-й или следующий параллельный запрос ожидает, пока поток не освободится, и после завершения этих операций обратный вызов помещается в очередь обратного вызова. и перехватывается циклом событий и отправляет ответ.

А вот и другая информация, что это не единственная очередь обратного вызова, есть много очередей.

  1. Очередь NextTick
  2. Очередь микрозадач
  3. Очередь таймеров
  4. очередь обратного вызова ввода-вывода (запросы, операции с файлами, операции с базами данных)
  5. Очередь опроса ввода-вывода
  6. Проверить очередь фазы или SetImmediate
  7. закрыть очередь обработчиков

Всякий раз, когда приходит запрос, код запускается в этом порядке очереди обратных вызовов.

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

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

Все ставится в очередь в зависимости от типа обратного вызова и обрабатывается в указанном выше порядке.

Вот хорошее объяснение из этой средней статьи:

Учитывая приложение NodeJS, поскольку Node является однопоточным, скажем, если обработка включает Promise.all, который занимает 8 секунд, означает ли это, что клиентский запрос, который приходит после этого запроса, должен ждать восемь секунд? Нет. Цикл обработки событий NodeJS является однопоточным. Вся серверная архитектура для NodeJS не является однопоточной.

Прежде чем перейти к архитектуре сервера Node, чтобы взглянуть на типичную модель ответа на многопоточный запрос, веб-сервер будет иметь несколько потоков, и когда параллельные запросы попадают на веб-сервер, веб-сервер выбирает threadOne из threadPool, а threadOne обрабатывает requestOne и отвечает на clientOne, и когда приходит второй запрос, веб-сервер берет второй поток из threadPool, берет requestTwo, обрабатывает его и отвечает clientTwo. threadOne отвечает за все виды операций, которые требует requestOne, включая выполнение любых блокирующих операций ввода-вывода.

The fact that the thread needs to wait for blocking IO operations is what makes it inefficient. With this kind of a model, the webserver is only able to serve as much requests as there are threads in the thread pool.

Веб-сервер NodeJS поддерживает ограниченный пул потоков для предоставления услуг по запросам клиентов. Несколько клиентов делают несколько запросов к серверу NodeJS. NodeJS получает эти запросы и помещает их в EventQueue. Сервер NodeJS имеет внутренний компонент, называемый EventLoop, который представляет собой бесконечный цикл, который принимает запросы и обрабатывает их. Этот EventLoop является однопоточным. Другими словами, EventLoop является слушателем для EventQueue. Итак, у нас есть очередь событий, в которую помещаются запросы, и цикл событий, который прослушивает эти запросы в очереди событий. Что произойдет дальше? Слушатель (цикл событий) обрабатывает запрос, и если он может обработать запрос без необходимости каких-либо блокирующих операций ввода-вывода, тогда цикл событий сам обработает запрос и сам отправит ответ обратно клиенту. Если текущий запрос использует блокирующие операции ввода-вывода, цикл обработки событий определяет, есть ли потоки, доступные в пуле потоков, выбирает один поток из пула потоков и назначает конкретный запрос выбранному потоку. Этот поток выполняет блокирующие операции ввода-вывода и отправляет ответ обратно в цикл событий, и как только ответ попадает в цикл событий, цикл событий отправляет ответ обратно клиенту.

Чем NodeJS лучше традиционной многопоточной модели ответа на запрос? В традиционной многопоточной модели запрос / ответ каждый клиент получает отдельный поток, тогда как, как и в случае с NodeJS, более простые запросы обрабатываются непосредственно EventLoop. Это оптимизация ресурсов пула потоков, и нет накладных расходов на создание потоков для каждого клиентского запроса.

Кажется, вы думаете, что большая часть обработки выполняется в цикле событий узла. Node фактически передает работу ввода-вывода потокам. Операции ввода-вывода обычно занимают на порядки больше времени, чем операции ЦП, так зачем же ЦП этого ждать? Кроме того, ОС уже очень хорошо справляется с задачами ввода-вывода. Фактически, поскольку Node не ждет, он обеспечивает гораздо более высокую загрузку ЦП.

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

2022 WebDevInsider