如何使用开源SFU构建RTC云服务
大家好,我是来自百度智能云的李永兴,在百度智能云媒体云团队主要负责RTC产品的研发工作。
01
开源SFU的现状与不足
在研发RTC产品的过程中,我们调研了许多优秀的开源WebRTC服务器,例如:Janus、MediaSoup、Licode、SRS4等,这些SFU都有不同的设计理念和特点,我们从中受益颇多。同时我们也发现如果要基于这些优秀的开源的SFU构建一个高可用高并发的RTC云服务,就必须对这些SFU进行相应的改造。本次分享会主要介绍这些“改造部分”,这些改造其实具备一些普遍性,即针对开源SFU普遍存在的问题进行优化和改造,并不局限于某一特定的SFU。
RTC云服务的要求
要想构建一个RTC云服务,存在以下几点要求:
-
高并发:RTC云服务必须要支持海量并发用户,同时还需要支持海量房间。
-
高性能:除了单机性能,能抗更多的流次外,还要具备更高的连通率,保证通信的稳定。同时还要求有很强的抗弱网性能。
-
高可用:单机单节点出现故障时不影响系统可用性。
-
弹性伸缩:系统可以很方便的进行扩容操作,并且扩容时尽可能减少相应配置,这样可以使系统迅速进行扩容。
当前一些开源SFU的现状,例如Janus和MediaSoup,其服务端都会开UDP的操作范围,即服务端用不同的端口服务不同的客户端的媒体连接。同时在Janus中,信令和媒体是耦合在一起的;而在MediaSoup中,官方提供了nodejs库,其本身只是一个媒体层的库。但同时官方也提供了一个Demo,其媒体层和信令也是耦合在一起的;SRS4实际是国产之光,产品推出的时间不久,目前只支持WebRTC拉流功能。
对于这些开源SFU,主要的改进点有:
-
使用端的UDP服务端端口进行流媒体的传输;
-
信令和媒体层分离设计,可以支持大规模分布式部署;
-
关于级联方面,各个开源SFU都没有相对完整的解决方案。在我们的系统中,采用路由表方式的级联,并且是私有协议的级联,可以很好的支持和用户就近接入。
当然对于整个RTC云服务,除了SFU这个核心功能之外,RTC云服务还需要支持一些混流、录制、多协议网关支持(例如RTMP的接入:方便微信小程序的接入、SIP的接入)等。
02
单端口方案
目前无论是Janus还是MediaSoup,服务端都是使用单独的UDP端口服务单独的PeerConnection, SFU在启动时会配置一个可用的UDP的端口范围,用于客户端的数据传输。服务端接收到客户端的请求后,会从配置的端口范围内为客户端分配一个未被使用的端口,通过SDP把服务端的端口传给客户端。客户端收到SDP端口并进行解析,然后就可以向服务端发送或接收数据。这就要求服务端同时暴露成千上万个端口,对于网络安全性是很不友好的,同时可运维性也较差。另外,客户端的网络可能会对目的端口进行一些限制,如果分配的端口在允许范围之外,那么客户端就连接不到服务器,导致整个连通的失败。
为了实现云服务的高可用、弹性伸缩一般会配置负载均衡设备作为网络的接入设备。在真正生产环境中,可能一个IP后面会挂着几十甚至上百台机器,当机器宕机时不会导致整个服务的不可用。常见的负载均衡设备中很少看到有支持UDP PortRange方式的,即使支持了,由于暴露了很多端口,健康检查方面实际是不可能完成的任务。
鉴于以上问题,我们就需要对SFU进行相应的改造,以使得服务端使用单端口对流媒体的数据进行传输。
Janus使用了Libnice库作为底层网络传输库,该库本身是多端口的实现,因此要在Janus基础上实现单端口存在两种方案:一种是直接替换掉Libnice库,重新构建底层,改为单端口的传输方式。但是由于Janus和Libnice库的耦合非常紧密,若要使用重新构建底层的方式,实现较为复杂的,难度很大;另外一种方式就是保留Libnice多端口的实现,在Janus上增加单端口代理的功能。代理的功能是指将单一的对外端口传输的客户端的数据,在接收到数据之后,同时将相应的数据转发到Libnice内部分配的不同服务端的内部端口中。这种方式修改起来会更简单一些。
若选择使用代理方式,其实现难点在于来自不同客户端的数据都是通过同一个服务端端口进行传输,服务端该如何判断传输的数据与用户的对应关系。对此,我们可以通过SDP协商里面的ICE-Ufrag字段进行解决,当服务端接收到客户端的SDP后,按照之前的流程,会创建本地服务的端口,并且将相应的ICE-Ufrag与该端口映射起来。服务端会将对外的IP端口写入SDP传给客户端,然后一直监听对外端口。客户端建联时会发送Stun包, Stun包中会带有ICE-Ufrag,服务端接收并解析出ICE-Ufrag,再根据之前的映射关系,从IP-MAPS中找到对应的服务端端口。同时服务端还会记录Stun包的来源客户端IP和端口,服务端就会将用户侧的IP和端口与服务端的IP和端口映射起来。每次收到客户端的数据之后,就可以查看数据源的IP和端口,通过MAP的映射关系查到对应的服务端的端口,将数据转发到相应的服务端端口中。同理,服务端发出的数据也会从映射关系中找到对应客户端的IP和端口,通过单个端口发出。
通过这种单端口的方案,我们就可以将SFU部署在负载均衡设备之后,并且可以很方便的进行台线扩容和健康检查,达到高并发和高可用的目的。另外,服务端是有公网地址的,因此WebRTC的ICE、打洞的操作实际上也就不需要了。在进行地址映射时,需要使用客户端Stun包的真实地址。在测试中我们发现,有时候真实地址与客户端发送过来的Candidate中的地址不一样,如果使用Candidate中的地址则会存在连通失败的问题。
MediaSoup虽然也是多端口方案,但是并未使用Libnice库,因此可以直接在底层实现整套单端口方案,并不需要Porxy的存在。
这里值得一提的是SRS4,虽然SRS4目前只支持WebRTC的拉流,但是其实现是基于原生的单端口方案,没有使用Libnice库,整个MAP的建立过程与前面所描述的是一致的,也不需要Porxy的存在。SRS4在单端口方面还是相当友好的,可以很简单的实现集群化的分布式部署。
03
信令分离
WebRTC标准本身并没有规定信令的部分,因此各个开源的SFU基本都是自定义实现的。Janus实现了基于HTTP或WebSocket的信令,MediaSoup本身是nodejs的库,不包含信令部分,但是其官方的Demo也实现了HTTP或WebSocket的信令。它们的共同点是信令部分的实现和媒体部分的实现是集成在一起的。信令一般基于TCP协议的,媒体一般是基于UDP协议的。如果它们的实现集成在一起的话,就需要一个客户端的TCP信令和UDP流媒体数据发送到服务端的同一台机器上。这主要是因为服务端在收到客户端的信令后,会在本机进行一些资源的初始化工作,如果TCP信令和UDP流媒体数据不在同一台机器上是无法完成的。
这样就存在两种简单的方案,其一:每台机器都有一个单独的公网IP;其二使用源地址哈希的负载均衡。
如果选择单独的公网IP的方案,功能实现没有问题,但并不能达到高可用、高并发的要求。一台机器对应一个IP,如果这台机器上的流特别多,就会很难负载,无法进行弹性扩容。
我们的主要目的就是希望同一个客户端的TCP和UDP负载到同一个服务器上,而使用源地址哈希的方式,会出现两个问题:一个是负载不均衡的问题,如果多个用户共享同一个网络出口的话,会造成负载的不均衡;另外一个问题是在实际网络过程中,即使是同一个客户端,它的TCP出口与UDP出口也可能并不相同,这就会导致客户端的整个连通失败。
根据以上分析可知,造成这种问题的根本原因是由于SFU同时提供了信令和媒体服务,我们的解决方案就是将信令从SFU中分离出来,信令分离其实有两层意思,其一:是将信令服务从SFU中分离,SFU作为单纯的流媒体处理器使用。其二:是将信令分为两部分,一部分是与客户端交互的信令,另外一部分是信令服务器与SFU或MeidiaServer之间的内部交互信令。
将信令服务分离之后,就可以单独实现信令服务器,为客户提供基于TCP的信令服务,包括SDP解析、生成服务。客户端首先要连接到信令服务器上,进行媒体协商,信令服务器会根据一定的策略选择SFU或MeidiaServer的节点的IP通过SDP返回给客户端,同时信令服务器还会把接收到的客户端信息向对应的分配的SFU进行广播。客户端接收到SDP之后,根据IP相应的连接到SFU的节点,SFU的节点中的所有机器其实都已经具备了客户端的信息,这样客户端就可以进行正常的推拉流。
因为采用了信令分离,所以也就不需要依赖于源地址哈希的负载均衡策略。同一个客户端的多个PeerConnection可能会打到后端不同的SFU上,也就达到了比较好的负载均衡的目的。
信令分离之后,紧接着的一个问题就是:信令服务器与SFU或MeidiaServer之间内部信令如何交互。信令服务器需要向SFU或MeidiaServer广播用户的信息,SFU需要向信令服务器上报一些媒体状态。这些内部信令的特点就是可以异步处理,不需要等待处理的返回结果,因此就可以使用消息队列去完成内部信令的交互,消息队列的引入进一步使得信令服务器与SFU进行应用的解耦,二者的部署就更加灵活。信令服务器可以与SFU进行混合部署,也可以进行单独部署。
信令服务器除了向客户端提供一些信令服务之外,还会使用客户端真实的IP通过http-DNS服务获得最佳的SFU节点地址,并返回给客户端。这样就会使得SFU的调度更加的准确,提供更好的服务。
Janus的信令与媒体的耦合较为紧密,因此分离起来会稍显复杂,同样有两种方案:一种是基于现有的Videoroom的插件去做修改,另外一种是直接自己实现一个SFU插件。两者的工作量都不算太小,如果自己实现SFU插件,Janus Core里面的部分也需要进行修改。
对于MediaSoup本身来说,它只是一个nodejs库,不包含信令部分,只需要实现一些上层消息队列的收发以及内部信令的解析功能即可,需要一个单独的信令服务器与客户端提供信令服务。
SRS4内部有一个很简单的拉流信令部分,如果想用SRS4实现WebRTC的拉流功能,信令的分离工作也是需要去做的。
04
级联Relay
对于级联Relay部分的改造,RTC的多方通话存在跨地域、跨运营商的问题。为满足更多用户的优质体验,需要用户就近接入,即通话多方分布在不同的SFU上。这就需要我们的SFU具备级联Relay的能力,将相关的媒体流转发到需要媒体流的SFU上。目前开源的Janus和MediaSoup都不具备完备的级联能力,都需要进行相应的改造。
级联主要涉及两个问题:其一,网络拓扑的问题,其二是级联协议的问题。
级联网络拓扑问题中最主要的是级联路由选择的问题。传统的CDN网络是树形结构,由中心节点和边缘节点构成,其主要优点是回源结构比较简单,多级放大,并发能力较强。其主要缺点是由于中心源栈的存在,多级的回源结构导致延迟较大,不太适合RTC的应用。
由于网络状况每时每刻都在发生变化,所以我们也不能确定RTC的应用最适合哪种结构。因此我们在设计的时候,最看重的是网络的灵活性和自适应性。
网络灵活性是指可以灵活的通过配置的方式,改变网络的拓扑结构,并且可以适应多种网络环境。级联可能会有很复杂的应用场景,例如,Relay可能是在公网做的,也可能是内网做的,也可能是几点间或内部进行级联。
自适应性是指系统可以根据实时的网络状况自动调整路由选择。为此,在级联中我们引入了路由表的设计,路由表就包括目的地址和下一跳地址。中心控制节点会将路由表下发到各个节点的SFU中,与CDN回源不同,由于RTC中没有中心源站上的概念,因此采用的是主动转发的方式,而不是类似CDN拉流回源的方式。中心节点其实会保存每条流的节点位置的信息,当某个节点需要某条流时,中心节点会向距离最近的并且有这条流的节点转发命令,SFU收到转发命令之后,会将路由信息写到转发包的头里面,并根据路由表查询下一跳到哪里,进行转发。下一跳接收到转发包之后,重复相应的过程,直到到达最后的目的地。这种做法的优点在于每次转发只需要中心节点下发一次命令即可,后续的转发完全由SFU自主完成。
中心控制节点还具备路由表的自动生成能力,如果有新节点上线,会自动生成新节点相关的路由表并下发,这样就可以保证新节点上线时,自动的完成数据流转的畅通。自适应的原理是节点主动对相邻的节点进行延迟和丢包的探测,并将这些探测结果上传到中心节点,中心节点根据这些探测结果对路由表进行一些调整、下发,这个功能目前我们还在处于测试阶段。
级联的另外一个问题是协议的问题,级联主要是在SFU之间进行,我们采用的是通过私有协议进行级联。WebRTC的协议本身是基于P2P的,因此如果使用WebRTC协议做SFU之间的级联就太重了,很多内容是不需要的。同时我们会将一些业务信息,例如房间号、用户号、路由信息等加到私有协议中,当接收端收到包之后就不需要再单独进行查询操作,同时也可以自动完成路由数据包的转发。
SFU使用单独的Relay端口进行私有协议和数据的监听和转发,同时级联的端口也可以开放给客户端,客户端也就可以通过私有协议接入RTC系统。由于有一些公网Relay场景的存在,私有协议里我们还会加入丢包重传FEC的功能,以保证公网之下Relay的质量。
Janus有一个RTP forword的功能,可以将用户的媒体流以RTP的方式forword到一个地址里。如果要基于Janus做级联,可以基于这个功能进行一些改造,增加级联的监听功能,可以实现整个媒体流的转发。
05
RTC云架构
上图所示是整体的RTC云架构,除了前面讲到的流媒体服务器,还包含其它一些模块,例如业务后台的模块Platform,包括Relay、路由表、房间等控制,Platform、信令服务器、流媒体服务器之间使用MQ进行信令的同步和转发,另外还有一些混流的服务,将多路视频流混成一路,推向旁路直播或者存储,混流服务器的流媒体转发也是通过Relay的方式进行的。除了以上,还有一些多协议的网关,例如支持RTMP(微信小程序)或SIP(传统的视频会议、终端)的接入。
【线上分享】移动音视频SDK工程实践
今晚19:30,我们邀请到了百度智能云音视频SDK产品技术负责人李明路从音视频数据角度出发,梳理音视频SDK的发展和技术演进。分析数据在常见音视频模块上遇到的问题与挑战,并提出相应的解决思路和技术实践。