根本原因是ffmpeg串行执行时独占CPU、磁盘I/O和内存资源;应改用proc_open()实现非阻塞、可超时、可终止的进程控制,并严格处理路径转义、stderr捕获与timeout设置。

PHP调用ffmpeg加水印为什么慢
根本原因不是PHP本身,而是ffmpeg串行执行时独占CPU、磁盘I/O和内存资源。单个视频加水印可能耗时几十秒,10个就卡住整个Web请求;exec()默认阻塞,超时设置不当还会拖垮PHP-FPM子进程。更麻烦的是,用户上传后干等,体验差,还容易触发Nginx或Apache的超时中断。
用proc_open()替代exec()控制进程生命周期
exec()黑盒执行,没法杀进程、读实时日志、判断是否卡死;proc_open()能拿到stdin/stdout/stderr句柄,配合stream_select()可实现带超时的非阻塞等待,还能在超时时主动proc_terminate()清理残留进程。
- 必须设置
timeout参数(比如120秒),避免某个坏文件把整个队列拖死 - stderr流一定要捕获,
ffmpeg报错如No such file或Invalid data found都从这里出来 - 不要直接拼接路径,全部用
escapeshellarg()包裹,尤其是含空格或中文的$videoPath - 示例关键片段:
$descriptors = [ 0 => ['pipe', 'r'], 1 => ['pipe', 'w'], 2 => ['pipe', 'w'] ]; $process = proc_open($cmd, $descriptors, $pipes, '/tmp', $_ENV); if (is_resource($process)) { $start = time(); while (proc_get_status($process)['running'] && (time() - $start) < 120) { usleep(50000); } if (proc_get_status($process)['running']) { proc_terminate($process, 9); } }
多进程分发任务而不是多线程
PHP的pthreads扩展要求ZTS编译、仅限CLI、Windows支持差,线上环境几乎不可用;真正稳定的做法是用pcntl_fork()启动多个子进程,每个处理一个视频,父进程用pcntl_waitpid()回收。注意:Web服务器(如Apache mod_php)不支持pcntl,必须跑在PHP-FPM的CLI模式或Supervisor下。
- 子进程数建议设为CPU核心数×1.5,太多反而因上下文切换拖慢整体速度
- 每个子进程完成任务后,必须
exit(0),否则会继承父进程所有资源(包括数据库连接、打开的文件句柄) - 不要在子进程中调用
session_start()或写全局log,容易冲突;结果统一写入临时JSON文件或Redis队列 - 用
pcntl_signal(SIGCHLD, function($sig) { pcntl_waitpid(-1, $status); });防止僵尸进程
异步调度必须加消息队列兜底
即使做了多进程,突发大量上传仍可能打爆内存或进程数上限。生产环境必须把“加水印”这个动作从HTTP请求中剥离开——前端上传完立刻返回任务ID,后端用Redis List或RabbitMQ存任务,再由常驻Worker进程消费。
立即学习“PHP免费学习笔记(深入)”;
- 任务结构至少包含:
video_path、watermark_type(text/image)、position(右下角/居中)、callback_url - Worker进程要定期检查
ffmpeg是否存活,挂了就自动拉起,不能靠crontab轮询 - 失败重试最多2次,第三次进死信队列人工干预,避免无限循环消耗资源
- 临时文件(如截图帧、中间水印图)必须用唯一前缀+
unlink()及时清理,不然磁盘迟早写满
实际最难的不是写fork或proc_open,而是当某个ffmpeg进程卡在IO等待、又没输出stderr时,你怎么判断它真死了还是只是慢——这得靠组合信号、超时、资源监控三重判断,漏掉任何一环,队列就会慢慢淤积。



















