Promise.all() 与等待

Posted

技术标签:

【中文标题】Promise.all() 与等待【英文标题】:Promise.all() vs await 【发布时间】:2020-11-01 20:55:32 【问题描述】:

我正在尝试了解 node.js 单线程架构和事件循环,以使我们的应用程序更高效。所以考虑这种情况,我必须为一个 http api 调用进行几个数据库调用。我可以使用Promise.all() 或使用单独的await

示例:

使用异步/等待

await inserToTable1();
await insertToTable2();
await updateTable3();

使用Promise.all() 我也可以这样做

await Promise.all[inserToTable1(), insertToTable2(), updateTable3()]

这里对于给定时间的一个 API 命中,Promise.all() 会更快地返回响应,因为它会并行触发数据库调用。但是,如果我每秒有 1000 次 API 命中,会有什么不同吗?对于这种情况,Promise.all() 是否更适合事件循环?

更新 假设以下, 1000 次 API 命中是指应用程序的总流量。考虑有 20-25 个 API。其中一些可能会执行 DB 操作,一些可能会进行一些 http 调用等。此外,我们永远不会达到 DB 池的最大连接数。

提前致谢!!

【问题讨论】:

Promise.all 如果只有一个 Promise 捕获,就会捕获。 使用 Promise.all([]),您需要获取数据库的可用连接数或 poolSize。如果数据库请求的数量超过该数量,您将限制并发。 按照我的理解,您的数据库将成为决定性因素。使用Promise.all() 和 1000 次点击,您将看到同时向数据库发出的最大3000 查询,但是,如果您按顺序使用async/await,则向数据库发出的最大1000 查询同时给出 1000 hits/second 假设。而且我认为您应该重视@StevenLu 所说的话,您的节点实例将有一个N 连接的连接池,这将是一个瓶颈。 如果知道这件事真的很重要,那么您必须设计一个合适的测试工具和 MEASURE。在您衡量之前,没有多少教皇可以知道。 如果您运行的 CPU 密集型并行查询多于 mysql 数据库中的内核数量,则更多并行化的结果可能会更糟。如果您使用的 I/O 多于磁盘的容量,则顺序 可能会更好。没有一般规则。就像其他人说的那样,测量 =) 【参考方案1】:

像往常一样,在系统设计方面,答案是:视情况而定

有很多因素决定了两者的性能。通常,等待单个 Promise.all() 会并行等待所有请求。

事件循环

事件循环恰好使用 0% 的 CPU 时间来等待请求。请参阅我对这个相关问题的回答,了解事件循环的工作原理:Performance of NodeJS with large amount of callbacks

所以从事件循环的角度来看,顺序请求和使用Promise.all() 并行请求之间没有真正的区别。因此,如果这是您问题的核心,我猜答案是两者没有区别

但是,处理回调确实需要 CPU 时间。同样,完成执行所有回调的时间是相同的。所以再次从 CPU 性能的角度来看两者没有区别

并行发出请求确实会减少总体执行时间。首先,如果服务是多线程的,那么您实际上是通过发出并行请求来使用它的多线程性。这就是 node.js 快速的原因,即使它是单线程的。

即使您请求的服务不是多线程的并且实际上是按顺序处理请求,或者您请求的服务器是单核 CPU(现在很少见,但您仍然可以租用单核虚拟机)然后并行请求减少了网络开销,因为您的操作系统可以在单个以太网帧中发送多个请求,从而将数据包标头的开销分摊到多个请求上。但是,在超过大约六个并行请求之后,这确实会产生递减的收益。

一千个请求

您假设发出 1000 个请求。天气或不等待 1000 个并行承诺实际上会导致并行请求,这取决于 API 在网络级别的工作方式。

连接池。

很多数据库都实现了连接池。也就是说,该库将打开一些与数据库的连接,例如 5 个,并重用这些连接。

在某些实现中,通过此类库发出 1000 个请求将导致该库的低级网络代码一次批处理 5 个请求。这意味着您最多可以有 5 个并行请求(假设池中有 5 个连接)。在这种情况下,发出 1000 个并行请求是完全安全的。

然而,一些实现有一个可增长的连接池。在这样的实现中,发出 1000 个并行请求将导致您的软件打开 1000 个套接字来访问远程资源。在这种情况下,发出 1000 个并行请求的安全程度将取决于远程服务器允许这样做的天气。

连接限制。

Mysql 和 Postgresql 等大多数数据库都允许管理员配置连接限制,例如 5,这样数据库将拒绝超过每个 IP 地址限制的连接数。如果您使用的库不会自动管理与数据库的最大连接数,那么您的数据库将接受前 5 个请求并拒绝剩余的请求,直到另一个插槽可用(可能在 node.js 完成打开第 1000 个套接字之前释放了一个连接)。在这种情况下,您无法成功发出 1000 个并行请求 - 您需要管理您发出的并行请求数量。

