用 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 条评论
还没有评论,写下第一条。