应控制并发+流式拼接:用find查精简元数据,p-limit限流3–5个,逐个append到archiver;禁用archiver缓存(store:false),监听error和close事件并销毁流,加AbortController超时保护。

GridFS 文件批量读取时如何避免内存爆炸
直接用 find() 拿到全部 GridFSFile 再逐个调用 openDownloadStream(),很容易在高并发或大文件场景下把 Node.js 进程内存打满。根本原因在于 GridFS 的流默认不自动限速、不背压感知,且多个流同时读取会竞争数据库连接和缓冲区。
正确做法是控制并发 + 流式拼接,而不是“全量加载再压缩”:
- 用
find({ _id: { $in: [...] } })查元数据,但只取_id和filename字段,减少网络和内存开销 - 用
p-limit或Promise.allSettled限制同时打开的下载流数量(建议 3–5) - 每个流拿到后立刻 pipe 到 zip 库(如
archiver),不等待全部流就绪 - 务必监听
error事件并销毁流,否则失败文件会卡住整个 zip 流
用 archiver 实时生成 Zip 流的关键配置
archiver 默认缓存所有 entry,必须显式禁用才能实现真正流式输出。否则即使你 pipe 了,它也会等所有文件加完才开始写 zip 头,失去“实时生成”意义。
关键配置项:
-
archiver('zip', { zlib: { level: 6 }, store: false })——store: false是必须的,否则不压缩也占内存 -
archive.pipe(res)必须在archive.finalize()之前调用,且不能等所有文件 add 完才 pipe - 每个文件添加要用
archive.append(stream, { name: filename }),不要用file()(它会读入内存) - 设置
res.setHeader('Content-Type', 'application/zip')和res.setHeader('Content-Disposition', 'attachment; filename="batch.zip"')
MongoDB 驱动中如何安全获取 GridFS 流并透传错误
GridFS 的 openDownloadStream() 返回的是 Readable 流,但它内部可能触发 MongoDB 查询错误(如文件不存在、权限不足、连接中断),这些错误不会自动冒泡到外层 zip 流。
必须手动绑定错误处理链:
- 对每个流调用
stream.on('error', err => { archive.emit('error', err) }),让 archiver 统一捕获 - 不要用
stream.pipe(archive)直连,改用archive.append(stream, ...),因为前者无法拦截流创建阶段的 error - 如果某文件查不到(
GridFSFile not found),驱动抛出的是MongoServerError,需在openDownloadStream()外层 try/catch,并返回空流或跳过(视业务而定) - 注意:MongoDB Node.js 驱动 v5+ 中,
openDownloadStream()不再接受 callback,必须用 promise +await或.then()
响应超时与客户端断连的兜底处理
Zip 流一旦开始 pipe,Node.js 就认为响应已发出,此时客户端断连或 Nginx 超时,服务端不会自动终止流。残留的 GridFS 读取和 archiver 处理会持续占用资源,甚至导致内存泄漏。
必须主动监听断连信号:
res.on('close', () => { archive.destroy(); streams.forEach(s => s.destroy()); })res.on('finish', () => { /* 清理资源 */ })- 给整个操作加 timeout(如
AbortController),防止某个文件卡死拖垮整批下载 - Nginx 反向代理需调大
proxy_read_timeout和proxy_buffering off,否则会缓存 zip 流并延迟传输
最易被忽略的是:GridFS 流没有超时机制,哪怕 MongoDB 响应慢,它也不会自己 abort。得靠上层控制流生命周期,而不是依赖底层自动清理。


















