用 request 包发了几大波请求后 Nodejs 好像阻塞了,可能是什么原因
用 request 包发了几大波请求后 Nodejs 好像阻塞了,可能是什么原因
概述
其实不是阻塞,但不知道怎么用一句话描述,具体现象后面有详细讲。
用的是 request. 伪代码如下
let count = 100;
function batch() {
// 从数据库用 SELECT … LIMIT 100 取出 100 条 url
for (let url in urlList) {
const req = request(…);
req.on(‘error’, () => {
// 一些其它操作
count–;
if (count === 0) {
count = 100;
batch();
}
});
req.on(‘end’, () => {
// 一些其它操作
count–;
if (count === 0) {
count = 100;
batch();
}
});
}
第一波发出去的请求是正常的,过了几波后,在创建完 request, 控制权交还给事件循环时, 要等好长时间才会开始发请求,这个等待的时间会越来越久。试过调整连接池,但只能缓解,不能解决。
大家有没有碰到过类似的问题?或者有没有排查问题的思路?
等待发请求时的具体现象
- 通过打印日志,确定:
- 计数器工作正常
- 创建 request 对象的那个循环已经执行完了
- 但是
- lsof 看到没有创建任何 tcp 连接(前面的数据库连接除外)。
- 没有任何 request 的调试日志输出
- 用 gdb backtrace node 进程,看到几个线程是阻塞在 poll 上,其余几个线程是阻塞在信号量上
- 等待一段时间后
- 出现 request 的调试日志输出, lsof 看到 tcp 连接
- 这个等待时间会越来越久
batch 里的循环没有引用 count, 在 error/end 修改 end 有什么用…
你这样一下创建了 n 多请求, 会导致系统打开的 fd 急剧增加…
如果你是使用 callback 的话可以使用 async.parallelLimit, see https://github.com/caolan/async#paralleltasks-callback
promise, co, 或者 async/await 可以使用 promise.map 控制下并发, see https://github.com/magicdawn/promise.map
> 在 error/end 修改 end 有什么用…
在 error/end 修改 count 有什么用…
我的目的就是并发很多请求,因为这些 url 大部分是无法访问的。
修改 count 这个先不管吧,这是伪代码(公司政策不允许外发代码),实际代码这里肯定是没问题的,因为打印的日志显示计数从 100 降到 0 了,并且 batch 也已经被调用了。
async.parallel 看起来不错,下周我去换上。不过这个应该不是关键问题吧,我这也相当于是自己实现了 async.parallel 的功能。
在 Node.js 中使用 request
包发送大量请求后遇到阻塞问题,可能的原因包括以下几个方面:
-
事件循环阻塞: Node.js 是基于事件循环的,如果某个操作(如网络请求)占用了太多时间,会导致事件循环无法及时处理其他任务。虽然
request
包是异步的,但如果请求数量过多且未正确处理,可能会导致资源耗尽。 -
内存泄漏: 如果请求处理不当,可能会导致内存泄漏,进而耗尽系统资源,使 Node.js 进程变得缓慢甚至阻塞。
-
连接池耗尽: 默认情况下,
request
包可能不会有效管理 HTTP 连接池。如果发送的请求数量超过连接池的限制,新的请求可能会被阻塞,等待连接释放。
为了解决这个问题,你可以考虑以下几点:
- 使用 Promise 或 async/await 控制并发: 通过限制并发请求的数量,避免一次性发送过多请求。
const request = require('request-promise-native');
async function fetchUrls(urls) {
const results = [];
for (const url of urls) {
results.push(await request(url));
}
return results;
}
// 使用并发控制库,如 p-limit
const pLimit = require('p-limit');
const limit = pLimit(5); // 限制并发数为5
async function fetchUrlsLimited(urls) {
const promises = urls.map(url => limit(() => request(url)));
return Promise.all(promises);
}
-
检查并优化内存使用: 确保请求处理中没有内存泄漏。
-
配置连接池: 使用支持连接池的 HTTP 客户端库,如
axios
或node-fetch
(配合undici
等)。