【问题标题】:Performance of NodeJS with large amount of callbacks具有大量回调的 NodeJS 的性能
【发布时间】:2015-05-11 18:31:08
【问题描述】:

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

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

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

我找到了这个thread,所以在我看来,一个线程需要处理所有回调和新的请求。

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

【问题讨论】:

  • 最好的做法是信任nodeJs单线程架构,它是用来和回调一起使用的,不管有多少都使用它们
  • 您使用什么库来进行 http 调用?而且,您是否将 http 代理的 maxSockets 设置为适当的值?见stackoverflow.com/questions/16472497/…
  • 我使用的是数据库的sdk,所以网络请求由这个库管理。我的应用在 express 上运行,是否有一个全局设置来告诉 express 或 nodejs 不要限制对网络的出站呼叫量?
  • 所以没有http调用(“网络操作”)?
  • @Tobi 没有使用 http/https 模块,网络操作由数据库库在后台处理,它自然使用异步模式(错误,结果回调)将结果返回给我应用程序。

标签: javascript node.js callback


【解决方案1】:

对于网络操作,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。这就是节点快速的原因 - 它并不是真的更快,但不会浪费时间等待。

回调处理

通过我们现在对节点如何处理网络 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;
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2013-05-09
    • 2020-03-09
    • 1970-01-01
    • 1970-01-01
    • 2021-08-19
    • 2013-07-28
    • 1970-01-01
    • 2011-01-04
    相关资源
    最近更新 更多