B站直播的自研P2P实践 | 助力S12英雄联盟总决赛

时间:2022-11-07 12:12:02

  万水千山逐梦遥,最后一舞动四方——恭喜DRX获得2022全球总决赛冠军!

  哔哩哔哩自研直播 P2P 助力英雄联盟总决赛,下周我们期待更多S12相关技术内容,你想了解的哔哩哔哩技术都在这里!

   01 引言

  随着硬件的不断发展,直播观众的电脑性能不断提升。芯片越来越快,显示器越来越大,显示器上的像素点越来越小,能展现的画面也越来越清晰;同时我国的网络建设进步飞速,光纤入户在大部分城市已经普及。这成为网络直播提供更高的画质的有利条件。

  观众对画质的追求越来越高,有需求就有市场,主播也使用越来越高的画质进行直播。在这个过程中,直播平台需要负担的带宽成本也迅速攀升。

  事实上,网络带宽的支出在技术成本侧是占比最大的部分。为了将成本控制在一个可以接受的范围,各直播平台纷纷使用P2P技术来降低服务器带宽。云服务提供商也提供了成套的解决方案,可以为没有自研能力、无法负担自研成本或短时间内无法完成自研的直播平台提供快速接入。

  随着B站内部对HLS协议直播的传输研发完成度不断提高,自研P2P的前提条件具备了。HLS本身是一种切片式的直播传输格式,具体细节可以参考前面的《HLS直播协议在B站的实践》。因为切片是静态文件,所以可以通过HTTP带Range头的请求下载这个文件的指定部分。如果让不同的观众下载同一个切片文件的不同部分,然后这些观众之间再互相交换一下数据,大家就都有完整的数据了,而服务器事实上只传了一份数据出去,带宽成本就大幅度降低了。

   02 利用WebRTC通信

  WebRTC最早来源于被谷歌收购的Global IP Solutions公司的IP电话和视频会议解决方案。谷歌在收购之后对其进行了开源,并与IETF和W3C等机构合作推进其标准化。Chrome从28版本开始集成了WebRTC,随后不同浏览器也进行了跟进;它们遵照同一种标准,能够互相连接和通信。

  由于WebRTC是浏览器内建的所有数据传输方式中,一个不依赖Server-Client模式的。故只有使用WebRTC作为P2P的传输协议,才能兼容消费带宽占比最大的浏览器端用户。

  2.1 WebRTC的连接流程

  WebRTC的连接,靠的是会话描述协议(Session Description Protocol,简称SDP)进行握手。这个会话描述协议就好像是Socket编程中的Socket Name那样,要建立连接的两方互相交换了SDP之后,连接才能建立。这里用到的SDP分为Offer和Answer两种,主动创建的SDP属于Offer,为了回应别人的SDP而创建的属于Answer。

  WebRTC提供的用于交换SDP的接口有如下几个:CreateOffer,SetLocalDescription,CreateAnswer,SetRemoteDescription。在最初创建Offer前,需要告诉WebRTC要用什么传输通道,例如音视频、数据通道等;因为我们在这里不使用音视频通道,所以只添加数据通道。利用这些API的简易建立连接流程示意如下:  

