Как вообще Node.js обрабатывает 10 000 одновременных запросов?


395

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

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

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


5
Потому что большая часть работы (перемещение данных) не связана с процессором.
OrangeDog

5
Также обратите внимание, что только то, что Javascript выполняет только один поток, не означает, что не так много других потоков работают.
OrangeDog

Этот вопрос либо слишком широкий, либо дублирует другие вопросы.
OrangeDog


Наряду с однопоточностью Node.js делает то, что называется «неблокирующим вводом-выводом». Вот где все волшебство сделано
Ананд Н

Ответы:


765

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

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% CPU, почему бы не запустить некоторый код, когда мы не используем CPU? Таким образом, каждый запрос все равно будет занимать столько же процессорного времени, что и многопоточные приложения, но нам не нужно запускать поток. Итак, мы делаем это:

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, которые замедляют нас.

Волшебная, невидимая нить

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

Где однопоточный подход терпит неудачу

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

Еще одна ловушка однопоточных приложений заключается в том, что они будут использовать только одно ядро ​​процессора. Так что, если у вас есть четырехъядерный сервер (в наши дни это не редкость), вы не используете остальные 3 ядра.

Где многопоточный подход терпит неудачу

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

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

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

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

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

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

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


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

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

3
@CaspainCaldion Это зависит от того, что вы подразумеваете под очень быстрым и большим количеством клиентов. Таким образом, node.js может обрабатывать более 1000 запросов в секунду, а скорость ограничивается только скоростью вашей сетевой карты. Обратите внимание, что это 1000 запросов в секунду, а не клиенты, подключенные одновременно. Он может обрабатывать 10000 одновременных клиентов без проблем. Настоящим узким местом является сетевая карта.
Slebetman

1
@ Slebetman, лучшее объяснение когда-либо. Хотя, если у меня есть алгоритм машинного обучения, который обрабатывает некоторую информацию и, соответственно, выдает результаты, должен ли я использовать многопоточный подход или однопоточный
Ганеш Каревад

5
@GaneshKarewad Алгоритмы используют процессор, службы (база данных, REST API и т. Д.) Используют ввод / вывод. Если AI - это алгоритм, написанный на js, вы должны запустить его в другом потоке или процессе. Если AI - это сервис, работающий на другом компьютере (например, сервисы Amazon, Google или IBM AI), тогда используйте однопоточную архитектуру.
slebetman

46

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

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


5
Спасибо за аналогию с рестораном! Я нахожу аналогии и примеры из реальной жизни намного проще для изучения.
LaVache

13

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

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

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

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

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

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

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

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

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

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

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

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

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


12

Шаги обработки модели однопотокового цикла:

  • Клиенты Отправить запрос на веб-сервер.

  • Узел JS Web Server внутренне поддерживает пул ограниченных потоков для предоставления услуг клиентским запросам.

  • Узел JS Web Server получает эти запросы и помещает их в очередь. Он известен как «Очередь событий».

  • Внутренний узел JS Web Server имеет компонент, известный как «цикл обработки событий». Почему он получил это имя, потому что он использует неопределенный цикл для получения запросов и их обработки.

  • В цикле событий используется только один поток. Это главное сердце модели обработки платформы Node JS.

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

  • Если да, то получите один клиентский запрос из очереди событий

    1. Начинает обрабатывать этот запрос клиента
    2. Если этот запрос клиента не требует блокирования операций ввода-вывода, обработайте все, подготовьте ответ и отправьте его обратно клиенту.
    3. Если этот клиентский запрос требует каких-либо блокирующих операций ввода-вывода, таких как взаимодействие с базой данных, файловой системой, внешними службами, тогда он будет использовать другой подход
  • Проверяет доступность потоков из внутреннего пула потоков
  • Получает один поток и назначает этот запрос клиента этому потоку.
  • Этот поток отвечает за принятие этого запроса, его обработку, выполнение операций блокирования ввода-вывода, подготовку ответа и отправку его обратно в цикл обработки событий.

    очень приятно объяснил @Rambabu Posa для более подробного объяснения иди киньте эту ссылку


диаграмма, приведенная в этом сообщении в блоге, кажется неправильной, что они упоминали в этой статье, не совсем правильно.
rranj

11

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

Внутренне event loopиз Node.JSперекачивают thread pool, где каждая нить обрабатывает non-blocking requestи цикл событий продолжает слушать больше после запроса делегирования работы одной нити из thread pool. Когда один из потоков завершает работу, он посылает сигнал тому, event loopчто он закончил, иначе callback. Event loopзатем обработайте этот обратный вызов и отправьте ответ обратно.

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


2

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

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

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

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

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

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

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

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

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.