具有大量回调的NodeJS的性能

时间:2015-03-10 10:47:57

标签: javascript node.js callback

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

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

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

我找到了这个thread,所以在我看来,单个线程需要解决所有回调和新请求。

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

1 个答案:

答案 0 :(得分:6)

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

  

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

答案是是和不是。是的,仅在主线程空闲时才执行回调。不,线程空闲时不执行“处理”。具体来说:没有“处理”-如果您正在等待“处理”的意思,则节点花费零CPU时间来“处理”数千个回调。

异步I / O的工作方式(使用任何编程语言)

硬件

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

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

  

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

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

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

如果您一直在关注,您会发现中断处理程序的概念实际上是回调。您将CPU配置为在事件发生后的某个时间调用函数。因此,即使回调也不是一个新概念-它比C还要早。

操作系统

中断使现代操作系统成为可能。没有中断,CPU将无法暂时停止程序以运行OS(嗯,这是协作式多任务处理,但现在暂时忽略它)。操作系统的工作方式是在CPU中设置硬件计时器以触发中断,然后告诉CPU执行程序。运行您的操作系统的正是此定时定时器中断。除了计时器外,OS(或更确切地说是设备驱动程序)还为I / O设置了中断。发生I / O事件时,操作系统将接管您的CPU(或多核系统中的一个CPU),并检查其数据结构以处理下一步处理I / O的过程(这称为抢先式多任务处理。

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

进程

现在,我们对此有了清晰的了解,我们可以了解该节点的作用。各种操作系统具有提供异步I / O的各种不同API-从Windows上的重叠I / O到Linux上的轮询/轮询,再到BSD上的队列到跨平台select()。 Node在内部将libuv用作这些API的高级抽象。

这些API的工作方式相似,但细节有所不同。本质上,它们提供了一个函数,当调用该函数时,它将阻塞您的线程,直到OS向其发送事件为止。因此,是的,即使非阻塞I / O也会阻塞您的线程。这里的关键是阻塞I / O将在多个地方阻塞您的线程,但非阻塞I / O只会在一个地方(您等待事件的地方)阻塞您的线程。

这允许您执行的操作是以面向事件的方式设计程序。这类似于中断使OS设计人员实现多任务处理的方式。实际上,异步I / O对框架来说是对OS的中断。它使节点可以花费恰好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;
}