You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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");
}

问题根源分析

  1. 重试配置未生效:原生fetch不自带重试功能,你提到的重试配置没有实际作用,需要自己实现重试逻辑。
  2. 超时时间过短:3秒超时在服务器高负载时不足以完成响应,提前中断请求导致错误。
  3. 无并发控制:大批量URL并行请求会瞬间创建大量socket连接,触发服务器限流或本地端口耗尽,导致连接被主动断开。
  4. 文件处理存在竞态: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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.24 00:15:05