Node 18 上读大 JSON 文件,别直接 require
线上有个离线任务,每天要读一批第三方导出的 JSON 文件,单个 60 到 200MB,Node 版本是 18.17,跑在 4C8G 的容器里。现象是任务刚起来 RSS 就冲到 1.2G 左右,两三个文件轮着处理时偶尔直接被 OOM kill,日志里连异常都没有。
一开始我以为是下游处理逻辑把数据全堆在内存里,后来把处理逻辑全注释掉,只留读取,内存照样飙。原因是我用了 require('./data/xxx.json')。
改成流式读之后稳定在 300MB 上下。核心做法是用 stream-json,逐条往外拿,不在内存里拼完整对象:
const fs = require('fs');
const { chain } = require('stream-chain');
const { parser } = require('stream-json');
const { pick } = require('stream-json/filters/Pick');
const { streamArray } = require('stream-json/streamers/StreamArray');
async function readItems(file, onItem) {
const pipeline = chain([
fs.createReadStream(file, { highWaterMark: 1024 * 1024 }),
parser(),
pick({ filter: 'items' }),
streamArray(),
]);
for await (const { value } of pipeline) {
await onItem(value);
}
}
如果文件本身就是一行一个对象的 JSONL,那更简单,用 readline 就够,不用上 stream-json:
const rl = require('readline').createInterface({
input: fs.createReadStream(file),
crlfDelay: Infinity,
});
for await (const line of rl) {
if (!line.trim()) continue;
await onItem(JSON.parse(line));
}
为什么 require 会这么吃内存:它走的是 CJS 加载器,文件先被完整读成字符串,再 JSON.parse 成对象,而且解析结果会进 require.cache,同一路径第二次 require 拿的是缓存,旧对象不会被回收。也就是说字符串和对象会同时在内存里存在一小段时间,峰值大概是文件体积的 3 到 5 倍。60MB 的文件峰值到两三百 MB 很正常,200MB 的直接就危险了。
流式读的代价是慢一些,stream-json 是纯 JS 解析,比原生 JSON.parse 慢好几倍,我这边 200MB 大概多花两三秒,但换来的是内存可预测。另外 pick 的 filter 要写对路径,写错了不会报错,只是拿不到数据,这个我踩过一次,排查了半天。
会失效的地方也说清楚:如果单个数组元素本身就很大(比如一条记录几十 MB),流式也救不了,内存还是会被单条撑爆;如果处理逻辑里把所有 item push 进一个数组,那等于把省下来的内存又还回去了,得边读边落库或者边写文件。还有 for await 里做 await onItem 会串行,吞吐不够的话要自己做并发控制,别直接 Promise.all 一把梭。
现在这个任务我加了 --max-old-space-size=768 兜底,跑了一周没再被 kill。想问问大家,这种离线大文件任务,你们是直接在 Node 里流式处理,还是干脆丢给 jq 或者别的进程先切分?
0 条评论
还没有评论,写下第一条。