Node.js网络爬虫大负载下出现socket hang up问题求助
Node.js爬虫大批量URL处理时出现socket hang up错误
问题现象
处理小批量URL时爬虫运行正常,但处理3000条URL的大批量任务时,周期性出现以下错误:
"FetchError: request to "URL" failed, reason: socket hang up"
每3000条URL中约有4-6次该错误,报错伴随ECONNRESET超时。所有URL均有效,单独重试出错URL可正常爬取,推测是请求量过大导致服务器临时限流或连接中断。
已尝试的调整:
- 在fetch中设置
{signal: AbortSignal.timeout(3000)} - 配置了
{retries: 3, retryDelay: 1000}但未实际生效
现有核心代码:
try { await fetch(line) .then(res => { if (res.ok && res.status === 200) { const dest = fs.createWriteStream(OUTPUT_DIR + parent_path + updated_filename) if (!fs.existsSync(OUTPUT_DIR + parent_path + updated_filename)) { res.body.pipe(dest); } else { console.log("File Already Exists"); } } else { console.log("There was an issue with the URL"); } }); } catch (error) { console.log(error); errors.write(line + "\r\n"); }
问题根源分析
- 重试配置未生效:原生
fetch不自带重试功能,你提到的重试配置没有实际作用,需要自己实现重试逻辑。 - 超时时间过短:3秒超时在服务器高负载时不足以完成响应,提前中断请求导致错误。
- 无并发控制:大批量URL并行请求会瞬间创建大量socket连接,触发服务器限流或本地端口耗尽,导致连接被主动断开。
- 文件处理存在竞态:
fs.existsSync和createWriteStream的顺序存在时间差,可能出现重复写入或判断错误;同时未监听流的错误事件,可能导致未捕获异常。
解决思路与方案
1. 实现针对特定错误的重试机制
针对ECONNRESET、socket hang up这类临时错误,实现带指数退避的重试逻辑:
async function fetchWithRetry(url, options = {}, retries = 3, baseDelay = 1000) { try { // 延长超时时间到10秒,给服务器足够响应时间 const fetchOptions = { ...options, signal: options.signal || AbortSignal.timeout(10000), headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36', ...options.headers } }; const res = await fetch(url, fetchOptions); if (!res.ok) throw new Error(`HTTP状态异常: ${res.status}`); return res; } catch (error) { // 仅对可重试错误进行重试 if (retries > 0 && (error.name === 'FetchError' && error.message.includes('ECONNRESET') || error.message.includes('socket hang up'))) { const delay = baseDelay * Math.pow(2, 3 - retries); // 指数退避 console.log(`重试 ${url},剩余次数:${retries - 1},延迟:${delay}ms`); await new Promise(resolve => setTimeout(resolve, delay)); return fetchWithRetry(url, options, retries - 1, baseDelay); } throw error; } }
2. 控制请求并发数
使用并发限制工具(如p-limit)避免瞬间发起大量请求,推荐并发数设置为10-20(根据目标服务器承受能力调整):
const pLimit = require('p-limit'); const fs = require('fs').promises; // 改用异步文件API,避免阻塞 const limit = pLimit(15); // 限制同时处理15个请求 // 批量处理URL列表 async function processUrls(urlList) { await Promise.all( urlList.map(url => limit(() => handleSingleUrl(url))) ); } async function handleSingleUrl(line) { try { const res = await fetchWithRetry(line); const filePath = `${OUTPUT_DIR}${parent_path}${updated_filename}`; // 用'wx' flag避免覆盖已存在的文件,同时避免竞态 const dest = fs.createWriteStream(filePath, { flags: 'wx' }); await new Promise((resolve, reject) => { res.body.pipe(dest); dest.on('finish', resolve); dest.on('error', err => { if (err.code === 'EEXIST') { console.log(`文件已存在:${filePath}`); resolve(); } else { reject(err); } }); res.body.on('error', reject); // 监听响应流错误 }); } catch (error) { console.error(`处理失败:${line}`, error.message); await fs.appendFile('./errors.log', `${line}\r\n`); // 异步写入错误日志 } }
3. 优化请求细节
- 添加浏览器请求头:模拟正常浏览器请求,降低被识别为爬虫的概率(已在
fetchWithRetry中实现)。 - 避免同步文件操作:改用
fs.promises异步API,避免阻塞事件循环影响请求处理。
4. 错误日志优化
区分不同错误类型,仅将重试多次仍失败的请求写入错误日志,避免日志冗余。
内容的提问来源于stack exchange,提问作者Somosfeer
相关产品推荐
相关产品推荐

