能否删除fs.createReadStream已读取的文件数据?实现缓冲式流功能
关于Node.js流结合ffmpeg实现可回收缓冲区的解决方案
嘿,这个问题挺实际的——要7×24小时运行,还得让文件流起到缓冲区作用同时能清理已读数据,直接操作单个文件肯定行不通,我来给你详细拆解下可行的思路:
为什么直接删除已读数据不可行?
Node.js的fs.createWriteStream和fs.createReadStream都是基于普通文件的,而普通文件是线性存储的:
- WriteStream会持续往文件末尾追加数据,文件指针一直停在末尾;
- 如果你尝试用
fs.truncate截断已读的前半部分,比如把10GB的文件截断到剩下5GB(假设已经读了前5GB),WriteStream的文件偏移量还是原来的10GB位置,后续写入会在文件末尾补出空洞,不仅浪费磁盘空间,还会导致ReadStream读到无效的空字节。 - 更关键的是,只要有进程(不管是WriteStream还是ReadStream)还打开着这个文件,直接删除会在Windows下报错,Linux下也只是标记删除,直到所有文件描述符关闭才会真正释放空间,根本没法实时回收磁盘。
可行的替代方案
1. 滚动文件缓冲区(最推荐,适合长期运行)
这是日志系统常用的思路,核心是不用单个文件,而是自动切换多个文件存储数据,读完一个文件就安全删除它,完美适配你“顺序运行ffmpeg”的场景:
- 当当前写入的文件达到阈值(比如1GB,或者按时间比如1小时),就自动关闭旧文件,创建新文件继续写入;
- 因为你的ffmpeg是顺序运行的,每个ffmpeg进程可以写入当前的活跃文件,进程结束后再判断是否需要切换文件;
- ReadStream读完旧文件后,就可以彻底删除它——因为已经没有任何进程再写入这个文件了。
你可以用现成的第三方库简化实现,比如rotating-file-stream,它能自动处理文件轮转、旧文件清理,支持按大小/时间轮转,还能自动保留指定数量的文件。
2. 环形文件缓冲区(适合固定大小的场景)
如果你的磁盘空间有限,想固定占用一块磁盘区域当缓冲区,可以实现环形文件:
- 创建一个固定大小的文件(比如5GB),WriteStream写满末尾后回到文件开头覆盖旧数据;
- 同时需要跟踪ReadStream的读取位置,确保WriteStream不会覆盖还没读取的数据。
不过这种方式实现起来比较复杂,需要自己维护读写指针的同步,而且一旦ReadStream速度跟不上WriteStream,旧数据会被覆盖丢失,适合对数据丢失容忍度较高的场景。
3. 内存+文件混合缓冲区(小流量场景)
如果数据量不大,可以用内存缓冲区暂存一部分数据,当内存达到阈值再写入文件,读完文件后删除。但7×24小时运行的话,内存很容易溢出,所以只适合小流量的场景,不推荐作为长期方案。
关键注意事项
- 确保删除文件时无进程占用:不管用哪种方案,删除文件前必须确认WriteStream已经关闭该文件,ReadStream也已经读完并关闭了该文件,否则会出现文件占用错误或空间无法释放的问题;
- 控制磁盘占用上限:一定要设置旧文件的保留数量或总大小,比如最多保留3个1GB的文件,避免磁盘被占满导致服务崩溃;
- ffmpeg进程的流处理:每个ffmpeg进程的输出流要通过
{ end: false }参数pipe到WriteStream,避免单个ffmpeg进程结束时关闭整个WriteStream,影响后续进程的写入。
简单示例代码(滚动文件方案)
const fs = require('fs'); const { spawn } = require('child_process'); const rotatingFileStream = require('rotating-file-stream'); // 初始化轮转文件流:每个文件最大1GB,最多保留3个旧文件 const writeStream = rotatingFileStream('buffer-%Y%m%d-%H%M%S.dat', { size: '1G', maxFiles: 3, path: './ffmpeg-buffer' }); // 顺序执行ffmpeg进程的函数 async function runSequentialFFmpeg(commands) { for (const cmdArgs of commands) { const ffmpegProc = spawn('ffmpeg', cmdArgs); // 将ffmpeg的输出(这里假设是stdout,根据你的实际需求调整)写入轮转流 ffmpegProc.stdout.pipe(writeStream, { end: false }); // 等待当前ffmpeg进程结束 await new Promise(resolve => ffmpegProc.on('exit', resolve)); } // 所有进程结束后关闭写入流 writeStream.end(); } // 启动读取流的函数 function startReadingBuffer() { // 创建读取流,自动从最旧的文件开始读,读完继续读新文件 const readStream = rotatingFileStream.createReadStream('buffer-%Y%m%d-%H%M%S.dat', { path: './ffmpeg-buffer', start: 0 }); // 处理读取到的数据(替换成你的业务逻辑) readStream.on('data', (chunk) => { console.log(`读取到 ${chunk.length} 字节数据`); // 比如发送到其他服务、处理编码等 }); readStream.on('end', () => { console.log('所有已生成的缓冲区文件已读完,等待新数据...'); // 可以在这里监听新文件创建事件,继续读取 }); } // 启动读写流程 startReadingBuffer(); // 替换成你的ffmpeg命令数组,比如每个命令都是["-i", "input1.mp4", "-f", "rawvideo", "-"] runSequentialFFmpeg([/* 你的ffmpeg命令参数数组 */]);
内容的提问来源于stack exchange,提问作者Henry
相关产品推荐
相关产品推荐

