Как однопоточный Node.js обрабатывает запросы одновременно? - PullRequest
0 голосов
/ 19 ноября 2018

Я сейчас глубоко изучаю платформу Nodejs.Как мы знаем, Nodejs является однопоточным, и если он выполняет операцию блокировки (например, fs.readFileSync), поток должен дождаться завершения этой операции.Я решил провести эксперимент: я создал сервер, который отвечает на каждый запрос огромным количеством данных из файла

const { createServer } = require('http');
const fs = require('fs');

const server = createServer();

server.on('request', (req, res) => {
    let data;
    data =fs.readFileSync('./big.file');
    res.end(data);
});

server.listen(8000);

Также я запустил 5 терминалов для параллельных запросов к серверу.Я ждал, чтобы увидеть, что пока обрабатывается один запрос, остальные должны дождаться завершения операции блокировки с первого запроса.Однако остальные 4 запроса были даны одновременно.Почему это происходит?

Ответы [ 2 ]

0 голосов
/ 19 ноября 2018

Возможно, это не связано напрямую с вашим вопросом, но я думаю, что это полезно,

Вы можете использовать поток вместо чтения полного файла в память, например:

const { createServer } = require('http');
const fs = require('fs');

const server = createServer();

server.on('request', (req, res) => {
   const readStream = fs.createReadStream('./big.file'); // Here we create the stream.
   readStream.pipe(res); // Here we pipe the readable stream to the res writeable stream.
});

server.listen(8000);

Смысл этого заключается в следующем:

  • выглядит лучше.
  • Вы не сохраняете полный файл в оперативной памяти.

Это работает лучше, поскольку не является блокирующим, а объект res уже является потоком, и это означает, что данные будут передаваться порциями.

Хорошо, так streams = chunked

Почему бы не прочитать куски из файла и отправить их в режиме реального времени вместо чтения действительно большого файла и разделить его на куски после?

Кроме того, почему это действительно важно на реальном производственном сервере?

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

1 запрос на файл 1 ГБ = 1 ГБ в оперативной памяти

2 запроса на файл 1 ГБ = 2 ГБ в оперативной памяти

и т.д.

Это явно не хорошо масштабируется, верно?

Streams позволяет отделить эти данные от текущего состояния функции (внутри этой области), поэтому в простых сроках их будет (с размером по умолчанию chunk 16kb):

1 запрос на файл 1 ГБ = 16 КБ в оперативной памяти

2 запроса на файл 1 ГБ = 32 КБ в оперативной памяти

и т.д.

Кроме того, ОС уже передает поток на узел (fs), поэтому он работает с потоками от начала до конца.

Надеюсь, это поможет: D.

PD: Никогда не используйте операции синхронизации (блокирование) внутри асинхронных операций (неблокирование).

0 голосов
/ 19 ноября 2018

То, что вы, вероятно, видите, является либо асинхронной частью реализации внутри res.end() для фактической отправки большого объема данных, либо вы видите, что все данные отправляются очень быстро и последовательно, но клиенты не могут обрабатывать его достаточно быстро, чтобы фактически показывать его последовательно, и поскольку каждый клиент находится в своем собственном отдельном процессе, они «появляются», чтобы показать, что он прибывает одновременно, только потому, что они слишком медленно реагируют, чтобы показать действительную последовательность поступления.

Нужно использовать сетевой сниффер, чтобы увидеть, какой из них на самом деле происходит, или запустить несколько разных тестов, или поместить некоторую запись в реализацию res.end(), или нажать на некоторую запись в стеке TCP клиента, чтобы определить фактический порядок. прибытия пакета среди различных запросов.


Если у вас один сервер и один обработчик запросов, выполняющий синхронный ввод-вывод, вы не сможете одновременно обрабатывать несколько запросов. Если вы считаете, что это происходит, то вам придется документировать, как именно вы это измерили или пришли к выводу (поэтому мы можем помочь вам разобраться в вашем недопонимании), потому что это не то, как работает node.js при использовании блокирующих, синхронных операций ввода-вывода, таких как как fs.readFileSync().

node.js запускает ваш JS как однопоточный, и когда вы используете блокировку синхронного ввода-вывода, он блокирует этот единственный поток Javascript. Вот почему вы никогда не должны использовать синхронный ввод-вывод на сервере, за исключением, возможно, кода запуска, который запускается только один раз при запуске.

Очевидно, что fs.readFileSync('./big.file') является синхронным, поэтому ваш второй запрос не будет запущен до тех пор, пока не будет выполнен первый fs.readFileSync(). И снова и снова вызывать его для одного и того же файла будет очень быстро (кэширование диска ОС).

Но, res.end(data) неблокирующий, асинхронный. res - это поток, и вы даете потоку некоторые данные для обработки. Он будет отправлять столько данных, сколько может, через сокет, но если он получает управление потоком по протоколу TCP, он приостанавливается до тех пор, пока в сокете не останется больше места для отправки. То, как много это произойдет, зависит от разных вещей на вашем компьютере, его конфигурации и сетевого подключения к клиенту.

Итак, что может происходить, так это последовательность событий:

  1. Первый запрос приходит и делает fs.readFileSync() и звонит res.end(data). Это начинает отправку данных клиенту, но возвращается, прежде чем это будет сделано из-за управления потоком TCP. Это отправляет node.js обратно в цикл обработки событий.

  2. Второй запрос приходит и делает fs.readFileSync() и звонит res.end(data). Это начинает отправку данных клиенту, но возвращается, прежде чем это будет сделано из-за управления потоком TCP. Это отправляет node.js обратно в цикл обработки событий.

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

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


Когда вы используете асинхронный ввод-вывод, такой как fs.readFile(), тогда правильно написанный Javascript-код node.js может иметь много запросов «в полете» одновременно.На самом деле они не работают одновременно в одно и то же время, но можно запустить, выполнить некоторую работу, запустить асинхронную операцию, а затем дать возможность запустить другой запрос.При правильно написанном асинхронном вводе / выводе из внешнего мира может возникать параллельная обработка, даже если это больше похоже на совместное использование одного потока, когда обработчик запросов ожидает завершения асинхронного запроса ввода / вывода.Но код сервера, который вы показываете, не является этим кооперативным асинхронным вводом-выводом.

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