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

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

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

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

Ответ 1

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

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% CPU, ожидая возвращения базы данных. При этом им пришлось выделять память, необходимую для потока, который включает в себя полностью отдельный стек программ для каждого потока и т.д. Кроме того, им нужно было бы начать поток, который, хотя и не так дорог, как запуск полного процесса, все еще не совсем дешево.

Цикл событий Singlethreaded

Поскольку мы тратим большую часть своего времени на использование 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, что бы замедлить нас.

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

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

Если сбой однопоточного подхода

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

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

В случае сбоя многопоточного подхода

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

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

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

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

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

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

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

Ответ 2

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

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

Ответ 3

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

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

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

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

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

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

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

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

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

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

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

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

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

Ответ 4

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

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

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

Ответ 5

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

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

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

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

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

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

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

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

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

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