基于Java+FFmpeg搭建一个处理音视频的处理中台。
主要的架构设计分为两块内容。
一个对外提供服务的音视频处理任务中台,承接来自业务方的所有音视频处理需求,核心逻辑包括
- 将音视频处理任务落库
- 将音视频任务发送至消息队列
- 消费来自消息队列的消息,将音视频处理结果更新到数据库
- 通过回调任务,将结果回调给各个业务方
一个承接所有音视频任务的处理集群,核心逻辑包括
- 消费来自消息队列的消息
- 通过FFmpeg进行处理
- 将处理好的文件上传至OSS服务
- 将处理结果发送至消息队列
音视频处理任务中台
中台的考虑很简单,能承受住业务方的所有流量即可。内部需要的角色也较为简单。
分布式JOB
用于处理音视频回调通知,所有提交过来的任务都必须携带回调地址,比如Http接口。
分布式MQ
作为中台与逻辑处理集群的通信方式,最开始为了简单快捷起见,选择了使用Redis(lpush + bpop)作为消息中间件来处理,当下来看是一个错误的决定。后来统一完成了Kafka。
为何不能是Redis作为消息中间件
1、生产方与消费方的处理速率根本不再一个量级,当消费方处理速率比生产方低时,消息会大量积压,进而形成了一个Big key
2、Redis有丢失的风险,所以生产方需要做好一个合适的二次投递机制
3、缺少MQ部分功能,比如可靠的重试机制
音视频任务处理集群
该服务主要基于Java + FFmpeg来实现,因为业务特性的问题,FFmpeg是极其吃性能,特别是CPU,所以机器的性能会更好。
此处设计之初的目标是能快速的复制,当业务量突然进来时能通过加机器来抗住,所以节点必须是无状态,其次为了保证节点的稳定性,我们也考虑了进程内任务的一些排队处理方案。
思路
简单的描述一下当前的一些程序设计以及考量,并非是绝对的正确,因为很多方案选择的时候是需要考虑时间成本、人力成本的,只能慢慢通过演进的方式找到最佳实践。
为何要放在后端
如果客户端可以做,一定要放在客户端做。因为后端如果来处理这种大规模的音视频内容,所需要的成本太高了。放在客户端做,不管是从体验还是成本都是更优的方案。(但是也要考虑一些性能不是很好地机子去处理,防止程序卡死)
有兴趣的可以在本地的机子上体验一下FFmpeg,简直就是CPU猛兽。
部署
毫无疑问,必须是Docker。将FFmpeg的打包到镜像中,节点的复制是镜像级别的复制。纯jar包的形式手工运维很痛苦,加机器还要处理FFmpeg,包括下载,解压,配置环境变量一系列操作,即便写成脚本也是运维同学的一个心智负担。
Java CV
最开始我们并未注意到JavaCV,所以先通过Java Process+FFmpeg+Shell做了第一版功能的实现。
后来注意到Java CV,通过依赖POM的形式就能把FFmpeg集成到Java程序中,毫无疑问比起通过Java Process来调用命令行工具更让我们心动。
可是JavaCV的学习曲线看起来很陡,目前也没有找到比较合适的学习资料,传送门,虽然能够实现我们所需要的相关功能,但是唯恐后续的成本太高甚至有可能切会Process的方式,所以我们当下仅作为调研方案参考。
并行处理
我们在考虑要不要使用线程池来并行消费MQ。
维护要并行处理?因为要更好的利用CPU,但是熟知FFmepg的同学一定知道,它是非常吃CPU性能的,并且FFmpeg本身就做了并行处理的多线程技术方案,那还需要并行处理吗?
在CPU密集型的任务中,其实是不建议开启多线程,因为本身CPU就很忙了,多线程下多了一些CPU的上下文切换,反而造成了性能的损耗。
答案是要,但是要拆分来看。把整个任务处理分成多个段来看,首先是FFmpeg处理,处理成功后要把本地文件上传至OSS服务器上,然后发出MQ,最后将本地文件删除。
拆分后可以很好的将任务分成CPU密集型+IO密集型两段,所以我们可以把后续的IO密集型线程池化处理。
Process
想来这是服务端开发这个方向上,Process应该算是比较冷门的API。
-
waitFor(long timeout, TimeUnit unit) 而不是 waitFor()
,小心因为子进程阻塞把父进程耗死。 -
getInputStream + getErrorStream + close
,两个流的内容都打印,遇到问题好排查 -
waitFor timeout 后调用 destroyForcibly
,小心子进程丢失在父进程的管控下 -
exitValue
,0表示成功,其他表示失败,比如1表示主动destroy, 6表示process 被hangs, 137表示子进程被其他进程kill
CosClient 踩坑
在使用CosClient不当时,还遇到了连接池泄露的问题。
程序设计
我们的程序设计改了两三次。
第一次设计忙于调研+交付,没有足够的时间来做设计,也没有进行合理的抽象与规划,导致第二个需求进来时几乎涉及大量复制,代码重复明显,所以进行初步设计。第二次设计又因为业务交付时间紧急只做了一部分的抽象。第三次忍无可忍在周末重新做了一遍设计。
核心逻辑主要通过责任链来组织核心代码。
前文提到把任务处理分成多个段来看,把任务分成了主要的CPU密集型+IO密集型的方式,通过责任链可以把任务拆分成更多的节点,让程序的扩展性更好。
幂等
为啥要做幂等?主要考虑MQ重试,重复投递等。
令牌桶
为啥要做令牌桶?考虑到当下我们申请的服务器的核数申请的比较高,单个任务并没有把我们的CPU核数给彻底跑满,或者接近满,所以设计了一个小的线程池来提高并发(SynchronousQueue)。
令牌桶当下我们有2个选择,原生JDK中的Semaphore,以及自定义基于Redis LPush + LPop的命令实现的令牌桶。
最后选择基于Redis LPush + LPop 的方案。为何?性能并不是我们的第一指标,Semaphore不支持动态的调整令牌个数,想要实现动态的调整令牌个数需要自定义实现。而基于Redis的方案成本很低,通过配置中心或者暴露出来的运维接口就可以较好的动态调整(令牌减少需要跟业务线程竞争令牌)。
推荐动态的去调整线程池大小,而不是令牌桶方案,这也是我们下个迭代会考虑的方案,并且对我们而言改动很小。
CPU线程池
前文已经说过了,绝大多数的任务并没有把我们的CPU的核数给彻底跑满,甚至还有不少富余,所以适当的增加并发是有必要的。
IO线程池
毋庸置疑,IO线程池干的就是大量IO的操作,适当的增加线程池的大小
FFmpeg的线程数
尝试通过限制FFmpeg线程数+CPU线程池的方式来合理的提供并发,后来发现并无优势,而FFmpeg内部的并行方案没有看到相关的源代码并不好判断,所以现在的方案是去掉了这些参数,由FFmpeg自行动态的调整。
filter_threads
filter_complex_threads
转码效率
在很多时候,需要面临处理一些特别大的视频,为了提高效率,我们需要通过一些参数来优化,但同时可能会损耗原视频的质量,评估的核心还是损耗后的质量是否在可接受范围呢。
根据码率、分辨率等参数来判断视频的质量,如何获取码率等信息?建议使用ffprobe工具,能够提取到音视频很多信息并以Json的格式返回。
具体参数:preset,参考:https://trac.ffmpeg.org/wiki/Encode/H.264