触发并行 1k HTTP 请求会卡住

Ris*_*vik 10 event-loop node.js

问题是当您触发 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');
})();
Run Code Online (Sandbox Code Playgroud)

Ped*_*ter 3

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

首先,您可能知道node它的工作原理和event 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
Run Code Online (Sandbox Code Playgroud)

请注意,事件循环永远不会结束,直到有待处理的操作系统任务为止。换句话说,除非有待处理的 HTTP 请求,否则您的节点执行将永远不会结束。

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

因此,您的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");
  }
})();
Run Code Online (Sandbox Code Playgroud)