繁体   English   中英

大量回调的 NodeJS 性能

[英]Performance of NodeJS with large amount of callbacks

我正在开发一个 NodeJS 应用程序。 有一个特定的 RESTful API (GET),当由用户触发时,它需要服务器执行大约 10-20 次网络操作才能从不同来源提取信息。 所有这些网络操作都是异步回调,一旦它们全部完成,结果由 nodejs 应用程序合并并发送回客户端。 所有这些操作都是通过 async.map 函数并行启动的。

我只是想了解,由于 nodejs 是单线程的,并且它不使用多核机器(至少不是没有集群),当它有许多回调要处理时,节点如何扩展? 回调的实际处理是否取决于节点的单线程空闲,还是回调与主线程并行处理?

我问的原因是,我看到我的 20 个回调从第一个回调到最后一个回调的性能下降。 例如,第一个网络操作(10-20 个中)需要 141 毫秒才能完成,而最后一个需要大约 4 秒(以函数执行到函数回调返回值或一个错误)。 它们都是针对同一个数据源的同一个网络操作,所以数据源不是瓶颈)。 我知道数据源响应单个请求的时间不超过 200 毫秒。

我找到了这个线程,所以在我看来,一个线程需要解决所有回调和即将出现的新请求。

所以我的问题是,对于会触发许多回调的操作,优化其性能的最佳实践是什么?

对于网络操作,node.js 实际上是单线程的。 然而,一直存在一种误解,即处理 I/O 需要恒定的 CPU 资源。 你的问题的核心归结为:

回调的实际处理是否取决于节点的单线程空闲,还是回调与主线程并行处理?

答案是肯定的和否定的。 是的,回调仅在主线程空闲时执行。 不,线程空闲时不进行“处理”。 具体来说:没有“处理”——如果您所说的“处理”正在等待,节点“处理”数千个回调所需的 CPU 时间为零。

异步 I/O 如何工作(在任何编程语言中)

硬件

如果我们真的需要了解节点(或浏览器)内部是如何工作的,不幸的是,我们必须首先了解计算机是如何工作的——从硬件到操作系统。 是的,这将是一次深潜,所以请耐心等待。

这一切都始于中断的发明..

这是一个伟大的发明,也是一盒潘多拉 - Edsger Dijkstra

是的,上面的引用来自同一个“Goto 被认为是有害的”Dijkstra。 从一开始,将异步操作引入计算机硬件就被认为是一个非常困难的话题,即使对于行业中的一些传奇人物也是如此。

引入中断是为了加速 I/O 操作。 硬件不需要用软件轮询某些输入(从有用的工作中占用 CPU 时间),硬件会向 CPU 发送一个信号,告诉它一个事件发生了。 然后 CPU 将挂起当前运行的程序并执行另一个程序来处理中断——因此我们称这些函数为中断处理程序。 “处理程序”这个词一直在堆栈中一直停留在调用回调函数“事件处理程序”的 GUI 库中。

如果您一直在注意,您会注意到中断处理程序的这个概念实际上是一个回调 将 CPU 配置为在稍后发生事件时调用函数。 所以即使是回调也不是一个新概念——它比 C 更古老。

操作系统

中断使现代操作系统成为可能。 如果没有中断,CPU 将无法暂时停止您的程序运行操作系统(好吧,有协作多任务处理,但现在让我们忽略它)。 操作系统的工作原理是它在 CPU 中设置一个硬件定时器来触发中断,然后它告诉 CPU 执行您的程序。 正是这种周期性的定时器中断运行您的操作系统。 除了定时器,操作系统(或者更确切地说是设备驱动程序)为 I/O 设置中断。 当 I/O 事件发生时,操作系统将接管您的 CPU(或多核系统中的一个 CPU)并检查它的数据结构,它接下来需要执行哪个进程来处理 I/O(这称为抢占式多任务处理)。

因此,处理网络连接甚至不是操作系统的工作——操作系统只是在其数据结构(或者更确切地说,网络堆栈)中跟踪连接。 真正处理网络 I/O 的是您的网卡、路由器、调制解调器、ISP 等。因此等待 I/O 占用的 CPU 资源为零。 它只是占用一些 RAM 来记住哪个程序拥有哪个套接字。

流程

现在我们已经清楚地了解了这一点,我们可以理解该节点的作用。 各种操作系统都有各种不同的 API 来提供异步 I/O - 从 Windows 上的重叠 I/O 到 Linux 上的 poll/epoll 到 BSD 上的 kqueue 到跨平台select() Node 在内部使用 libuv 作为对这些 API 的高级抽象。

尽管细节不同,但这些 API 的工作方式相似。 本质上,它们提供了一个函数,当调用该函数时将阻塞您的线程,直到操作系统向其发送事件。 所以是的,即使是非阻塞 I/O 也会阻塞你的线程。 这里的关键是阻塞 I/O 会在多个地方阻塞你的线程,但非阻塞 I/O 只会在一个地方阻塞你的线程——在那里你等待事件。

这允许您以面向事件的方式设计您的程序。 这类似于中断允许操作系统设计人员实现多任务处理。 实际上,异步 I/O 之于框架就像中断之于操作系统一样。 它允许节点花费恰好 0% 的 CPU 时间来处理(等待)I/O。 这就是使 node 快速的原因——它并不是真的更快,但不会浪费时间等待。

回调处理

通过了解我们现在对节点如何处理网络 I/O 的了解,我们可以了解回调如何影响性能。

  1. 等待数千个回调的 CPU 损失为零

    当然,节点仍然需要在 RAM 中维护数据结构以跟踪所有回调,因此回调确实有内存损失。

  2. 处理回调的返回值是在单个线程中完成的

    这有一些优点和一些缺点。 这意味着节点不必担心竞争条件,因此节点不会在内部使用任何信号量或互斥锁来保护数据访问。 缺点是任何 CPU 密集型 javascript 都会阻止所有其他操作。

你提到:

我看到我的 20 次回调的性能从第一次回调到最后一次下降

回调都是在主线程中顺序同步执行的(实际上只有等待是并行完成的)。 因此,您的回调可能正在执行一些 CPU 密集型计算,并且所有回调的总执行时间实际上是 4 秒。

但是,对于这么多的回调,我很少看到这种问题。 仍然有可能,我仍然不知道您在回调中在做什么。 我只是觉得不太可能。

你还提到:

直到函数的回调返回值或错误

一种可能的解释是您的网络资源无法处理那么多同时连接。 您可能认为这并不多,因为它只有 20 个连接,但我见过很多服务会以 10 个请求/秒的速度崩溃。 问题是所有 20 个请求都是同时发生的。

您可以通过从图片中取出节点并使用命令行工具同时发送 20 个请求来测试这一点。 curlwget类的东西:

# assuming you're running bash:
for x in `seq 1 20`;do curl -o /dev/null -w "Connect: %{time_connect} Start: %{time_starttransfer} Total: %{time_total} \n" http://example.com & done

减轻

如果事实证明问题是同时执行 20 个请求是在强调其他服务,那么您可以做的是限制同时请求的数量。

您可以通过批处理请求来做到这一点:

async function () {
    let input = [/* some values we need to process */];
    let result = [];

    while (input.length) {
        let batch = input.splice(0,3); // make 3 requests in parallel

        let batchResult = await Promise.all(batch.map(x => {
            return fetchNetworkResource(x);
        }));

        result = result.concat(batchResult);
    }
    return result;
}

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM