声网 Agora SDK 资深架构师章真分享了 Agora SDK 的架构设计。
问题与挑战
首先,从场景角度讲,我们会遇到的问题和挑战有哪些呢?
传统的 RTC 场景:现在我们可以看到很多场景,例如说 4K 高清视频,如果传统的SDK不做改善的话,传输一个 4K 视频,对它的内存、CPU等各方面都会带来极大的挑战。
娱乐社交和在线教育:现在不光需要打开 Web 浏览器、摄像头,还需要打开本地的播放器,传输本地播放器的内容。
云游戏加速:现在很多厂商还在开发云游戏,游戏运行于服务端,数据以音视频、指令等形式传输至手机,手机仅仅负责渲染,其中最大的挑战就是延时,如果从服务端到手机的传输延时超过 200ms 的话,游戏体验会变得很差,这就需要一个类似于声网的实时码流加速传输网络。
SIP/PSTN:SIP传统的网络电话,在全球有大量的业务需求,通过网络的流量来达到整个 RTC 的效果。
WebRTC 加速:如果在中国和美国之前通过公网 P2P 沟通,却缺少一个底层网络网和SDK的介入的话,其实是很难工作的。一个没有任何 QoS(服务质量)保障的连接,通话会很糟。
这些都是我们在 RTC 领域会遇到的场景,而 WebRTC 一类的开源引擎是远不能达到我们对场景的技术要求的,需要一个具备网络传输、音视频编解码等能力的 SDK 来实现。 面对这样的场景需求,SDK 需要具备哪些特性呢?首先是合理的架构设计,它有两个特点:第一点是媒体和网络是独立控制的。因为在类似 PSTN、云游戏加速传输的场景中,它的媒体数据是由自己处理的,仅需要我们提供网络传输加速的能力。但像 4K 音视频的实时传输,从采集、编码、渲染到传输,都需要 SDK 来完成。所以对于不同场景,SDK 就需要提供不同层次和不同模块的接口。第二是面向对象的 API 设计。关于 WebRTC 有个小故事,P2P 连接的协商过程是通过 SDP 协议做的,而整个能力协商的过程通过交换 offer 和 answer 就可以快速握手。最初这种设计认为协商过于复杂,一般的工程师搞不懂,所以并没有开放接口让开发者控制SDP相关内容。微软在进入 RTC 领域后,基于 WebRTC 贡献了 ORTC 项目,它 API 设计则是面向对象的。他们曾经有过这样一个看法,如果可以开放更多面向底层、面向对象的 API,开发者可以根据自己的场景需要来搭建。这也是面向对象 API 设计的重要性。现在很多提供 API 的公司都强调一点,叫做易用性,十几行代码就可以让你实现某个功能。因为以前开发者的能力普遍还没有那么强,也不清楚 RTC 场景是怎样的,所以我们通过这种简单的方式,让任何一个小白开发者都可以轻松做出一个 App。随着这些年的发展,场景变得越来越复杂,开发者的能力也越来越强,我们完全可以提供面向对象的 API,让开发者自己通过它们构建自己想要的场景。除了合理的架构设计,还要支持丰富的媒体传输能力,具备低延时、高性能、高并发的特性等。这些我稍后会详细分析。
架构与API设计
先说一下传输 SDK 的分层。如上图,SDK 的分层最底下是网络层。最早之前的一些网络传输都是基于 TCP 的,TCP 和 UDP 之间的区别,我就不说了,但是对于媒体的实时传输来讲,在有网络丢包时,TCP 的延时会非常大,完全不能满足实时互动的要求,所以最核心的是说媒体其实是不需要,就是在网络上丢包的情况下,TCP现在几乎所有的媒体实时传输都是基于 UDP 实现的,包括比较新的 QUIC 协议,底层也是基于 UDP的。Transport(UDP)上面是拥塞控制与网络连接控制,这是 RTC 领域最重要的一个技术环节和算法模块。目的是要在比较复杂错综的网络环境下,实现更灵活的网络控制。然后是 Media stream 层,它类似于一个 RTP 的协议,更多是面向媒体流,这一层有时间戳和一些标准的协议。再上面就是 Media Engine。Media Engine有两层,一层是编解码器,一层是输出编码后的数据,比如 VP8、VP9,也包括一些传统的编码码率。再往上是 Frame YUV/PCM。WebRTC 一般只能传YUV和PCM的数据。这里讲一个小的故事,很多中国的开发者会把 WebRTC 当成一个 SDK 用,其实 WebRTC 根本算不上是一个 SDK,它仅仅是一个 Media Engine。Media Engine 和 SDK 最主要的差别是什么呢?Media Engine仅仅是提供了一个功能,比如说像谷歌自己也有 RTC 的功能,它仅仅是把 WebRTC 的代码当成一个功能模块来使用,Chromium 才是一个真正的 SDK。说完网络与对象的简单分层,我们来一起看一下对象的建模。
我们去分析一个业务场景,或者是去设计一个 API,最重要是要了解你控制的对象是什么。首先,我们一般的输入源有摄像头、屏幕共享、录音设备,以及文件或客户自定义数据,对于这些对象,我们通过 Audio Source 和 Video Source 作为管理,既可以管理 YUV/PCM 这种原始采集数据,也可以管理类似 H264/VP8 这种编码后数据。这些数据源可以产生媒体流,对于媒体流对象,我们用 Video Track 或者 Audio Track 来管理,对于本地发布流和远端订阅的流,用 local 和 remote 作为区分。而最重要的模块自然就是网络,我们抽象为一个叫 RTC Connection 的对象,负责网络连接到我们的 SD-RTN™ 上。每一个 Connection 都有且只有一个 local user 负责媒体流的发布和订阅。除此以外,video 和 audio 的处理模块也都对象化处理,如 video filter、audio filter、audio device manager 等。把媒体流发布到这个 Connection 上,你可以进行远端的通话了。在这里我们可以看到面向对象 API 的一些优点。你可以在其中创建多个对象,对应这个图来讲就是可以创建多个 Local Video Track,能同时有几个或几千个 RTC Connection,可以同时与多人建立连接,或者创建更多频道。从我们的理解来讲,API 的设计还有一个非常重要的地方。很多初级开发者都会觉得 API 仅仅是把 SDK 的功能体现给使用者。而在我们看来,好的 API 设计“能自己讲故事”。当别人看过你 30%的 API 之后,就能知道你整个架构和设计理念是什么,它能成为架构师与开发者对话的一个渠道。如果发送编码数据和发送原始数据 是完全两套API的style,就会给开发者带来困惑。所以在 API 的设计之中,架构要做的不仅仅是展现功能,还将你的API 设计理念通过 API 传达给使用者。
举一个例子。我们怎么实现与远端用户的通话。首先你要创建一个 Connection,你作为一个 Local User 想要发布流就需要一个 Local Track,这时候你需要调用 Publish Track 把 Local Track 发送到 Connection 上,这样远端的用户就能看到你了。同样的,你也可以去订阅远端用户(Remote Users)的流,他的 Remote Track 会通过 Connection 发送到 Local Users 这一端。这就是一个完整的“故事”。在听完这个“故事”之后,如果有一天你想传输你的摄像头数据,对你来讲,它仍然是一个 Track,只是 Source 不同了。只有会讲“故事”的 API,才能让用户理解如何去灵活使用。另外,还有很重要的一点,就是不要创造新的名词,应该符合全球定义的标准。我们在定义 API 的时候,就会大量地翻阅一些国际标准,比如 W3C 的,这些都是符合开发者认知体系的。
媒体和网络控制
接下来,我们讲讲架构设计里面的一些具体实现。我不知道大家是否听过 SOLID 法则。在讲它之前,我们要讲讲为什么说 WebRTC 只是一个功能模块。当你去玩一些开源项目,谷歌提供的能力也好,WebRTC 的开源代码也罢,你可能会发现它的适用场景非常单一,它只是适合 P2P 或者跟一些服务器打交道。作为一个 SDK,要讲功能开放给开发者,就必须要实现一个 Pipeline。从最简单的 Pipeline 来讲,有 5 个 SOLID 法则:
单一责任法则。假如你有一个 100 人的团队,每个团队都有自己的任务,有做降噪的,有做视频编码的,好的架构是让这些人只需要专注于自身的功能模块的实现,代码如何写,算法如何改进,而不需要去考虑其它模块中的业务。
开闭法则。当你需要开发一个新功能的时候,不需要去修改之前的代码,这是好的架构。
模块可替换。作为一个好的 SDK 架构,SDK 中的任何接口和模块都是可以被无缝替换的。
接口隔离。用户可以清楚找到控制对象或者接口,而不需要理解很多不感兴趣的接口。
最后,依赖反转是特别重要的一点。任何API 都需要面向接口编程,这样一来,用户就不需要去理解模块内部是如何实现的,只需要看接口就行了。
我们的 Pipeline 如上图所示。绿色的是接收端,中间通过 Agora SD-RTN™进行传输。我们会将一些算法、引擎等用 Pipeline 的方式进行组织。基于 SOLID 法则,我们面向各种场景的应用,代码会变得越来越快、越来越方便,算法专家也不用去了解其他模块,只专注于手上的工作。
举个例子,我们有一个叫做 Media Player Kit 的组件,它支持本地媒体播放和多流互动(详见我们此前的文章),如上图是它的架构。Media Player 可以支持本地媒体播放,也可以将本地视频流发送到远端。如果你还记得“API需要讲统一的故事“,就能想象到,Media Player 是一个媒体数据源,可以提供 video track 和 audio track,如果将这些 track 加上 renderer,就可以本地播放,如果把这个 track 发布到 RTC Connection 就可以和远端用户共享了。
Pipeline 就像一个管道一样,一般来说 Pipeline 都是单向的,从管道的入口到出口,但其实Pipeline 里最核心的一些控制是通过负向反馈来做的,这也是控制理论经典的话题。
在 RTC 领域里,有一个很核心的 Pipeline 叫“带宽估计”,它可以实时监控当前网络是否有拥塞,当发现有拥塞的之后,会立即反馈预估的带宽值到 Video Quality Controller 模块,动态调整码流、帧率,以保证音视频流的实时体验。如上图所示,Video Quality Controller模块同时还会监听 CPU 状态,因为低端手机,遇到较高帧率、分辨率的视频会容易遇到 CPU 的性能瓶颈,从而出现卡顿。Video Quality Controller 模块会基于收到的带宽估计和 CPU 状态信息来动态改变编码码率,比如你现在发送的是 2M 的码流,但是遇到了网络拥塞,那么就会降低一些画质,改为发送 1M 的码流,能保证通话是流畅的。
在架构中,策略层和功能层是要严格区分的。从上图来讲,实线的部分就是数据通道,它提供了视频的采集、编码、传输功能,而下方的模块则是策略层,负责根据网络及设备情况来反馈给功能模块,调整其中的码率、帧率这样的参数。
低延时、高性能、高并发
除此弱网对抗的算法等常规方法以外,我们还可以在开发工具层面来进一步优化网络延时。就好像莱特兄弟造飞机一样。他们做的最重要的一项设计就是风洞。这飞机真正试飞前就可以进行充分的测试。我们也一样,在此方面也花费了很多精力。我们做了配套的性能调查工具、系统工具,比如perf性能瓶颈的查找,热点代码的定位等,以此来做到 SDK 的白盒化。我们通过这些工具来不断优化SDK 的各项指标,包括延时、弱网对抗、内存优化、CPU 优化等。以分段延时为例,如果以光的速度来计算,从中国到美国直线传输大概需要 30ms。我们声网在全球的平均延时可以达到 76ms。下图是一个传输的分段延时示意图。我们通过工具来对每段延时生成清晰的报表。这些监测数据让我们能有针对性地优化不同的模块。
同时,我们还要对弱网对抗算法进行不断的验证和优化。我们会模拟丢包、模拟延迟,我们在算法上会关注码率跟踪速度、带宽预估准确度。如下图所示,红线是我们的预估值,黑线是验证的数值,两者越接近,说明码率控制得越好。
在高性能方面,我们提出了内存池和线程池的概念。 我们需要根据系统内存情况,自动调整内存池的大小,不同大小的空闲队列需要自动进行负载均衡,同时要有效地减少 malloc/free 调用次数、页错误数量。确保 SDK 在低内存环境中的可用性。在某些服务器推流的场景下,高并发可以极大的降低用户的服务器使用成本。如果每一路通话或者推流都需要一个进程实例的话,在并发情况下,CPU 会消耗在线程切换上。在我们的 SDK 中,我们可以通过线程的方式多开实例,可以极大地降低线程梳理,从而提高并发量。我们也进行了一些测试,业界其他产品在相同机器环境下,并发路只有 600 路,而我们声网的最大并发数可以到达 3400 路。
在我们的SDK中,线程是通过统一的线程池管理的,这种做法既让研发功能模块中,降低并发编程模型的复杂度,有可以让我们的线程数目受控,比如如果模块或者功能团队需要新的线程,需要提出申请,SDK通过注入的方式,将线程给予模块使用。这对于 SDK 的性能改善会很有帮助。
评论
查看更多