天天看点

聊聊WebRTC网关服务器3:如何优化Server的线程方案?

《聊聊WebRTC网关服务器》系列文章系由WebRTCon2018中网易云信音视频技术专家的分享内容《从零开始构建音视频网关服务器》整理而成,该系列文章将和大家分享网易NRTC在WebRTC网关项目的自研过程中遇到的一些问题,以及我们最终的解决方法。

在分享完端口方案与PeerConnection的方案后,本篇文章我们将讲解如何优化WebRTC网关服务器的线程方案。这个也是网关服务器架构设计的核心部分。

我们做WebRTC网关服务器的时候,不仅要考虑功能可用,还要考虑并发性能。有三种方案可以选择:

  • 第一种方案是一种多线程的方案,就是为每一个client或者是每一个PeerConnection建立一个独立的线程去做收发。这个方案的劣势很明显,它跨线程的流程就会很多,效率也不高。但是,这确实是某些开源服务器的方案;
  • 第二种方案是单线程的方案,所有用户的I/O以及业务操作都在服务端的一个线程里做完,在用户量不大或者服务端硬件资源可以隔离的情况下,它是一个比较好的方案。但是这种方案也有劣势,当我们使用物理机作为服务器时,单线程是没有办法利用多核优势的;如果同时启动多个进程,则需要多个外网端口和域名,维护的成本也会比较高;
  • 第三种方案是多线程优化方案,也是网关一期实现的方案,I/O单独在一个线程来做,同一个通话的用户在同一个业务逻辑线程做业务操作,同时创建一组业务逻辑线程为所有用户服务。

我们看一下这个方案具体是怎么做的?

多线程优化方案

多线程优化方案,主线程监听在一个端口,并使用前面提到方案在服务器将用户的数据包进行识别和区分。

聊聊WebRTC网关服务器3:如何优化Server的线程方案?

首先我们服务器端主线程会创建一个UDP的socket,绑定到7000端口,并注册监听可读事件,WebRTC客户端会给服务端7000端口发数据包。当然在之前SDP和ICE Candidate的协商流程已经做完了,我们从stun的第一个数据包(stun binding request)中就可以拿到这个WebRTC的web address(IP+端口)以及区分会议的RoomID和区分与会者的UserID。

拿到RoomID以后,我们可以把客户端的web address和RoomID绑定,建立一个映射关系。为什么要做这个绑定关系?

这是为了后续我们可以把同一个会议的用户全部投递到一个业务逻辑处理线程来处理。如果用户的数据是在不同逻辑处理线程上处理,我们在做数据处理的时候就要做跨线程的处理,跨线程处理就一定要加锁,这是我们不太愿意看到的。

如果同一会议的用户数据都在一个逻辑线程里面处理,其实它是在一个单线程里面的顺序处理,没有并发的问题,也不需要加锁,所以它的性能会比较高。

有了刚刚我们建立的映射关系,后续处理同一个会议的用户上行的stun包、dtls、SRTP、SRTCP,我们都根据RoomID做同样的Hash算法,Hash到我们后面的业务主线程里面,这样每一次主线程把I/O做完后,一次性将主线程收到的各个数据包按照Hash算法投递给对应的后端的逻辑处理多线程,这样同一个会议的SFU逻辑都在同一个线程里面处理完了。

这个是我们一开始使用的方案,但是大家也能看到,这个方案还有一个缺陷,就是I/O都是在主线程做的,后续的数据都需要通过跨线程的投递方式让后续的逻辑处理现场来做,那么有没有办法不要做跨线程的投递,让用户的数据包可以直接在对应的逻辑处理多线程里面处理。

其实传统的TCP服务器是比较容易做到的,它可以在主线程上Accept后为每个客户端创建一些新的fd,并分配一个子线程处理这个新fd的读写。那UDP能不能这么做?UDP在常规的编程方法是无法做到的,针对这个问题,我们设计了一个进阶方案。

多线程优化方案(进阶)

我们利用Linux reuseaddr和connect以后的udp fd特性,只有第一个数据包经主线程收包后投递到逻辑处理线程,其它数据包直接在逻辑线程接收。

聊聊WebRTC网关服务器3:如何优化Server的线程方案?

我们具体是怎么做的?

首先主线程还是创建一个fd,绑定7000端口。当然这里面有一个关键点是需要设置套接字的选项——reuseaddr,这个套接字选项在TCP领域用的很多,在UDP领域中大家可能还比较少接触。同样在主线监听可读事件。主线程开始收包了,同样的流程,收到到第一个stun包,获取WebRTC的web address(IP+端口)以及区分会议的RoomID和区分与会者的UserID。使用Hash(RoomID)到一个逻辑处理的子线程,到此为止流程与之前的多线程方案没有太大区别。

在逻辑处理子线程里面的方案就有一定的技巧了。它首先会在这个子线程里面再创建一个fd,这个fd监听的端口和主线程是一样的,也是7000端口。因为它设置了reuseaddr,所以说它的绑定可以成功,也就是说这个7000端口可以在多个线程里面同时监听。

接下来的流程就比较重要了,我们有了这个fd之后,我们需要做一次connect, connect在UDP领域大家使用得不多,但是它connect之后有什么效果呢?

大家可以阅读《UNIX网络编程 卷1第3版》的8.11,简单概括:UDP fd connect之后,会绑定本端与对端的四元组。当内核选择fd来收udp包时采用的是最佳匹配的原则。

因此connect过后的fd是四元组匹配最高的,这时内核会直接选择我们在子线程里面创建的fd。这个方案有点绕,但是还是比较有意思的,涉及到一些内核的事情,推荐大家感兴趣可以深入研究一下。在逻辑处理子线程中,我们还是会把这个新创建的fd注册到我们的事件循环里面。此后这个用户的上下行数据的IO操作将直接在这个逻辑处理线程。

这个方案就是我们优化后的Server的线程方案,它可以最大程度的降低跨线程的调用与加锁,整体的性能和代码的可读性也都会变高。

继续阅读