Janus Gateway是一个webrtc的server,一种插件式架构,基于这个架构,开发了一些列的插件,比如streaming、SIP、videoroom、audiobridge等,甚至支持lua、javascrip等脚本性语言去处理会话和音视频内容等等,毫无疑问是个集大成者。
那我们原生的Janus适合生产环境部署不?能支持多少并发量?
我们目前的业务主要用到了videoroom、audiobridge和lua三个插件,在8核16G并且开启loop=8(ice loop的feature,及RTP/RTCP/DataChannel的收发开启了线程池的模式)的环境配置下,我们测试的情况如下:
- audiobridge支持的并发是300路64K码率的音频,这个跟他们的专业测评是一样的,请参考https://core.ac.uk/download/pdf/74316352.pdf ,且延迟在300路时大道了500毫秒左右;
- 测试videoroom的时候,我们设置的脚本是每隔60毫秒加入一路视频,到1000路之前,每次都会core dump,无法继续测试;
以上是性能压测的一个结果,实际上我们在继续研究它的设计时,也有一些发现,好的方面就不详细描述,大致描述下不足,或者个人认为的不足:
- 函数代码过长,甚至有多个函数代码超过2000行,这让二次开发和维护的困难较大,第一次开发个静音通知的业务需求时,足足看了3天的代码,最后还存在一个BUG,就是重复通知导致APP重复显示联系人,可以这么说在国内的很多公司都不太可能出现这样的状况;
- 插件间存在大量冗余的代码,甚至有些业务的实现还不一致,比如token的业务,audiobridge和textroom、videoroom的实现不一致,且三个插件里重复代码;
- libnice和janus都是基于glib开发的,glib的对象对内存是有缓存的,这就给内存方面的bug定位带来了困难,比如内存泄露;
- libnice和janus的事件循环是基于glib的GMainContext和GMainLoop的,在它们之上实现事件源的接口就可以收发事件了:
struct GSourceFuncs {
gboolean (*prepare) (GSource *source,
gint *timeout_);
gboolean (*check) (GSource *source);
gboolean (*dispatch) (GSource *source,
GSourceFunc callback,
gpointer user_data);
void (*finalize) (GSource *source); /* Can be NULL */
}
这样的一个实现意味着你如果想要对事件源进行操作就必须挨个对每个事件调用prepare函数,这显然是低效的,而且该事件循环基于poll实现,poll的性能明显不如epoll。
并发模型采用线程的模式,线程间使用mutext同步原语的GAsyncQueue;janus core使用线程池,ice loop使用线程池,每个插件创建handle线程去处理接口,audiobridge的每个房间创建一个mixer线程去混音,并且每个participant都创建一个线程去opus编码混音后的数据,更不可接受的是mixer线程调用sleep去间隔性取音频数据解码并混音;所有这些线程关系导致几乎每个对象每条消息处理都需要使用mutext或者原子操作去同步,这并不是一个好的并发模型。
在处理SDP的offer和answer时使用sleep这个野蛮的方式等待收集地址完成后的callback,并发量不大的情况下还可接受,如果并发量较大并发性接入服务器,会出现部分客户端接入时因ICE过程而失败,也较容易出现黑屏或者声音断断续续的问题;
/* Are we still cleaning up from a previous media session? */
if(janus_flags_is_set(&ice_handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_CLEANING)) {
JANUS_LOG(LOG_VERB, "[%"SCNu64"] Still cleaning up from a previous media session, let's wait a bit...\n", ice_handle->handle_id);
gint64 waited = 0;
while(janus_flags_is_set(&ice_handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_CLEANING)) {
JANUS_LOG(LOG_VERB, "[%"SCNu64"] Still cleaning up from a previous media session, let's wait a bit...\n", ice_handle->handle_id);
g_usleep(100000);
waited += 100000;
if(waited >= 3*G_USEC_PER_SEC) {
JANUS_LOG(LOG_VERB, "[%"SCNu64"] -- Waited 3 seconds, that's enough!\n", ice_handle->handle_id);
JANUS_LOG(LOG_ERR, "[%"SCNu64"] Still cleaning a previous session\n", ice_handle->handle_id);
janus_sdp_destroy(parsed_sdp);
return NULL;
}
}
}
而且这段代码很不好重构。
- videoroom在1.0版本前依然不支持一个peerconnection去拉取多个流,导致每有一个publish stream的用户加入,现有用户都要去再次创建peerconnection,然后subscribe这路stream,那么在多人房间的时候,这个设计给网络、客户端性能和服务端性能都带来了极大的挑战,所以体验也会很差。
以上种种问题导致Janus的author Lorenzo Miniero也会感叹“I’m getting older, but unlike whisky, I'm not getting any better author of the Janus WebRTC Server,and the only viking with no muscles”,当然他很谦卑,也在自嘲,但是的确我们在测试性能的时候,以及后续的优化都碰到了很多的困难,甚至有时候很莫名其妙。
那么针对以上问题我们又是如何优化的呢?