触发并行的 1k HTTP 请求会卡住
Triggering parallel of 1k HTTP requests would get stuck
问题是当您触发 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');
})();
为了确定发生了什么,我需要对您的脚本进行一些修改,但这里有。
首先,您可能知道 node
及其 event loop
的工作原理,但让我快速回顾一下。当您 运行 一个脚本时,node
运行 时间首先 运行 它的同步部分然后安排 promises
和 timers
执行下一个循环,并在检查时解决它们,运行 另一个循环中的回调。这个简单的要点解释得很好,归功于@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
Note that the event loop will never end until there is pending OS tasks. In other words, your node execution will never end until there is pending HTTP requests.
在你的例子中,它 运行 是一个 async
函数,因为它总是 return 一个承诺,它将安排它在下一个循环迭代中执行。在您的异步函数中,您在 map
迭代中立即安排另一个 1000 承诺(HTTP 请求)。在那之后,你正在等待所有然后解决完成该程序。它肯定会起作用,除非你在 map
上的匿名箭头函数不会抛出任何 错误 。如果你的一个承诺抛出错误而你没有处理它,一些承诺将不会调用它们的回调,使程序运行到 end 而不是 exit,因为事件循环会阻止它退出,直到它解决了所有任务,即使没有回调。正如它在 Promise.all
docs 上所说:它会在第一个承诺拒绝后立即拒绝。
因此,您的 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");
}
})();
问题是当您触发 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');
})();
为了确定发生了什么,我需要对您的脚本进行一些修改,但这里有。
首先,您可能知道 node
及其 event loop
的工作原理,但让我快速回顾一下。当您 运行 一个脚本时,node
运行 时间首先 运行 它的同步部分然后安排 promises
和 timers
执行下一个循环,并在检查时解决它们,运行 另一个循环中的回调。这个简单的要点解释得很好,归功于@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
Note that the event loop will never end until there is pending OS tasks. In other words, your node execution will never end until there is pending HTTP requests.
在你的例子中,它 运行 是一个 async
函数,因为它总是 return 一个承诺,它将安排它在下一个循环迭代中执行。在您的异步函数中,您在 map
迭代中立即安排另一个 1000 承诺(HTTP 请求)。在那之后,你正在等待所有然后解决完成该程序。它肯定会起作用,除非你在 map
上的匿名箭头函数不会抛出任何 错误 。如果你的一个承诺抛出错误而你没有处理它,一些承诺将不会调用它们的回调,使程序运行到 end 而不是 exit,因为事件循环会阻止它退出,直到它解决了所有任务,即使没有回调。正如它在 Promise.all
docs 上所说:它会在第一个承诺拒绝后立即拒绝。
因此,您的 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");
}
})();