B站直播的自研P2P实践 | 助力S12英雄联盟总决赛

  从流程中可以看出,在连接成功建立之前,需要一台服务器在两个用户之间中转,传递Offer和Answer的数据,使得双方都完成SetLocalDescrption和SetRemoteDescription(图中加粗部分)。我们开发了一台Tracker服务器,通过WebSocket协议和浏览器页面实时通信,用于观看同一路直播流的观众之间互相发现、互相交换Offer和Answer,从而帮助这些观众之间建立连接。

  2.2 应用层协议

  在完成了WebRTC的连接之后,其中的DataChannel数据通道就可以用来实时收发数据了。数据通道的发送单位为一个“消息”。消息可以是字符串也可以是二进制。考虑到二进制类型更加通用,我们选择了二进制协议作为传输类型。协议的可扩展性也很重要,这关系到后续升级的时候是不是可以更容易地互相兼容。在Web开发领域通常会使用JSON作为数据结构的序列化、反序列化协议。但JSON不能直接兼容二进制数据的封装,Base64等编码又会使得数据的体积增加三分之一。为了同时满足方便使用、可扩展、传输二进制的要求,我们选择了Message Pack作为传输协议。它自描述,额外开销少,原生支持容纳二进制数据。

  WebRTC的DataChannel以消息为单位、采用的是SCTP协议进行数据传输。尽管标准中给出了如何拆分大的消息体,但并非所有浏览器都实现了它。所以在不同的WebRTC实现之间,传输大的消息体会出现微妙的兼容性问题。文档中提到小于16KB的数据块可以没有顾忌地收发,但这对于我们来说有点太小了。经过我们在需要支持的浏览器间两两测试,最终发现64KB是一个能用的最大值。为了开发和调试方便,我们定义单次请求和单次响应的最大大小为64KB,这样在应用层可以不需要额外开发数据的拆分和合并逻辑。考虑到Message Pack本身协议实现需要一定开销,为了后续可扩展性考虑也不应把64KB吃干抹净,所以我们定义单次传输的数据块最大上限为60KB。

  为了提高吞吐量,我们允许DataChannel里的通信并不是严格遵照请求——响应——请求——响应这样的流程,而是允许前一个请求得到应答之前,直接发送下一个请求,这样在网络延迟大、用户处理请求慢的情况下,克服了“停止等待协议”似的缺点。因为存在晚发的请求需要更少的时间处理这种情况,所以还要允许不按照请求顺序发送应答。所以我们设计了一个请求ID,通过ID来关联请求和对应的响应。

  综上,协议设计如下所示:  

B站直播的自研P2P实践 | 助力S12英雄联盟总决赛

  我们将通信流程设计成HTTP似的被动提供服务的方式。例如有A和B两个用户。A向B请求一个数据块,B回给A一个数据块。A没有向B发送请求的时候,B不会给A发送数据。请求和请求之间没有关联,协议是无状态的,这样可以减轻提供服务一方的负担。

  单个消息限制在60KB以内,而HLS协议的直播流中,单个分片随便都能超过60KB。所以我们把每个分片(即M4S文件)拆分成许多60KB的数据块,以数据块为单位分片传输。同时需要一个接口,能查询该用户对某个分片的下载进度——每个数据块的已下载、正在下载、未下载的状态。

   03 Peer间分工

  3.1 任务分配难题

  现在,用户之间怎么互相连接、连上之后怎么通信、怎么利用这种通信来节省CDN的带宽消耗已经都讨论完了。到把它们串通起来一起运行的时候了。

  首先,我打开直播间页面,网页上的直播播放器组件被创建,随后加载直播流的地址并开始播放。与此同时,在网页上看不见的地方,P2P组件开始连接到Tracker服务器,告诉服务器播放器正在看的是哪一路直播流;服务器返回了一个包含了也在看这路直播的用户的列表;P2P组件根据要连的用户数量、创建一批PeerConnection对象,并逐一调用CreateOffer获得建立连接要用的SDP字符串;这些SDP字符串通过Tracker服务器发给了不同的人,而收到了的人也创建PeerConnection对象、接受了Offer并产生Answer,然后通过Tracker服务器发回给我。这样一套流程下来,这一批PeerConnection对象就同时开始尝试与交换了SDP的直播间内其他观众连接。受到复杂的网络结构影响,不同用户间的WebRTC连接不保证能成功。如果最终连上的用户数没有到达预期,那么可以继续向Tracker请求下一批用户列表。

  现在,我已经和观看同一路直播流的几个小伙伴建立了连接。下一步就是用上面介绍的那些协议,从其他观众那边下载我要的数据:

  

B站直播的自研P2P实践 | 助力S12英雄联盟总决赛

  

B站直播的自研P2P实践 | 助力S12英雄联盟总决赛

  

