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

Node.js下载Cloud Storage文件夹及子文件时代码挂起问题

GCS存储桶文件夹下载程序挂起问题排查与修复

核心故障原因

  • 无限制并发触发资源耗尽:代码中通过files.map直接为所有文件发起并发下载请求,没有做并发数控制。当文件夹下文件数量较多时,会瞬间打满Node.js进程的TCP连接池、操作系统文件句柄上限,GCS服务端也会对过量并发请求做限流排队,导致后续请求无响应、进程挂死。
  • 分页逻辑缺失:调用getFiles时设置了autoPaginate: false,但没有手动处理分页游标,默认最多只会拉取单页(通常1000个)的文件列表,不仅无法下载全量文件,单页内的大文件并发请求很容易直接堵死连接。
  • 无效参数触发客户端异常:调用storage.bucket(bucketName).file()时错误传入了optionsGetFiles(仅用于列举文件的prefix配置)作为第二个参数,该参数不属于file方法的合法配置项,可能触发客户端内部未捕获的静默异常,导致对应promise永远处于pending状态。
  • 错误处理不完善:下载请求的reject分支仅打印日志,没有做超时兜底、状态标记,只要有一个请求因为网络、限流原因卡住无返回,Promise.all就会永久等待,表现为程序挂起无响应。
  • 冗余IO增加阻塞风险:每遍历一个文件就调用一次fs.mkdirSync同步创建目录,大量重复的同步IO调用会阻塞Node.js事件循环,拖慢整体执行效率。

可直接落地的修复方案

  1. 开启文件列表自动分页,拉取全量文件清单
  2. 移除file方法调用时传入的无效配置参数
  3. 增加并发控制,将同时下载的并发数限制在5~10区间(可根据网络带宽调整)
  4. 为每个下载请求增加超时兜底,避免单个请求卡死整个流程
  5. 优化目录创建逻辑,使用异步IO方法减少事件循环阻塞

修复后的参考代码:

import path from 'path';
import fs from 'fs/promises';
import { createWriteStream } from 'fs';
import { pipeline } from 'stream/promises';
import { Storage } from '@google-cloud/storage';

const storage = new Storage();
// 可根据实际网络带宽、服务器配置调整并发数
const concurrencyLimit = 8;
let runningCount = 0;
const taskQueue = [];

async function runTask(task) {
  runningCount++;
  try {
    await task();
  } catch (err) {
    console.error('Task run error:', err);
  } finally {
    runningCount--;
    if (taskQueue.length > 0) {
      const nextTask = taskQueue.shift();
      runTask(nextTask);
    }
  }
}

function addTask(task) {
  if (runningCount < concurrencyLimit) {
    runTask(task);
  } else {
    taskQueue.push(task);
  }
}

export async function downloadFolder(bucketName, srcFolder) {
  // 开启自动分页,拉取全量文件列表
  const optionsGetFiles = {
    prefix: `${srcFolder}/`,
    autoPaginate: true
  };
  const [files] = await storage.bucket(bucketName).getFiles(optionsGetFiles);
  const baseLocalPath = path.resolve("./utils/buckets");
  const downloadTasks = [];

  for (const file of files) {
    // 跳过目录占位对象
    if (file.name.endsWith('/')) continue;

    const destPath = path.join(baseLocalPath, file.name);
    const destDir = path.dirname(destPath);
    // 异步创建目录,避免阻塞事件循环
    await fs.mkdir(destDir, { recursive: true });

    // 封装单个下载任务,加入并发控制队列
    const task = () => new Promise((resolve) => {
      // 15秒超时兜底,避免请求永久挂起
      const timeout = setTimeout(() => {
        console.log(`Download timeout: ${file.name}`);
        resolve(false);
      }, 15000);

      // 移除file方法的无效传参,用流方式下载更适配大文件场景
      const fileStream = storage.bucket(bucketName).file(file.name).createReadStream();
      const writeStream = createWriteStream(destPath);

      pipeline(fileStream, writeStream)
        .then(() => {
          clearTimeout(timeout);
          console.log(`File downloaded: ${file.name}`);
          resolve(true);
        })
        .catch((err) => {
          clearTimeout(timeout);
          console.log(`Error on file: ${file.name} | ${err.message}`);
          resolve(false);
        });
    });

    downloadTasks.push(new Promise((resolve) => {
      addTask(() => task().then(resolve));
    }));
  }

  await Promise.all(downloadTasks);
  // 等待队列剩余任务全部执行完成
  while (runningCount > 0 || taskQueue.length > 0) {
    await new Promise(r => setTimeout(r, 100));
  }
  console.log(`All files downloaded`);
  return true;
}

额外排查提示

如果修复后仍偶发挂起,可在启动Node.js进程时添加--trace-warnings参数,打印TCP连接、句柄泄漏的相关日志,定位具体卡住的请求点。


内容的提问来源于stack exchange,提问作者DamDam

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.29 06:54:28