用 Node 的流把日志文件切片,我踩了两个坑

上周处理一个线上问题,需要把一台机器上 2.4GB 的 access.log 按小时切出来,再统计各小时 5xx 的比例。环境是 Node 20.11,Linux,容器里内存只给了 512MB。第一版我图省事用了 fs.readFileSync 读整个文件再 split,进程直接被 OOM kill,一次都没跑完。

后来改成流式按行切分。核心是 readline 配 createReadStream,输出用 createWriteStream 按小时懒创建:

const fs = require('node:fs');
const readline = require('node:readline');

const rl = readline.createInterface({
  input: fs.createReadStream('access.log', { encoding: 'utf8' }),
  crlfDelay: Infinity,
});

let current = null;
let currentHour = null;

rl.on('line', (line) => {
  const hour = line.slice(1, 14); // 假设行首是 [10/Oct/2024:13
  if (hour !== currentHour) {
    currentHour = hour;
    current = fs.createWriteStream(`log-${hour.replace(/[:/]/g, '-')}.log`);
  }
  current.write(line + '\n');
});

rl.on('close', () => current?.end());

跑起来内存稳在 60MB 左右,两分多钟切完。但接着就出了第一个坑:current.write() 返回 false 时我没管,输出流背压把内存又顶上去了,峰值到了 300MB。补上判断后压回 70MB 以内:

if (!current.write(line + '\n')) {
  rl.pause();
  current.once('drain', () => rl.resume());
}

第二个坑是 crlfDelay。日志里有 Windows 机器写进来的行,结尾是 \r\n,不设这个参数时 line 末尾会留一个 \r,按行写回去没问题,但我后面拿 line.split(' ') 取字段时最后一列多了个 \r,状态码 200 变成了 200\r,统计全错。这个参数不解决性能,纯粹是行尾规范化的,Infinity 就够。

为什么这样取舍:流式读的代价是没法回头,切错了要重跑;但 2.4GB 全读进内存这条路在这个容器里走不通。按小时懒创建写流的好处是同时最多一个句柄;缺点是如果日志时间戳乱序,会不停开关文件,我这份日志基本有序才敢这么做。

边界也很明确:如果单行超过默认的 64KB,readline 会截断,得自己按 \n 切 buffer;另外写流错误没监听的话,磁盘满了会静默丢数据,我给每个流挂了 error 事件才踏实。

你们线上切日志一般用 logrotate 还是自己在进程里做?我这边容器里没装 logrotate,想找个不重启进程的方案。

0
赞同度 100% · 0 条评论 · 6 阅读

0 条评论

支持 Markdown · 回复会实时推送给对方

还没有评论,写下第一条。