这段时间做电视墙多进程解码和解码库的多进程解码实现时,一个必须面对和解决的问题就是进程间通信,针对具体的项目我到底采用什么样的通信模型呢?这里涉及到针对具体场景具体问题进行具体分析的问题。我们都知道不论在WINDOWS还是UNIX和LINUX等平台下,进程间通信的方式有很多,比如管道,消息队列,共享内存和SOCKET通信等等。在做具体的项目时并非所有的方式都去采用,而需要选择适合项目本身的通信方案。管道分为匿名管道和命名管道,前者只用于有亲缘关系的进程之中,后者的应用范围要广,可以适用于任意的进程之间,用得多的基本都是后者,因为灵活度比较大。管道是内核对象,实际上可以将其视为文件,用管道来进行通信时,进程之间需要做同步操作。消息队列我基本没有用到过,没觉得有什么好处在哪。共享内存是进程间最快的通信方式,其实就是申请一块内存,让不同进程去映射到那个内存地址进行读写。因为共用内存地址,通信时不需要做二次拷贝,只需要做同步操作。管道在UNIX下用得比较多,多用于输入输出操作,往往将上一个操作的输出做为下一个操作的输入。UNIX SHELL就支持用管道来进行操作,比如命令 ls -lrt|awk {print $1} 就是把ls 的结果当成awk命令的输入。共享内存适合的应用场景是多线程或者多进程对大量数据的读写,一般都是事先定义好的格式。SOCKET 很多情况下我们见到的都是用于网络通信,其实在进程间通信也常被采用,而且程序不论是单机还是分布式的布署基本不需要付出很大的维护成本。回到我们项目,多进程解码,需要在父子进程间进行通信,包括命令的控制和频繁的流数据发送和接收,选择怎样的通信方式我一开始的是比较纠结的。如果用共享内存速度无疑是最快的,但针对多个子进程来读取数据的实际情形,要不我就要在共享内存里做标识,要不就开多个命名的共享内存,让每个子进程读写自身的共享内存,需要对命令和流数据封包到共享内存并从共享内存解包,并且要对进程之间做同步操作,还有共享内存的管理等问题,复杂性不低。越复杂的事情风险就越大。如果用管道,照样要针对每个子进程开设管道,自定义封包和解包的协议,进程之间做同步操作,与共享内存差不多,在性能方面并不比共享内存理想。最终决定采用SOCKET通信来做父子进程之间的通信方案。主要是考虑几个好处,一是客户端和服务器的耦合性低,双方通信的控制由操作系统来做,并不需要我去做同步的处理,处理起来相对简单一些,二是有一些成熟的RPC调用框架帮我们做好了封包和解包处理,省去自定义协议的麻烦。
具体采用什么样的网络通信方式呢:
原生的网络通信,我们都知道一般是采用TCP流通信和UDP包通信,前者面向连接的通信,安全稳定,不利的是性能较低一些,且要处理分包粘包等问题,后者性能略高,不需要处理分包粘包等问题,但并不保证包的次序,也没有重发机制,除非程序自身来做。针对网络通信,有一些有名的开源库,我所知道的有ACE,ICE,libEvent,ZeroMq等。下面针对这些通信库一一做个简单的介绍。
ACE,是一个重量级的网络通信构架,当初一个人开发,基本是科研型的,里面用到大量的设计模式,包含大量的实用类,比如同步的Ace_Gurad,主动对象AceEvent等。主要支持反应器Reactor和前摄器Proactor模型。ACE支持跨平台通信,比较全面,但对LINUX的封装不是太好,如果要做RPC调用,需要自定义协议或者用GOOGLE的Protobuffer。
ICE,设计的主要目的是RPC调用,自定了SLICE语言和相应的解释器,支持跨平台和多种语言的代码生成。我认为其RPC调用是最吸引人的地方,方便分布式应用的同时减少了封包解包的麻烦。与其类似的还有一个FACEBOOK的thrift框架,同样提供的代码生成工具和脚本语言,但开发社区的人不多,感觉支持不多。
libEvent,一个轻量级的网络框架,用回调的方式来客户处理相应的事件,常用于文件,套接字描述符,定时器,信号等事件的处理。网络通信中通过针对套接字注册读写,出错等事件处理函数来处理相应的事件。其中的bufferevent的读写操作可用来做为网络的接收和发送。libEvent,是一个中性的平台,无论是服务器还是客户端采用它,对端都可用原生的SOCKET通信,但它的事件处理循环是在一个线程中,如果监听多个套接字的事件,最好收发数据都在这个单独的线程中来完成。如果我多线程操作,需要取出其FD直接操作,但这样并不安全。如果大量的数据发送通过libEvent来做,需要拷贝到相应的数据结构,比如队列等,然后libEvent的线程从队列中读取再发送,会影响一定的性能。这样还不如直接用TCP来的直接,所以没有采用libEvent。
ZeroMq,一个非中性的网络通信平台,服务器和客户端都需要用ZeroMq来做实现。ZeroMq 号称是史上最快的消息队列,内部每个连接分配一个无锁消息队列。ZeroMq帮用户处理好了分包和粘包的问题。ZeroMq 主要支持请求-应答模式,发布-订阅模式和推送-接收模式。请求-应答模式为一部一答的同步模式,适合做RPC的调用,但封解包需要自我实现,发布-订阅适合于微信朋友圈,微博等这些场景,并不保证接收的。推-拿模型适合做任务集群,任务发给一个客户程序就不会发给另外一个。前面这些模型都一个我不喜欢的问题,那就是,如果做为服务器,我根本不知道是哪个客户连接上了服务器,因为服务端口只有一个单点的zmq_socket,如果服务端要主动发数据给客户端,就不方便了。后来我发现有一个ROUTER-DEALER模型还算有用,可以让客户端发数据时把标记ID一起发过来,知道了客户端的标记ID,服务端就可以发消息给客户端了。不过消息得发两次,先发ID标识,再发实质消息。
结合实际的项目,ICE,ZeroMq,原生和TCP是比较好的通信模型。
具体项目讲解:
A.电视墙多进程解码
电视墙多进程解码最初采用了ICE来做网络实作,涉及的命令很少,基本都用在数据发送这块,解码的子进程开了十个,数据传送通过string对象走ICE RPC异步调用。结果是工作得还算顺利,但存在的一个问题是ICE调用时对CPU的占用比较高,父子进程光收发数据就占用到了百分之二十多,原因在ICE调用层次过深,每次发数据都会检测连接等操作,这样做命令操作还好,但我们要每秒往每个解码进程发送25桢音视频数据,十个客户端就是每秒250桢。因为设计上是电视墙每个独立播放窗口对应一个相应的解码进程,如果电视墙开的窗口越多,CPU占用会直线上升。这种结果显然不能接受。这里简单说一下我的机器资源。
后来采用ZeroMq来做通信,开十路子码流,收发数据时CPU降低到5%以下。但通过ZeroMq来发数据时,服务端只能通过一个单独的zmq_socket来发送,实质上在内部是通过各个连接的FD发送的,多个线程发送时需要竞用 zmq_socket资源。
B.解码库多进程实现
原有的解码库并不稳定,因为里面有对第三方库的调用,新的解码库要求做成多进程解码功能,让每一路请求都有独立的解码子进程来完成,原来的解码库只保留以前定义的接口,这样原有的用到此解码库的程序可以不用编译直接采用新的库。实际的针对第三方库的调用都在解码子进程中完成,就算解码子进程崩溃,也不影响使用库的程序。
解码库中声明的接口约有三十来个,如果自己来定义结构体来做参数的封包和解包是个麻烦事,并且不一定可靠,用protobuffer来做应该比较方便,但对这个不很熟悉,在项目比较紧的情况下,用比较熟悉的ICE来做RPC实现是不错的选择。用RPC来做命令控制效果是比较理想的,但发数据的接口不能通过ICE来做,之前有个教训,ICE还是不足够快。用ZeroMq吗?这个东西也有些局限,对多个线程同步带来的阻塞比较大。决定用原生的TCP来做,每个解码进程登录时注册自身的ID,父进程记录下ID和SOCKET FD的映射关系。发数据时可直接操作FD来发送。开10路解码测试,光收发数据,CPU占用不超过10%,在处理频发数据方面优于ICE。TCP要注意的问题是要正确处理分包和粘包的问题,一般用变长结构体来发数据,数据头要数据长度的描述,接收方解析头,然后读取余下的字节。