B站直播的自研P2P实践 | 助力S12英雄联盟总决赛

  真巧,其他人也是这么想的。所以这个P2P网络就这么把自己憋死了。

  数据不可能凭空出现,破局的关键是,必须要有人从服务器下载一份原始数据,大家才有得东西互相共享。还有一个问题,上传数据如果太多,对正常使用网络造成不良影响,用户端体验就会很差,所以不能一个人下载数据之后传给太多的人。用户和用户之间使用的网络不同,而众所周知网络是有“相性”问题的:用户A和用户B互相传数据很快,用户B和用户C互相传数据很快,但这不能推理得出用户A和用户C互相传数据很快。再加上,前面提到过用户间互相连接是有一定的成功率的,虽然理论上连接成功与否受到这两个用户的路由器类型有关,但WebRTC的STUN协议实现,在代码里写死了不接受指定服务器和端口以外的来源发来的数据包;在这种限制下,在浏览器中利用STUN协议只能区分出对称型和非对称型路由器,无法继续细分,对我们帮助不太大。连接数太多又会对性能有不好的影响,不可能做到应连尽连。虽然理论上,只要投入一份完整的数据到P2P网络中,经过足够长的时间,整个网络里的所有人就都能得到一份完整的数据;但直播的数据具有很高的实时性要求,不可能提供无限长的时间在观众之间互相传数据。由此,分工变得特别困难。

  3.2 *市场

  如果我们让观众之间随机*连接,在有限的连接成功率下,他们能组成一个特别复杂的网状结构。由于用户间存在前面提到的“网络相性”问题,网络中节点与节点之间的边就有了不同的权重值。再加上直播间内的观众进进出出瞬息万变,如果有一个算法能在这样复杂的网状结构中计算出任务分配方式,算法也必须实时更新,这种超高的算力要求是巨大的挑战。所以我们换了个思路,从播放端的P2P SDK服从服务器的统一管理,切换到P2P SDK自我管理。

  首先定义下载流程。因为对HLS协议中每一个分片文件,如果直接开始P2P数据交换,就会出现前面说的互相憋死的问题。所以必须要有一部分用户,先从服务器下载一些数据,并且要求不同人下载的数据尽可能不同,这样才能互通有无。数据从服务器进入P2P网络之后,用户间的互相数据分享就开始了。因为每个用户的分享能力有上限,我们定义的是分享的数据量不得大于其下载的数据总量。再加上传输过程中数据可能有损耗之类的情况,最终是会有用户无法获得完整的数据的。所以在用户之间互相分享之后,会有一个最终阶段,通过服务器把缺失的部分补完。  

B站直播的自研P2P实践 | 助力S12英雄联盟总决赛

  在这个流程中,三个环节是互相影响的:如果初始做种获得的数据太多,那么P2P发挥的CDN带宽节省作用就有限;而如果初始做种获得的数据太少,因为用户的分享数据量有限制,所以直接导致最终补完阶段需要消耗大量的带宽。所以在初始做种阶段,哪些观众下载哪部分数据就变得特别关键。

  我们发现,在这个流程中,最终补完和初始做种都是从服务器下数据,从节省率来说是没有影响的。只要让三个阶段的实施可以互相影响,难题就可以破局。这里的情形很像是*市场,而这两件事的发生则对应了供不应求和供大于求:某些数据块需要在最终补完阶段从服务器下载,说明这个数据块供不应求,那么我们可以在初始做种阶段获取这些数据,补充供给;某些数据块在互相交换阶段没人要,说明这个数据块已经供大于求了,以后初始做种阶段就不下载这些数据,从供给角色转变为需求角色。*市场中“看不见的手”发挥了作用,自动进行供需调整。  

B站直播的自研P2P实践 | 助力S12英雄联盟总决赛

  因为所有观众会执行一样的策略,所以如果这些用户同时进行了供需角色的转变行为,那么会直接导致这个P2P网络中从一个极端转向另一个极端,然后下一次再转回来,难以达到一种稳定的平衡状态。所以我们让用户在准备进行角色转变之前,先进行一次概率判断,就像摇骰子,只有摇中的那些人进行角色转变。如果这些人的角色转变没有解决供需不平衡的问题(转变的人不够,或者转变的人太多),那么就还会有下一次摇骰子。利用这种方式,每个观众就只要看着自己的数据进行调整,服务器侧也没有额外的计算需求,也不需要额外考虑用户进出的问题,P2P网络就能始终朝着好的方向变化。

   04 结语

  至此,一个简单高效的直播P2P系统就构建起来了。P2P内核在上传量小于等于下载量的约束下互相分工和协作,在有限的成本预算下,支撑海量观众看直播的需求。