gpt4 book ai didi

javascript - 大量回调的 NodeJS 性能

转载 作者:行者123 更新时间:2023-12-04 22:30:27 25 4
gpt4 key购买 nike

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

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

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

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

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

最佳答案

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

Does the actual processing of callbacks depend on node's single thread being idle, or are callbacks processed in parallel as well as the main thread?



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

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

硬件

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

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

It was a great invention, but also a Box of Pandora - 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 来记住哪个程序拥有哪个套接字。

流程

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

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

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

回调处理

通过了解我们现在对 Node 如何处理网络 I/O 的了解,我们可以了解回调如何影响性能。
  • 等待数千个回调的 CPU 损失为零

    当然, Node 仍然需要在 RAM 中维护数据结构以跟踪所有回调,因此回调确实有内存损失。
  • 处理回调的返回值是在单个线程中完成的

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

  • 你提到:

    I see the performance of my 20 callbacks deteriorate from the first callback to the last one



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

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

    你还提到:

    until the callback of the function returns a value or an error



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

    您可以通过从图片中取出 Node 并使用命令行工具同时发送 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;
    }

    关于javascript - 大量回调的 NodeJS 性能,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/28961698/

    25 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com