API批量数据抓取先遇内存问题,现出现connect ETIMEDOUT错误求助
解决大规模API抓取的内存溢出与连接超时问题
看起来你在抓取60多万页的API数据时遇到了两个典型问题:内存耗尽和connect ETIMEDOUT,这都是因为一次性发起了过高并发的请求导致的。咱们一步步来解决:
问题根源分析
- 内存问题:
async.map默认会同时发起所有60多万个请求,每个请求的响应数据都会存在内存里,直到全部请求完成才写入文件,内存自然扛不住。 - ETIMEDOUT错误:短时间内发起数万级请求,会耗尽本地TCP端口,同时目标服务器也会因为请求频率过高拒绝连接,导致超时。
针对性解决方案
1. 控制请求并发数
用async.mapLimit替代async.map,限制同时发起的请求数量(比如10-20,根据服务器承受能力调整),避免瞬间压垮服务器和本地资源:
const async = require('async'); const request = require('request'); const jsonfile = require('jsonfile'); const file = '/temp/data.json'; const urls = []; for (let i = 1; i <= 608469; i++) { const url = `https://api.demo.com/v2.1/people?user_key=auth_key&page=${i}&sort_order=created_at%20DESC`; urls.push(url); } function httpGet(url, callback) { const options = { url, json: true }; request(options, function(err, res, body) { // 针对超时错误增加一次重试 if (err && err.code === 'ETIMEDOUT') { return request(options, callback); } callback(err, body); }); } // 限制并发数为15,可根据实际情况调整 async.mapLimit(urls, 15, httpGet, function (err, res) { if (err) return console.error('抓取失败:', err); jsonfile.writeFileSync(file, res); console.log('数据已全部写入文件'); });
2. 优化内存占用:流式写入/分批写入
如果数据量极大,即使限制并发,全部存在内存里还是会爆,这时候可以边请求边写入文件,或者分批写入:
分批写入示例
const async = require('async'); const request = require('request'); const jsonfile = require('jsonfile'); const fs = require('fs'); const file = '/temp/data.json'; const urls = []; const batchSize = 1000; // 每1000条数据写入一次 let allData = []; for (let i = 1; i <= 608469; i++) { const url = `https://api.demo.com/v2.1/people?user_key=auth_key&page=${i}&sort_order=created_at%20DESC`; urls.push(url); } // 先清空文件并写入数组开头 fs.writeFileSync(file, '['); function httpGet(url, callback) { const options = { url, json: true }; request(options, function(err, res, body) { if (err && err.code === 'ETIMEDOUT') { return request(options, callback); } if (!err) { allData.push(body); // 达到批次大小就写入文件 if (allData.length >= batchSize) { const dataStr = allData.map(item => JSON.stringify(item)).join(','); fs.appendFileSync(file, dataStr + ','); allData = []; // 清空当前批次释放内存 } } callback(err, body); }); } async.mapLimit(urls, 15, httpGet, function (err) { if (err) return console.error('抓取失败:', err); // 写入剩余未达批次的数据 if (allData.length > 0) { const dataStr = allData.map(item => JSON.stringify(item)).join(','); fs.appendFileSync(file, dataStr); } // 写入数组结尾完成JSON格式 fs.appendFileSync(file, ']'); console.log('数据已全部写入文件'); });
3. 额外优化建议
- 增强重试机制:可以用
async.retry封装请求,针对超时、5xx等可恢复错误自动重试多次,进一步提高抓取成功率。 - 清理无用依赖:你代码里同时引入了
node-fetch和request但只用到了request,可以移除无用依赖减少冗余。 - 监控内存状态:在代码中加入
console.log(process.memoryUsage()),实时查看内存占用,方便调整并发数和批次大小。 - 考虑API速率限制:如果目标API有明确的请求频率限制,可在请求间隔加入短暂延迟(比如结合
setTimeout),避免触发限流。
内容的提问来源于stack exchange,提问作者Marianna Drobniak
相关产品推荐
相关产品推荐

