读个几百兆日志把进程搞崩:用 Node 流式按行读取
有次要从一个三百多兆的日志文件里筛错误行,我图省事用了 fs.readFile,结果进程直接被 OOM 干掉了。原因很简单:readFile 会把整个文件一次性读进内存,三百兆文件就是三百兆内存,再加字符串处理,扛不住。正确做法是用流,一点一点读,内存占用始终保持在一条线的水平。
一、用 readline 按行读
Node 内置的 readline 模块配上文件流,天然就是按行处理的,不用自己拼缓冲区。
const fs = require('fs');
const readline = require('readline');
const rl = readline.createInterface({
input: fs.createReadStream('app.log'),
crlfDelay: Infinity
});
let total = 0, errors = 0;
rl.on('line', (line) => {
total++;
if (line.includes('ERROR')) {
errors++;
console.log(line);
}
});
rl.on('close', () => {
console.log(`总行数 ${total},错误行 ${errors}`);
});
rl.on('error', (err) => console.error('读取失败', err));
crlfDelay: Infinity 这个参数是为了正确处理 Windows 的 \r\n 换行,不然有些行尾会多个 \r。line 事件每读到一行触发一次,处理完这一行就可以被回收,内存不会涨。
二、为什么流不会撑爆内存
fs.createReadStream 会分块读取,默认每块 64KB,读一块处理一块。readline 在这基础上负责把块拼成完整的行再交给你。整个过程内存里只有当前在处理的一行和少量缓冲区,文件多大都不影响。
如果不需要按行,只是想把文件搬个地方,那更省事,直接 pipe:
fs.createReadStream('big.zip').pipe(fs.createWriteStream('copy.zip'));
两端都是流,数据像水一样流过去,全程不落地内存。
三、踩坑提醒
第一,别用同步版本。readFileSync 会阻塞整个事件循环,服务端一用就全体请求排队,只能在脚本启动时读配置文件这种场景用一下。
第二,异步的 readFile 也不是万能的。它虽然不阻塞,但仍然是一次性把内容读进内存,大文件照样爆。判断标准很简单:文件几十兆以上,或者大小不确定,就用流。
第三,处理速度跟不上读取速度时要考虑背压。流默认会自己处理,但如果你在 line 回调里做了耗时的异步操作,数据会堆积在内存里。这种情况可以用 pause / resume 手动控制,或者把处理逻辑也做成流串联起来。
第四,别忘了错误监听。文件路径写错、权限不够、磁盘满了都会在流的 error 事件里抛出来,不监听就是一个未捕获异常,进程直接退出。
第五,编码要指定。默认是 utf8,遇到 GBK 之类的编码文件会乱码,这种得先用 iconv-lite 之类的转码,再交给 readline。
第六,处理完记得清理资源。流用完后调用 destroy,或者让它自然结束。循环处理多个文件时,不关流会耗尽文件描述符,报 EMFILE 错误。
第七,中间想做转换可以串一段 Transform 流。readline 适合按行,如果要改格式、过滤字段、按规则切分,就在读和写之间插一个 stream.Transform,三段用 pipe 连起来,比自己维护缓冲区干净得多,也不用担心背压。
第八,别在回调里攒一个会无限变大的结构。图省事把匹配到的行全 push 进数组,几百万行下来内存照样爆,等于白用流了。要留就只留关键字段,或者边处理边写进输出文件。
四、怎么验证真的没吃内存
跑起来之后看一眼进程内存最直观。Linux 上用 /usr/bin/time -v node xxx.js 看 Maximum resident set size,或者在脚本里定时打印 process.memoryUsage().rss。同一个文件分别用 readFile 和流跑一遍对比,你会看到前者的内存峰值跟文件大小成正比,后者基本稳定在几十兆,差距非常明显。
五、小结
大文件处理的第一原则就是别一次性读进内存。Node 的流把文件切成小块逐块处理,readline 让你能按行操作,pipe 让搬运数据变得一行搞定。记住几个要点:文件几十兆以上用流、别用同步 API、错误要监听、不用了要销毁。这几条做到,处理几个 G 的日志也不会把进程搞崩。
- 点赞
- 收藏
- 关注作者
评论(0)