【问题标题】:How does single-threaded Node.js handles requests concurrently?单线程 Node.js 如何并发处理请求?
【发布时间】:2018-11-19 07:03:32
【问题描述】:

我目前正在深入学习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 个请求同时响应。为什么会出现这种行为?

【问题讨论】:

  • 你能告诉我们你到底看到了什么吗?
  • 我想文件在第一次被读取时就被缓存了。不知道测试条件是什么。正如您已经注意到的,阻塞操作不能有并发请求。

标签: javascript node.js single-threaded


【解决方案1】:

您可能会看到res.end() 内部实现的一些异步部分来实际发送大量数据,或者您看到所有数据都非常快速且连续地发送,但客户端不能处理它的速度足够快以实际连续显示它,并且因为客户端每个都在他们自己的单独进程中,它们“似乎”显示它同时到达只是因为它们反应太慢而无法显示实际到达顺序。

必须使用网络嗅探器来查看其中哪些实际发生或运行一些不同的测试或在res.end() 的实现中放置一些日志记录,或在客户端的 TCP 堆栈中挖掘一些日志记录以确定实际顺序不同请求之间的数据包到达率。


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

node.js 将您的 JS 作为单线程运行,当您使用阻塞、同步 I/O 时,它会阻塞 Javascript 的一个单线程。这就是为什么您永远不应该在服务器中使用同步 I/O,除非在启动期间只运行一次的启动代码中。

很明显,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. 1234563如果它确实为这些事件提供服务,它可以(从客户端的角度)呈现不同请求的真正并发性)。

此外,客户端可能会导致它显示为已排序。每个客户端都在读取不同的缓冲套接字,如果它们都在不同的终端中,那么它们是多任务的。因此,如果每个客户端的套接字上的数据多于它可以立即读取和显示的数据(可能就是这种情况),那么每个客户端将读取一些、显示一些、读取更多、显示更多等等......如果在您的服务器上发送每个客户端的响应之间的延迟小于在客户端上读取和显示的延迟,那么客户端(每个客户端都在自己的单独进程中)能够同时运行。


当您使用异步 I/O(例如 fs.readFile())时,正确编写的 node.js Javascript 代码可以同时“进行”许多请求。它们实际上并不会在完全相同的时间同时运行,但可以运行,做一些工作,启动异步操作,然后让位于让另一个请求运行。使用正确编写的异步 I/O,可以从外部世界看到并发处理,即使它更类似于在请求处理程序等待异步 I/O 请求完成时共享单个线程。但是,您展示的服务器代码不是这种协作的异步 I/O。

【讨论】:

  • 你好。谢谢您的回答。我正在使用 CURL 来发出请求。当我在终端中发出请求时,文件的内容会立即出现在那里。
  • 不确定,res.end(data); 是否可以将一个大块写入一个流,然后在许多小块中异步读取,从而在客户端上显示所有响应都是并行提供的?
  • @longroad - 好的,正如 Bergi 提到的,res.end() 是异步的,因此在发送大量数据时可能会涉及一些异步行为,从而导致剩余数据量(在发送第一个块之后对于每个请求)要交错。这都是 res.end() 实现的内部,而不是交错你的实际 Javascript。看看我在答案末尾添加了什么。
  • @longroad - 此外,客户端处于不同的进程中,可能无法尽快显示传入数据。因此,他们的内部缓冲区备份,他们都试图同时显示它。因此,即使服务器连续发送它们,客户端也可能无法以足够快的速度显示它们以实际跟上,因此客户端最终看起来他们的数据是同时到达的,但实际上它是串行到达的,它们只是同时显示它。
  • @Bergi - 是的,这是可能的。 res.end() 可以得到流量控制,并且可能需要一些时间来实际发送所有数据(而其他传入的请求正在循环运行)。或者,这可能都是因为处于不同进程中的客户端处理和显示传入数据的速度不够快,无法实际直观地显示串行到达的数据。可以通过网络嗅探器确定它是哪一个(或两者的某种组合)(如果您可以通过查看它来避免干扰网络 - Heisenberg)。
【解决方案2】:

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

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

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 in ram

2 个 1gb 文件的请求 = 2gb in ram

这显然不能很好地扩展,对吧?

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

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

2 次请求 1gb 文件 = 32kb in ram

而且,操作系统已经将流传递给节点 (fs),因此它可以端到端地处理流。

希望对您有所帮助:D。

PD:永远不要在异步操作(非阻塞)中使用同步操作(阻塞)。

【讨论】:

  • 感谢您的评论!我已经知道 nodejs 中的流。我的示例仅用于演示目的,这就是我没有使用它们的原因。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-04-23
  • 2016-11-05
  • 2016-02-11
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多