根本原因是文件未流式处理导致V8堆内存溢出:上游同步读取或中间Buffer拼接使整个文件驻留内存,未进入GridFS前已OOM;须用fs.createReadStream直连uploadStream、设highWaterMark、禁用bodyParser、避免误监听data事件。

GridFS 上传大文件时 Node 进程内存爆掉,根本原因是流没控住
Node.js 默认把整个文件读进内存再传给 GridFSBucket,哪怕你用 createUploadStream(),如果上游没做流式读取或中间加了缓冲层(比如 fs.readFileSync、Buffer.from()),照样 OOM。这不是 GridFS 的锅,是数据还没进 GridFS 就卡在 V8 堆里了。
实操建议:
- 必须用
fs.createReadStream()直接对接uploadStream,绕过任何同步读取或中间Buffer拼接 - 避免在流管道中插入
.on('data', ...)或.pipe(transformStream)时未设highWaterMark,默认 16KB 太小,大文件会堆积大量待写 chunk - 上传前检查
req.headers['content-length'],超限直接res.status(413).end(),别让请求体进 Node
uploadStream.write() 报错 RangeError: Invalid array buffer length
这是典型的 Buffer 超限错误,常见于:前端用 FileReader.readAsArrayBuffer() 读完整文件后,再用 Buffer.from(arrayBuffer) 构造——此时整个文件已驻留内存,GridFS 还没开始写,V8 堆就撑不住了。
实操建议:
- 浏览器端不要读全文件,改用
fetch+ReadableStream直传(现代浏览器支持),或后端接收multipart/form-data时用busboy/formidable流式解析,立刻 pipe 到uploadStream - Node 端若必须处理本地大文件,确认
fs.createReadStream(path, { highWaterMark: 64 * 1024 }),别依赖默认值 - 禁用 Express 的
bodyParser中间件对multipart的自动解析,它默认会缓存整个 body
GridFS 分块大小(chunkSizeBytes)设太小反而更耗内存
默认 chunkSizeBytes: 255 * 1024(255KB),看着小,其实平衡了 MongoDB 网络包大小和客户端内存压力。如果你改成 8KB,单个文件会生成更多 chunk 文档,驱动要维护更多 pending write 操作,内部 buffer 队列变长,GC 压力不降反升。
实操建议:
- 除非有特殊合规要求(如加密分块),否则别动
chunkSizeBytes;改大到 1MB 也没啥收益,MongoDB wire protocol 单包上限约 16MB,但 driver 内部仍有开销 - 真正影响内存的是「并发上传数」,用
Promise.allSettled([...uploads])替代Promise.all(),避免一个失败拖垮全部,也防止同时触发太多 stream 管道 - 上传完记得调
uploadStream.end(),漏掉会导致 socket 不释放、buffer 不清空,后续请求持续累积内存
用 stream.pipeline() 替代手动 .pipe() 链能早发现断流
手动 .pipe() 不会自动传播 error,上游流出错(如磁盘满、网络中断),下游 uploadStream 可能卡住,Node 以为还在写,buffer 持续增长直到 OOM。
实操建议:
- 统一用
stream.pipeline(readStream, uploadStream, (err) => { if (err) console.error(err); }),它会在任一环节出错时立刻销毁所有流并回调 - 监听
uploadStream的'close'和'error',但别只靠这个——pipeline的回调才是唯一可信的完成信号 - 上传过程中别在
uploadStream上做.on('data'),它不是可读流,没有data事件;误监听会掩盖真实错误
真正卡住内存的,往往不是 GridFS 本身,而是你自以为“已经流式”却悄悄把整块数据 load 进来的那几行代码。检查每个 Buffer 构造、每个 readFileSync、每个没设 highWaterMark 的 createReadStream——这些地方比 MongoDB 配置更容易被忽略。










