Node.js 6.x中GridFS并发上传需手动控制:用p-limit限制并发数、设chunkSizeBytes减小chunks插入压力、每次新建UploadStream、错误时destroy流防连接池耗尽。

Node.js 6.x 中 GridFS 上传并发控制必须手动实现
MongoDB 官方 mongodb 驱动 2.x(对应 Node.js 6.x 常用版本)的 GridFSBucket 不支持内置并发上传,uploadFromStream() 是单次流式写入,多个文件并行调用会共享同一个 GridFSBucket 实例但各自走独立写流程——看似并发,实则底层可能因连接池争抢、内存缓冲堆积或 MongoDB 的 files/chunks 写入顺序混乱导致文件损坏或 ECONNRESET 错误。
关键判断:你不是“能不能并发”,而是“要不要以及怎么安全地并发”。真实场景中,盲目开高并发(如 Promise.all(files.map(...)))在 Node.js 6.x + 驱动 2.2.x 下极易触发 RangeError: Maximum call stack size exceeded 或 write after end 流错误。
- Node.js 6.x 默认最大堆内存约 1.4GB,单个大文件流 + 并发数 > 5 就容易 OOM
-
GridFSBucket的openUploadStream()返回的WriteStream不是可复用对象,每次上传必须新建 - MongoDB 服务端对单次
insert批量大小有限制(默认 1000 文档),而每个文件切块(chunk)就是一次 insert,高并发 = 瞬间大量 chunk 插入请求
用 p-limit 控制并发数比 Promise.all 可靠
Node.js 6.x 兼容性好、零依赖、轻量可控的方案是引入 p-limit(v1.x 版本,支持 Node.js 4+)。它不改变 Promise 执行逻辑,只限制同时 pending 的 Promise 数量,避免连接池打满和内存溢出。
示例:限制最多 3 个文件同时上传
const PLimit = require('p-limit');
const { GridFSBucket } = require('mongodb');
<p>const limit = PLimit(3);
const bucket = new GridFSBucket(db);</p><p>const uploadOne = (file) => {
return new Promise((resolve, reject) => {
const stream = bucket.openUploadStream(file.name);
stream.on('error', reject);
stream.on('finish', () => resolve(stream.id));
file.stream.pipe(stream); // file.stream 是 Readable
});
};</p><p>// files 是 { name, stream } 对象数组
const uploadPromises = files.map(file => limit(() => uploadOne(file)));
Promise.all(uploadPromises)
.then(ids => console.log('全部上传完成,_id 列表:', ids))
.catch(err => console.error('上传失败:', err));</p>
- 不要用
async/await在循环里直接await uploadOne(file),那变成串行 -
file.stream必须是未消费过的Readable流;若已读过,需用fs.createReadStream()重建 -
p-limitv1.x 返回函数而非 Promise,调用方式是limit(fn)而非limit.fn()
openUploadStream() 的 options 影响 chunk 大小和元数据写入时机
GridFS 每个文件被切成默认 255KB 的 chunk 存入 chunks 集合。上传前通过 openUploadStream() 的 chunkSizeBytes 控制单块大小,直接影响并发压力:
- 设为
1024 * 1024(1MB)→ chunk 数减少 →chunks插入次数下降 → 更适合高并发场景 - 设为
1024 * 256(256KB,默认)→ chunk 数多 → 插入更碎 → 并发高时易触发 MongoDB 写锁竞争 -
metadata字段必须在openUploadStream()时传入,后续无法修改;漏传会导致查询困难
正确写法:
const stream = bucket.openUploadStream(file.name, {
chunkSizeBytes: 1024 * 1024,
metadata: {
uploadedBy: 'admin',
source: 'web-upload'
}
});
注意:chunkSizeBytes 只影响新文件,不影响已有文件;且必须是 255KB 的整数倍(驱动会自动向上取整到最近合法值)。
上传失败后务必销毁流并捕获 close 事件
Node.js 6.x 中流错误处理不完善,uploadFromStream() 或 openUploadStream() 出错时,若不显式销毁流,残留的 WriteStream 可能持续占用 socket 连接,最终耗尽 mongodb 驱动的连接池(默认 5 连接),后续所有操作卡死。
必须做两件事:
- 监听
error后立即调用stream.destroy() - 监听
close事件(而非仅finish),确保底层资源释放
修正后的 uploadOne 片段:
const uploadOne = (file) => {
return new Promise((resolve, reject) => {
const stream = bucket.openUploadStream(file.name, options);
<pre class="brush:php;toolbar:false;">stream.on('error', (err) => {
stream.destroy(); // 关键:立刻销毁
reject(err);
});
stream.on('finish', () => resolve(stream.id));
stream.on('close', () => {
// 可选:记录流已彻底关闭
});
file.stream.pipe(stream);}); };
真正容易被忽略的是:Node.js 6.x 的 stream.destroy() 是实验性 API(v6.8.0+ 才稳定),若你用的是更早的 6.x 版本,需降级用 stream.emit('error', err) + stream.end() 组合兜底。











