单线程Node.js如何同时处理请求?

时间:2018-11-19 07:03:33

标签: javascript node.js single-threaded

我目前正在深入学习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 :(得分:3)

您可能看到的是res.end()内实现的一些异步部分,以实际发送大量数据,或者您看到所有数据都非常快速且串行地发送,但是客户端可以它处理的速度不够快,无法真正按顺序显示,而且由于每个客户都在各自独立的进程中,因此它们“出现”以显示并发到达,只是因为它们反应太慢而无法显示实际到达顺序。

一个人将不得不使用网络嗅探器来查看其中的实际情况,或者运行一些不同的测试,或者将一些日志记录放入res.end()的实现中,或者利用客户端TCP堆栈中的某些日志记录来确定数据包到达不同请求之间的实际顺序。


如果您有一台服务器,并且有一台正在执行同步I / O的请求处理程序,那么您将不会同时获得多个请求进程。如果您认为这种情况正在发生,那么您将必须准确记录如何衡量或得出结论(以便我们可以帮助您消除误解),因为在使用阻塞式同步I / O时,node.js的工作方式并不如此。为fs.readFileSync()

node.js以单线程方式运行JS,并且当您使用阻塞式同步I / O时,它将阻塞Javascript的一个线程。这就是为什么永远不要在服务器中使用同步I / O的原因,除非在启动代码中仅在启动期间运行一次。

很明显,fs.readFileSync('./big.file')是同步的,因此直到第一个fs.readFileSync()完成后,您的第二个请求才会开始处理。而且,一次又一次地在同一文件上调用它会非常快(OS磁盘缓存)。

但是,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()之类的异步I / O时,正确编写的node.js Javascript代码可以同时“运行”许多请求。它们实际上并不能在完全相同的时间并发运行,但是可以运行,执行一些工作,启动异步操作,然后让路让另一个请求运行。使用正确编写的异步I / O,即使在请求处理程序等待异步I / O请求完成时,它看起来更像是共享单个线程,这在外部处理中可能会出现并发处理的外观。但是,您显示的服务器代码不是这种协作的异步I / O。

答案 1 :(得分:3)

也许与您的问题没有直接关系,但是我认为这很有用,

您可以使用流而不是将整个文件读入内存,例如:

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);

这样做的重点是:

  • 看起来更好。
  • 您不将完整文件存储在RAM中。

这是更好的方法,因为它是非阻塞的,并且res对象已经是一个流,这意味着数据将按块传输。

好的,streams = chunked

为什么不从文件中读取大块并实时发送它们,而不是读取一个很大的文件然后将其分成大块?

为什么在真正的生产服务器上真的很重要?

由于每次收到请求时,您的代码都会将该大文件添加到ram中,并且该添加是并发的,因此您希望同时提供多个文件,因此让我们来做最高级的数学受过不良的教育允许:

1个1gb文件的请求= 1gb的内存

2个请求1GB文件= 2GB RAM

这显然不能很好地扩展吗?

Streams允许将数据与函数的当前状态(在该范围内)解耦,因此简单来说就是(默认chunk大小为16kb):

1个1gb文件的请求= ram中的16kb

2个请求,用于1gb文件= 32kb内存

此外,操作系统已经将流传递到节点(fs),因此它可以端对端地使用流。

希望它可以帮助:D。

PD:请不要在异步操作(非阻止)中使用同步操作(阻止)。