用 Node 的 spawn 跑 ffmpeg,我卡在 stderr 缓冲上

上周在一台 4 核 8G 的 CentOS 7 机器上,Node 18.17,要把用户上传的 200 多个 mp4 批量转成 720p。每个文件 30MB 到 400MB 不等,用的是 fluent-ffmpeg 包了一层 child_process.spawn。现象很怪:大部分任务正常,个别超过 300MB 的文件会卡住,进程不退出,也不报错,CPU 占用掉到接近 0。

排查下来不是 ffmpeg 本身的问题。我手动跑同一条命令,ffmpeg 一直在往 stderr 输出进度,转换正常完成。回到 Node 里,我把 spawn 的 stdout/stderr 都设成了 pipe,但只监听了 close 事件,没消费数据。

问题在这:ffmpeg 默认把进度信息写 stderr,输出量不小,超过管道缓冲区(Linux 上一般是 64KB)之后,子进程写不进去就阻塞,父进程又没读,双方互相等。文件小、进度输出少的时候不会触发,所以看起来像随机的。

我的改法是两件事。一是把 stderr 显式消费掉,只保留最后一段用于报错:

const { spawn } = require('node:child_process');

function runFfmpeg(args) {
  return new Promise((resolve, reject) => {
    const p = spawn('ffmpeg', args, { stdio: ['ignore', 'ignore', 'pipe'] });
    let tail = '';
    p.stderr.on('data', (chunk) => {
      tail = (tail + chunk.toString()).slice(-4096);
    });
    p.on('error', reject);
    p.on('close', (code) => {
      if (code === 0) resolve();
      else reject(new Error(`ffmpeg exit ${code}: ${tail}`));
    });
  });
}

二是如果确实要看进度,加 -progress pipe:1 -nostats,让 ffmpeg 输出结构化的进度到 stdout,再按行解析,比读 stderr 稳。

这么做的代价是内存里最多留 4KB 的错误尾巴,正常情况下够定位问题,但如果 ffmpeg 的错误信息很长,会被截断。我接受这个取舍,因为完整日志我另外用 -loglevel error 落到文件里了。

会失效的地方:如果你把 stdout 设成 pipe 又不读,同样会卡,只是 ffmpeg 默认不往 stdout 写内容,容易忽略。另外用 stdio: 'inherit' 在容器里也可能因为日志收集端不读而阻塞,这点我还没完全验证,不确定是不是所有运行时都一样。还有 close 和 exit 的区别,exit 触发时 stdio 可能还没关完,用 close 更稳。

你们批量跑外部命令的时候,是直接 inherit 还是统一收进日志文件?容器里 inherit 被日志驱动拖住的情况有人遇到过吗?

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

0 条评论

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

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