触发1k HTTP请求的并行将卡住


10

问题是,当您触发1k-2k传出HTTP请求时,实际上发生了什么?我看到它可以轻松解决500个连接中的所有连接,但是从那里往上移动似乎会导致问题,因为连接保持打开状态,Node应用程序将卡在那儿。经过本地服务器+示例Google和其他模拟服务器的测试。

因此,对于某些不同的服务器端点,我确实收到了原因:读取ECONNRESET,这很好,服务器无法处理请求并抛出错误。在1k-2k的请求范围内,程序将挂起。当您检查打开的连接时,lsof -r 2 -i -a您会看到有X数量的连接一直挂在那里0t0 TCP 192.168.0.20:54831->lk-in-f100.1e100.net:https (ESTABLISHED)。当您向请求中添加超时设置时,这些设置可能最终会导致超时错误,但是为什么否则连接会一直保持下去,并且主程序最终会陷入困境?

示例代码:

import fetch from 'node-fetch';

(async () => {
  const promises = Array(1000).fill(1).map(async (_value, index) => {
    const url = 'https://google.com';
    const response = await fetch(url, {
      // timeout: 15e3,
      // headers: { Connection: 'keep-alive' }
    });
    if (response.statusText !== 'OK') {
      console.log('No ok received', index);
    }
    return response;
  })

  try {
    await Promise.all(promises);
  } catch (e) {
    console.error(e);
  }
  console.log('Done');
})();

1
莫非的你后的结果npx envinfo,运行在我赢得10 / nodev10.16.0脚本结束你的例子在8432.805ms
卢卡斯Szewczak

我在OS X和Alpine Linux(docker容器)上运行示例,并获得了相同的结果。
Risto Novik

我的本地mac在7156.797ms中运行脚本。您确定没有防火墙阻止请求吗?
约翰

在不使用本地计算机防火墙的情况下进行了测试,但是我的本地路由器/网络是否可能存在问题?我将尝试在Google Cloud或Heroku中运行类似的测试。
Risto Novik

Answers:


3

为了确定发生了什么,我需要对您的脚本进行一些修改,但是这里有。

首先,您可能知道nodeevent loop工作方式及其工作原理,但让我快速回顾一下。运行脚本时,node运行时首先运行它的同步部分,然后计划promisestimers在下一个循环中执行,并且在检查了它们的解析后,在另一个循环中运行回调。这个简单的要点很好地解释了它,归功于@StephenGrider:


const pendingTimers = [];
const pendingOSTasks = [];
const pendingOperations = [];

// New timers, tasks, operations are recorded from myFile running
myFile.runContents();

function shouldContinue() {
  // Check one: Any pending setTimeout, setInterval, setImmediate?
  // Check two: Any pending OS tasks? (Like server listening to port)
  // Check three: Any pending long running operations? (Like fs module)
  return (
    pendingTimers.length || pendingOSTasks.length || pendingOperations.length
  );
}

// Entire body executes in one 'tick'
while (shouldContinue()) {
  // 1) Node looks at pendingTimers and sees if any functions
  // are ready to be called.  setTimeout, setInterval
  // 2) Node looks at pendingOSTasks and pendingOperations
  // and calls relevant callbacks
  // 3) Pause execution. Continue when...
  //  - a new pendingOSTask is done
  //  - a new pendingOperation is done
  //  - a timer is about to complete
  // 4) Look at pendingTimers. Call any setImmediate
  // 5) Handle any 'close' events
}

// exit back to terminal

请注意,事件循环永远不会结束,直到有待处理的OS任务。换句话说,您的节点执行将永远不会结束,直到有未决的HTTP请求。

在您的情况下,它运行一个async函数,因为它将始终返回promise,并将其安排在下一个循环迭代中执行。在异步功能上,您可以在该迭代中一次安排另外1000个 Promise(HTTP请求)map。之后,您正在等待所有解决方案完成。除非您的匿名箭头功能map不会引发任何错误,否则它将肯定起作用。如果您的一个诺言引发错误并且您没有处理错误,则某些诺言将不会调用它们的回调,从而使程序结束但不退出,因为事件循环将阻止它退出,直到解决为止所有任务,即使没有回调。正如它在Promise.all docs:第一个诺言被拒绝时,它会被拒绝。

因此,您的on ECONNRESET错误与节点本身无关,与您的网络有关,它使获取操作抛出错误,然后阻止事件循环结束。有了这个小修正,您将能够看到所有请求都被异步解决:

const fetch = require("node-fetch");

(async () => {
  try {
    const promises = Array(1000)
      .fill(1)
      .map(async (_value, index) => {
        try {
          const url = "https://google.com/";
          const response = await fetch(url);
          console.log(index, response.statusText);
          return response;
        } catch (e) {
          console.error(index, e.message);
        }
      });
    await Promise.all(promises);
  } catch (e) {
    console.error(e);
  } finally {
    console.log("Done");
  }
})();

嘿,佩德罗感谢您的解释。我知道Promise.all会在出现第一个承诺被拒绝时拒绝,但是在大多数情况下,没有错误可以拒绝,因此整个过程只会变得很空闲。
Risto Novik

1
>修复事件循环永远不会结束,直到有待处理的OS任务。换句话说,您的节点执行将永远不会结束,直到有未决的HTTP请求。这似乎很有趣,操作系统任务是通过libuv管理的吗?
Risto Novik

我猜libuv处理更多与操作有关的事情(确实需要多线程的事情)。但是我可能是错的,需要更深入地了解
Pedro Mutter
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.