Как Node.js обрабатывает 10,000 параллельных запросов?
Я понимаю, что Node.js использует однопоточную модель и цикл событий для обработки запросов, обрабатывая их по одному в ненаблюдающем режиме. Но как это работает, скажем, при 10,000 параллельных запросов? Цикл событий обработает все эти запросы? Разве это не займет слишком много времени?
Я пока не могу понять, как это может быть быстрее, чем многопоточный веб-сервер. Я понимаю, что многопоточный сервер будет более затратным с точки зрения ресурсов (память, процессор), но разве он не будет быстрее? Возможно, я ошибаюсь; пожалуйста, объясните, как этот однопоточный подход может быть быстрее при большом количестве запросов и что он обычно делает (в общих чертах) при обслуживании такого количества запросов, например, 10,000.
Кроме того, сможет ли этот однопоточный подход хорошо масштабироваться при таком большом количестве? Пожалуйста, имейте в виду, что я только начинаю изучать Node.js.
4 ответ(ов)
То, о чем вы, похоже, думаете, заключается в том, что большая часть обработки происходит в цикле событий Node. На самом деле Node передает работу по ввод/вывод в потоки. Операции ввода-вывода обычно занимают гораздо больше времени, чем операции ЦП, так зачем заставлять ЦП ждать? Кроме того, операционная система уже очень хорошо справляется с задачами ввода-вывода. На самом деле, благодаря тому, что Node не ждет, он достигает гораздо более высокой загрузки ЦП.
Для лучшего понимания можно провести аналогию: представьте NodeJS как официанта, принимающего заказы у клиентов, в то время как повара по вводу-выводу готовят блюда на кухне. В других системах есть несколько поваров, которые принимают заказ у клиента, готовят блюдо, убирают со стола и только затем обращаются к следующему клиенту.
Похоже, я могу не совсем правильно понять, о чем вы говорите, но фраза "один за раз" может указывать на то, что вы не до конца осознаете особенности событийно-ориентированной архитектуры.
В "традиционной" (не событийно-ориентированной) архитектуре приложения процесс часто просто ждет, пока что-то произойдет. В событийно-ориентированной архитектуре, такой как Node.js, процесс не остается в режиме ожидания, а может продолжать выполнять другую работу.
Например: вы получаете соединение от клиента, принимаете его, читаете заголовки запроса (в случае HTTP), а затем начинаете обрабатывать запрос. На этом этапе вы можете прочитать тело запроса и обычно в итоге отправите какие-то данные обратно клиенту (это упрощенное описание процесса, лишь для иллюстрации).
На каждом из этих этапов большую часть времени процесс проведет в ожидании данных с другой стороны - фактическое время обработки в основном потоке JS обычно минимально.
Когда состояние I/O объекта (например, сетевого соединения) изменяется таким образом, что требует обработки (например, данные получены на сокете, сокет стал доступным для записи и т.д.), основной поток Node.js активируется с списком элементов, требующих обработки.
Он находит соответствующую структуру данных и генерирует событие на этой структуре, что приводит к выполнению обратных вызовов, которые обрабатывают входящие данные или записывают больше данных в сокет и т.д. После обработки всех I/O объектов, требующих обработки, основной поток Node.js снова засыпает, ожидая, когда станет доступна новая информация (или завершится какая-либо другая операция).
В следующий раз, когда он пробуждается, это может быть связано с необходимостью обработки другого I/O объекта – например, другого сетевого соединения. Каждый раз выполняются соответствующие обратные вызовы, после чего поток снова засыпает, ожидая новых событий.
Важно отметить, что обработка различных запросов перекрывается, то есть она не выполняет один запрос от начала до конца, а затем переходит к следующему.
На мой взгляд, главное преимущество этого подхода в том, что медленный запрос (например, вы пытаетесь отправить 1 МБ данных ответа на мобильное устройство через 2G соединение или выполняете медленный запрос к базе данных) не блокирует более быстрые.
В традиционном многопоточном веб-сервере обычно есть поток для каждого обрабатываемого запроса, и он обрабатывает только этот запрос, пока не закончит. Что происходит, если у вас много медленных запросов? Вы получите множество потоков, которые ожидают завершения этих запросов, и другие, иногда очень простые и быстровыполимые запросы будут стоять в очереди за ними.
Существует много других событийно-ориентированных систем, помимо Node.js, и у них, как правило, схожие преимущества и недостатки по сравнению с традиционной моделью.
Я бы не стал утверждать, что событийно-ориентированные системы быстрее в каждой ситуации или при каждой загрузке - они, как правило, хорошо работают для задач, связанных с вводом-выводом, но не так хорошо для задач, ограниченных мощностью процессора.
Добавляя к ответу slebetman:
Когда вы говорите, что Node.JS
может обрабатывать 10 000 одновременных запросов, эти запросы, по сути, являются неблокирующими, т.е. они в основном касаются запросов к базе данных.
Внутри event loop
в Node.JS
управляет пулом потоков
, где каждый поток обрабатывает неблокирующий запрос
, а event loop
продолжает прослушивать новые запросы после делегирования работы одному из потоков пула. Когда один из потоков завершает работу, он отправляет сигнал в event loop
, что работа завершена, т.е. callback
. Затем event loop
обрабатывает этот callback и отправляет ответ обратно.
Так как вы новичок в Node.js, обязательно почитайте больше о nextTick
, чтобы понять, как работает event loop
внутри. Также рекомендую ознакомиться с блогами на http://javascriptissexy.com — они были действительно полезны для меня, когда я начинал изучать JavaScript и Node.js.
Добавляя к ответу slebetman для большей ясности о том, что происходит во время выполнения кода.
Внутренний пул потоков в Node.js по умолчанию содержит всего 4 потока. Это не значит, что на каждый запрос создаётся новый поток из пула – выполнение запроса происходит так же, как и с любым другим обычным запросом (без блокирующих задач). Однако, когда запрос включает долгую или ресурсоёмкую операцию, такую как вызов к базе данных, работа с файлами или HTTP-запрос, задача ставится в очередь внутреннего пула потоков, предоставленного библиотекой libuv. Поскольку Node.js по умолчанию предоставляет 4 потока в внутреннем пуле, каждый пятый или следующий параллельно приходящий запрос будет ожидать, пока не освободится поток. Как только эти операции завершены, колбек помещается в очередь колбеков и обрабатывается циклом событий, который возвращает ответ.
Теперь добавим, что это не одна единственная очередь колбеков, а несколько:
- Очередь NextTick
- Очередь микротасков
- Очередь таймеров
- Очередь IO-колбеков (запросы, операции с файлами, операции с БД)
- Очередь IO Poll
- Очередь Check Phase или SetImmediate
- Очередь обработчиков закрытия
Когда приходит запрос, код выполняется в порядке очереди колбеков.
Не следует думать, что при наличии блокирующего запроса он прикрепляется к новому потоку. По умолчанию есть всего 4 потока, поэтому происходит дополнительная очередь.
Когда в коде происходит блокирующая операция, такая как чтение файла, вызывается функция, которая использует поток из пула, и по завершении операции колбек передаётся в соответствующую очередь, а затем выполняется в указанном порядке.
Все колбеки помещаются в очередь на основе их типа и обрабатываются в порядке, упомянутом выше.
Как предотвратить установку "devDependencies" модулей NPM для Node.js (package.json)?
Ошибка "npm WARN package.json: Нет поля repository"
Как исправить ошибку "ReferenceError: primordials is not defined" в Node.js
Как протестировать один файл с помощью Jest?
nvm постоянно "забывает" Node.js в новой сессии терминала