Node.js Stream 与背压:大文件与 HTTP 场景
在 Node 里处理「大文件、持续产生的数据、高并发下载上传」时,一次性
readFile/ 拼字符串往往会把内存打爆。
这一篇围绕 Stream 展开,想讲清楚几件事:Stream 在解决什么问题、pipe背后背压(backpressure)是什么意思,以及在 HTTP 与大文件场景里怎么避免把进程拖死。
为什么需要 Stream:内存与时间的折中
典型反模式:
- 读一个几百 MB 的文件 →
readFile整个进内存; - 下载一个大文件 → 先攒成完整 Buffer 再写盘。
问题:
- 内存峰值随数据量线性上涨;
- 开始写出结果的时间被推迟到「全部读完」之后。
Stream 的思路是:
- 把数据切成一段一段(chunk)流动;
- 上游产生一点,下游就消费一点,内存里只保留有限窗口。
四类 Stream:读、写、双工、转换
常见分类:
- Readable:可读流(数据源);
- Writable:可写流(数据汇);
- Duplex:双工(可读可写,例如 TCP socket);
- Transform:转换流(读入、加工、再写出,例如压缩、加解密)。
心智模型可以简化为:
- 数据像水管里的水,流是管子;
- Transform 是管子中间加了一个「处理站」。
pipe:连接流,并把背压传回去
readable.pipe(writable) 是最常用的组合方式,它做了几件重要的事:
- 把 Readable 的数据自动写入 Writable;
- 在 Writable 忙不过来时,暂停 Readable,避免内存无限堆积——这就是 背压 的核心含义。
可以把它理解成:
- 下游说「我处理不过来了」,上游就要减速或暂停;
- 而不是上游一直往内存里灌。
如果手写 data 事件里不停 write,而不处理 drain 事件,就容易失去背压保护,造成内存暴涨。
大文件:createReadStream / createWriteStream
典型安全写法:
- 用
fs.createReadStream读; - 用
fs.createWriteStream写; - 中间需要处理时接上
Transform或第三方流(例如 gzip)。
这样:
- 峰值内存大致与 缓冲区大小 相关,而不是与文件总大小成正比。
HTTP:请求与响应也是流
在 Node 的 http / https 里:
- IncomingMessage(请求体、响应体)往往是可读流;
- ServerResponse 是可写流。
意味着:
- 上传大文件时,应边读边处理或边写磁盘,而不是先完整
buffer; - 下载代理、转发响应时,可以直接
pipe,减少中间拷贝。
常见坑:同步、全量缓冲、错误处理
几个高频问题:
- 在流式场景误用 同步阻塞 API,卡住事件循环;
- 忽略
error事件,导致进程以未处理异常退出; - 没有 destroy/清理 流,在异常路径上泄漏句柄。
更稳妥的习惯:
- 每个 pipeline 都处理
error(或用stream.promises.pipeline/finished辅助); - 在
try/finally或错误回调里关闭流。
小结:Stream 是 Node I/O 的默认语言
- Stream 解决的是 大数据量与持续数据流 下的内存与时间问题;
- 背压 是
pipe带来的关键价值,手写事件时要自己模拟这套协议; - HTTP 与大文件 是 Stream 最能发挥优势的场景,优先用流式心智,而不是「先读全再处理」。
掌握 Stream 之后,再去看框架封装(Express、Fastify 等)里的请求体解析、文件上传,会清楚它们底层在帮你管什么。