Node.js 流式处理大文件的正确姿势

发布于2026-10-16 16:34 阅读15次 用 readFile 处理几百 MB 的日志直接把容器读爆。本文讲清流为什么能控制内存,给出管道、按行处理、背压的完整示例,并指出 pipe 不传播错误、提前收尾、混用同步方法、高水位设置不当这四个坑,最后附可直接使用的转换脚本。
## 一次内存飙升
线上有个日志处理脚本,读一个几百 MB 的日志文件做格式转换。`fs.readFileSync` 一把梭,RSS 直接冲到 1.2GB,容器被 OOM kill。改成流式之后,常驻内存降到 40MB 左右。
这篇讲 Node.js 里流的基本用法和几个必须注意的点。
## 为什么 readFile 会炸
`fs.readFileSync` / `readFile` 会把整个文件一次性读进内存。读 100MB 的文件,内存就多占 100MB 以上——字符串、Buffer、中间结果叠加起来,实际占用往往是文件大小的数倍。
如果只是偶尔读个小配置没问题,但处理日志、视频、压缩包这类大文件,必须用流。
## 最基础的写法
```js
const fs = require('fs');
const rs = fs.createReadStream('big.log', { encoding: 'utf8' });
const ws = fs.createWriteStream('out.log');
rs.pipe(ws);
```
`pipe` 自动处理背压(backpressure),写不过来时会暂停读。这是最简单的正确用法。
但实际需求往往要边读边处理:
```js
const readline = require('readline');
const rl = readline.createInterface({ input: rs });
rl.on('line', (line) => {
// 逐行处理
});
rl.on('close', () => console.log('done'));
```
`readline` 按行切分,处理日志最顺手。
## 坑一:pipe 不处理错误
`rs.pipe(ws)` 本身不会传播错误。源文件读取失败或者目标写入失败,如果没有监听 `error` 事件,进程可能直接崩溃或者静默退出。
```js
rs.on('error', (err) => console.error('read error', err));
ws.on('error', (err) => console.error('write error', err));
```
两个都要挂,一个都不能少。
## 坑二:忘了 await / close 事件就以为结束了
流是异步的。很多人写完 `rs.pipe(ws)` 紧接着执行后续逻辑,此时文件还没写完。一定要等结束事件:
```js
ws.on('close', () => {
console.log('写入完成,可以做后续处理');
});
```
用 `stream/promises` 里的 `pipeline` 更稳,它会自动处理错误和清理:
```js
const { pipeline } = require('stream/promises');
await pipeline(rs, transform, ws);
```
**推荐用 `pipeline` 代替 `pipe`**,错误处理和资源清理都省心。
## 坑三:同步方法混进流里
在 `rl.on('line')` 里调用 `fs.writeFileSync`,等于把异步流写成同步,背压完全失效,内存又会涨。处理函数里如果有重活,要么异步化,要么用 `Transform` 流正确连接。
## 坑四:highWaterMark 用默认值不管
默认高水位 16KB(对象模式是 16 个对象)。处理超大文件时,这个值偏小,切分次数多、CPU 开销大。可以适当调大:
```js
fs.createReadStream('big.log', { highWaterMark: 1024 * 256 });
```
但别一味调大——它直接影响内存占用,256KB 到 1MB 是比较合理的区间。
## 一个完整例子
```js
const fs = require('fs');
const { pipeline } = require('stream/promises');
const readline = require('readline');
async function convert(input, output) {
const rs = fs.createReadStream(input, { encoding: 'utf8', highWaterMark: 1024 * 256 });
const ws = fs.createWriteStream(output);
const rl = readline.createInterface({ input: rs, crlfDelay: Infinity });
let count = 0;
rl.on('line', (line) => {
if (line.trim()) {
ws.write(`[${++count}] ${line}\n`);
}
});
await new Promise((resolve, reject) => {
rl.on('close', resolve);
rl.on('error', reject);
});
ws.end();
await new Promise((resolve) => ws.on('close', resolve));
}
```
## 判断标准
> 文件大小超过 100MB,就别用 `readFile`。
流的意义不只是省内存,更是让处理时间和内存占用都变得可控。对大文件来说,这是唯一正确的打开方式。