Node.js Stream 与背压:大文件与 HTTP 场景

在 Node 里处理「大文件、持续产生的数据、高并发下载上传」时,一次性 readFile / 拼字符串往往会把内存打爆。
这一篇围绕 Stream 展开,想讲清楚几件事:Stream 在解决什么问题、pipe 背后背压(backpressure)是什么意思,以及在 HTTP 与大文件场景里怎么避免把进程拖死。

典型反模式:

  • 读一个几百 MB 的文件 → readFile 整个进内存;
  • 下载一个大文件 → 先攒成完整 Buffer 再写盘。

问题:

  • 内存峰值随数据量线性上涨;
  • 开始写出结果的时间被推迟到「全部读完」之后。

Stream 的思路是:

  • 把数据切成一段一段(chunk)流动
  • 上游产生一点,下游就消费一点,内存里只保留有限窗口。

常见分类:

  • Readable:可读流(数据源);
  • Writable:可写流(数据汇);
  • Duplex:双工(可读可写,例如 TCP socket);
  • Transform:转换流(读入、加工、再写出,例如压缩、加解密)。

心智模型可以简化为:

  • 数据像水管里的水,流是管子;
  • Transform 是管子中间加了一个「处理站」。

readable.pipe(writable) 是最常用的组合方式,它做了几件重要的事:

  • 把 Readable 的数据自动写入 Writable;
  • 在 Writable 忙不过来时,暂停 Readable,避免内存无限堆积——这就是 背压 的核心含义。

可以把它理解成:

  • 下游说「我处理不过来了」,上游就要减速或暂停
  • 而不是上游一直往内存里灌。

如果手写 data 事件里不停 write,而不处理 drain 事件,就容易失去背压保护,造成内存暴涨。

典型安全写法:

  • fs.createReadStream 读;
  • fs.createWriteStream 写;
  • 中间需要处理时接上 Transform 或第三方流(例如 gzip)。

这样:

  • 峰值内存大致与 缓冲区大小 相关,而不是与文件总大小成正比。

在 Node 的 http / https 里:

  • IncomingMessage(请求体、响应体)往往是可读流;
  • ServerResponse 是可写流。

意味着:

  • 上传大文件时,应边读边处理或边写磁盘,而不是先完整 buffer
  • 下载代理、转发响应时,可以直接 pipe,减少中间拷贝。

几个高频问题:

  • 在流式场景误用 同步阻塞 API,卡住事件循环;
  • 忽略 error 事件,导致进程以未处理异常退出;
  • 没有 destroy/清理 流,在异常路径上泄漏句柄。

更稳妥的习惯:

  • 每个 pipeline 都处理 error(或用 stream.promises.pipeline / finished 辅助);
  • try/finally 或错误回调里关闭流。
  • Stream 解决的是 大数据量与持续数据流 下的内存与时间问题;
  • 背压pipe 带来的关键价值,手写事件时要自己模拟这套协议;
  • HTTP 与大文件 是 Stream 最能发挥优势的场景,优先用流式心智,而不是「先读全再处理」。

掌握 Stream 之后,再去看框架封装(Express、Fastify 等)里的请求体解析、文件上传,会清楚它们底层在帮你管什么。