你的浏览器无法正常显示内容,请更换或升级浏览器!

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

tenfei
tenfei
发布于2026-10-16 16:34 阅读15次
Node.js 流式处理大文件的正确姿势
用 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`。 流的意义不只是省内存,更是让处理时间和内存占用都变得可控。对大文件来说,这是唯一正确的打开方式。

1

0

文章点评
Copyright © from 2021 by namoer.com
458815@qq.com QQ:458815
蜀ICP备2022020274号-2