某些 API 服务还会限制您可以并行建立的连接数。例如,谷歌地图将您限制为每秒 500 个请求。因此等待 1000 个并行请求将导致 50% 的请求失败,并可能导致您的 API 密钥或 IP 地址被禁止。

网络限制。

您的机器或服务器可以打开的套接字数量存在理论上的限制。不过这个数字非常高,不值得在这里讨论。

但是,当前存在的所有操作系统都会限制打开套接字的最大数量。在 Linux(例如 Ubuntu 和 android)和 Unix(例如 MacOSX 和 ios)上,套接字被实现为文件描述符。每个进程分配的文件描述符的最大数量。

对于 Linux,此数字通常默认为 1024 个文件。请注意,一个进程默认打开 3 个文件描述符:stdin、stdout 和 stderr。剩下 1021 个文件描述符由文件和套接字共享。因此,您的 1000 个并行请求非常接近这个数字,如果两个客户端尝试同时发出 1000 个并行请求,则可能会失败。

这个数字可以增加,但它有一个硬性限制。当前您可以在 Linux 上配置的最大文件描述符数为 590432。但是,这种极端配置只能在没有运行守护程序(或其他后台程序)的单用户系统上正常工作。

怎么办?

编写网络代码时的第一条规则是尽量不要破坏网络。您在任何时候提出的请求数量要合理。您可以将请求批处理到服务期望的限制。

使用 async/await 很容易。你可以这样做:

let parallel_requests = 10;

while (one_thousand_requests.length > 0) 
    let batch = [];

    for (let i=0;i<parallel_requests;i++) 
        let req = one_thousand_requests.pop();
        if (req) 
            batch.push(req());
        
    

    await Promise.all(batch);

通常,您可以并行提出的请求越多,整体处理时间就会越好(越短)。我想这就是你想听到的。但是您需要在并行性与上述因素之间取得平衡。 5 一般是可以的。 10个也许。 100 取决于响应请求的服务器。 1000 或更多,安装服务器的管理员可能需要调整他的操作系统。

【讨论】:

我不喜欢这种方式,因为可以说 parallel_requests 是 10 个请求。前 9 个请求将花费 100 毫秒。最后一个请求将花费 500 毫秒。为什么要为 10 个请求等待 500 毫秒? @AhmedElMetwally 因为这是一个简单的实现。一个更复杂的实现会在每次旧请求完成时插入一个新请求,但并不容易遵循,这意味着您不能使用Promise.all()。其实我之前在***上写过这样的批处理代码。请参阅我对这个问题的回答:***.com/questions/13250746/…。这是一个旧答案,因此基于回调,但我将其转换为承诺作为作业【参考方案2】:

await 方法将暂停每个await 调用的函数执行并按顺序执行它们,而Promise.all 可以并行(异步)执行并在所有调用都成功时返回成功。

所以如果你的三个(inserToTable1()insertToTable2()table3())方法是独立的,最好使用Promise.all

通过事件循环和调用堆栈实现了 javascript 在通过挂起发生繁重操作时执行其他内容的能力。

事件循环

调用者与响应的分离允许 JavaScript 运行时执行其他操作,同时等待您的异步操作完成并触发它们的回调。

JavaScript 运行时包含一个消息队列,其中存储要处理的消息列表及其相关的回调函数。在提供回调函数的情况下,这些消息排队以响应外部事件(例如单击鼠标或接收对 HTTP 请求的响应)。

事件循环有一项简单的工作——监控调用堆栈和回调队列。如果调用堆栈为空,它将从队列中取出第一个事件并将其推送到调用堆栈,调用堆栈有效地运行它。

【讨论】:

是的。我明白那个。但我的实际问题是,如果有这么多并发请求,是否会对整体性能有任何影响? @PraveenKumar 您的服务可以并行执行的操作越多,与顺序版本的差异就越大。或者您所说的“整体表现”是什么意思? @Bergi 整体性能是指 API 的平均响应时间。 @PraveenKumar - 如果您想知道 1000 次并行 API 调用是否都对同一个 API 服务器性能更好,那么一次执行 100 次,那么您必须进行测试才能确定。可能不会。如果它们都去同一个目标服务器,它不太可能有效地并行化 1000 个请求。而且,如果它是一个非常大的服务器场(例如 Google),那么它可能会限制您的速率,因此您无论如何都无法快速完成 1000 个请求。真正的答案是测试和测量。 @PraveenKumar 您提出的各个 api 请求的响应时间不会有太大差异 - 这取决于特定 api 是否可以很好地扩展。但是当您使用Promise.all 时,您的服务器将比按顺序执行三个 API 调用时更快地响应其客户端。

以上是关于Promise.all() 与等待的主要内容,如果未能解决你的问题,请参考以下文章

Promise.All不等待Promise解决

结合像Promise.all这样的等待

等待 VS Promise.all

Promise.all()函数中的JS“等待仅在异步函数中有效”[重复]

当我从等待移动到 Promise.all 时,TypeScript 函数返回类型错误

等待Promise.All都没有在Anuglar中等待