应使用 for await 遍历游标并逐块写入 zlib.creategzip() 流,避免 toarray() 全量加载或错误背压处理;需监听 error、close 事件及时清理游标,write() 返回 false 时等待 drain 事件。

直接用 zlib.createGzip() 管道会失败
常见错误是把 collection.find().toArray() 的结果转成 Buffer 再压缩——这会让整个查询结果全进内存,process.memoryUsage().rss 瞬间飙升,大集合下直接 OOM。更糟的是,即使你用了流式游标(for await 或 cursor.pipe()),若没正确衔接 zlib 流的背压机制,也会出现响应卡住、部分数据丢失或 ERR_STREAM_PREMATURE_CLOSE。
find().stream() 已被弃用,改用 for await + zlib.createGzip()
原生 MongoDB Node.js 驱动 v6+ 移除了 .stream() 方法,必须用异步迭代器消费游标。关键点在于:不能等所有文档收集完再压缩,而要边读边压,且确保每个 chunk 都触发 res.write() 或管道到 gzip 流。
- 用
const cursor = collection.find(query, { projection: { _id: 1, name: 1 } })限定字段,减少传输体积 - 创建 gzip 流:
const gzip = zlib.createGzip({ level: 6 })(level: 6是压缩比与速度的合理平衡点) -
res.writeHead(200, { 'Content-Encoding': 'gzip', 'Content-Type': 'application/json' })—— 必须提前设 header,否则浏览器不识别为 gzip 响应 - 逐个
JSON.stringify(doc) + '\n'写入 gzip 流,末尾加换行便于客户端按行解析(如 ndjson 场景)
如何避免 ERR_STREAM_DESTROYED 和内存泄漏
游标未关闭、gzip 流未监听 error、客户端断连未清理,都会让 Node 进程持续持有资源。MongoDB 游标默认 10 分钟超时,但若中间出错未手动 cursor.close(),它可能在后台继续消耗连接池。
使用一条命令部署ProbeChain Rydberg测试网代理节点。自动注册为Agent(NodeType=1),免gas,支持macOS/Linux/Windows。触发词:/r
- 始终包装
for await在try/catch中,出错后立即await cursor.close() - 监听
gzip.on('error', err => { res.destroy(err) })和res.on('close', () => cursor.close()) - 给整个请求加超时:
req.socket.setTimeout(30000),超时后调cursor.close()并res.destroy() - 禁用 Express 默认的
compression中间件——它会对已压缩的响应二次压缩,导致乱码
大结果集导出 JSONL + Gzip 的最小可行示例
这不是“返回一个 zip 包”,而是“流式吐出 gzip 压缩的 JSON 行”——适合前端用 fetch().body + DecompressionStream 或服务端直存。
app.get('/export', async (req, res) => {
const cursor = db.collection('logs').find({ ts: { $gt: Date.now() - 86400000 } });
res.setHeader('Content-Type', 'application/x-ndjson');
res.setHeader('Content-Encoding', 'gzip');
res.setHeader('Content-Disposition', 'attachment; filename="logs.jsonl.gz"');
<p>const gzip = zlib.createGzip({ level: 6 });
gzip.on('error', (err) => res.destroy(err));
res.on('close', () => cursor.close().catch(() => {}));</p><p>try {
for await (const doc of cursor) {
const line = JSON.stringify(doc) + '\n';
if (!gzip.write(line)) await new Promise(r => gzip.once('drain', r));
}
gzip.end();
} catch (err) {
await cursor.close();
throw err;
}
});</p>
真正容易被忽略的是:gzip 流的 write() 可能返回 false,表示内部缓冲区已满,必须等 drain 事件才能继续写——否则会丢数据或触发 ERR_STREAM_WRITE_AFTER_END。这个细节在小数据量下不暴露,一到百万级文档就崩。










