GridFSDownloadStream一读就OOM,因其默认尽全力拉取数据而未控流;必须用pipelineAsync配合highWaterMark(建议1–2MB)精确控流,并先findOne校验文件存在且元数据完整。

GridFSDownloadStream 为什么一读就 OOM?
不是流本身有问题,而是你没控制它的“吐速”。GridFSDownloadStream 是可读流,但默认行为是尽可能快地从 fs.chunks 拉数据——如果下游(比如 HTTP 响应、文件写入)处理慢,Node.js 就会在内存里缓存大量未消费的 chunk,直到爆掉。常见现象包括:响应卡住、内存占用飙升、服务被系统 kill。
用 pipeline() 替代 pipe() + 显式 highWaterMark
pipe() 虽自动处理背压,但对 GridFS 这类可能拉取数 MB/chunk 的场景不够稳;pipeline() 不仅自动传播错误、清理资源,还能配合 highWaterMark 精确控流。
- 读取流必须设
highWaterMark:建议 1–2MB(1024 * 1024 * 2),匹配视频 GOP 或大文本段落,避免太小导致频繁 read() 调用、太大失去流控意义 - 下载流创建后,不能直接
stream.pipe(res),要走pipeline(stream, res) - 务必
awaitpipelineAsync(),否则错误会静默丢失
示例:
const { pipeline } = require('stream');
const { promisify } = require('util');
const { GridFSBucket } = require('mongodb');
<p>const pipelineAsync = promisify(pipeline);</p><div class="aritcle_card flexRow">
<div class="artcardd flexRow">
<a class="aritcle_card_img" href="/xiazai/skill8612" title="Zenlink - Node SDK for ZenHeart"><img
src="https://img.php.cn/upload/skill/000/000/081/179214129612849.jpg" alt="Zenlink - Node SDK for ZenHeart" onerror="this.onerror='';this.src='/static/lhimages/moren/morentu.png'" ></a>
<div class="aritcle_card_info flexColumn">
<a href="/xiazai/skill8612" title="Zenlink - Node SDK for ZenHeart">Zenlink - Node SDK for ZenHeart</a>
<p>ZenHeart zenlink 与 zenlink-mcp 为 OpenClaw,涵盖安装、MCP 注册、钩子接线、入站轮询及升级维护。</p>
</div>
<a href="/xiazai/skill8612" title="Zenlink - Node SDK for ZenHeart" class="aritcle_card_btn flexRow flexcenter"><b></b><span>下载</span> </a>
</div>
</div><p>async function downloadFile(res, bucket, fileId) {
const fileDoc = await bucket.findOne({ _id: fileId });
if (!fileDoc) throw new Error('File not found');</p><p>const downloadStream = bucket.openDownloadStream(fileId, {
// 关键:限制每次内部 pull 的最大缓冲量
highWaterMark: 1024 <em> 1024 </em> 2 // 2MB
});</p><p>// 手动设置响应头(GridFS 不自动透传)
res.set({
'Content-Type': fileDoc.contentType || 'application/octet-stream',
'Content-Disposition': <code>attachment; filename="${encodeURIComponent(fileDoc.filename)}"</code>
});</p><p>await pipelineAsync(downloadStream, res);
}为什么不能用 stream.on('data') + write() 手动控制?
手动监听 data 事件再调 writable.write(chunk) 看似可控,但极易漏掉关键环节:
- 没检查
writable.write()返回值,忽略false信号 → 背压失效 - 没监听
drain事件并调readable.resume()→ 流永久暂停 - 没处理
error和end的竞态,比如write()报错后还继续resume() - HTTP 响应流(
res)不 emitdrain,所以这种模式在 Web 场景下基本不可用
findOne 查元数据这一步不能跳
bucket.openDownloadStream(fileId) 看似直接,但前提是 fileId 必须是有效的 ObjectId,且对应文件真实存在。如果传了字符串、null 或错误 ID:
- 返回空流,
pipeline()立即 resolve,前端收不到任何数据 - 或抛
TypeError: Cannot read property '_id' of null - 更隐蔽的是:文件存在但
metadata字段缺失contentType,导致浏览器无法正确解析响应
务必先 await bucket.findOne({ filename: "xxx" }) 或 { _id: new ObjectId(idStr) },再校验 doc 是否为真值。
实际部署时最容易被忽略的,是把 highWaterMark 设在 createReadStream 上——GridFS 没有这个 API;它只能设在 openDownloadStream() 的 options 里,而且这个值只影响驱动层从 MongoDB 拉 chunk 的节奏,不影响后续 Node.js 流管道的缓冲行为。

















