聲網(wǎng) Agora SDK 資深架構(gòu)師章真分享了 Agora SDK 的架構(gòu)設(shè)計。
問題與挑戰(zhàn)
首先,從場景角度講,我們會遇到的問題和挑戰(zhàn)有哪些呢?
傳統(tǒng)的 RTC 場景:現(xiàn)在我們可以看到很多場景,例如說 4K 高清視頻,如果傳統(tǒng)的SDK不做改善的話,傳輸一個 4K 視頻,對它的內(nèi)存、CPU等各方面都會帶來極大的挑戰(zhàn)。
娛樂社交和在線教育:現(xiàn)在不光需要打開 Web 瀏覽器、攝像頭,還需要打開本地的播放器,傳輸本地播放器的內(nèi)容。
云游戲加速:現(xiàn)在很多廠商還在開發(fā)云游戲,游戲運行于服務(wù)端,數(shù)據(jù)以音視頻、指令等形式傳輸至手機,手機僅僅負責(zé)渲染,其中最大的挑戰(zhàn)就是延時,如果從服務(wù)端到手機的傳輸延時超過 200ms 的話,游戲體驗會變得很差,這就需要一個類似于聲網(wǎng)的實時碼流加速傳輸網(wǎng)絡(luò)。
SIP/PSTN:SIP傳統(tǒng)的網(wǎng)絡(luò)電話,在全球有大量的業(yè)務(wù)需求,通過網(wǎng)絡(luò)的流量來達到整個 RTC 的效果。
WebRTC 加速:如果在中國和美國之前通過公網(wǎng) P2P 溝通,卻缺少一個底層網(wǎng)絡(luò)網(wǎng)和SDK的介入的話,其實是很難工作的。一個沒有任何 QoS(服務(wù)質(zhì)量)保障的連接,通話會很糟。
這些都是我們在 RTC 領(lǐng)域會遇到的場景,而 WebRTC 一類的開源引擎是遠不能達到我們對場景的技術(shù)要求的,需要一個具備網(wǎng)絡(luò)傳輸、音視頻編解碼等能力的 SDK 來實現(xiàn)。 面對這樣的場景需求,SDK 需要具備哪些特性呢?首先是合理的架構(gòu)設(shè)計,它有兩個特點:第一點是媒體和網(wǎng)絡(luò)是獨立控制的。因為在類似 PSTN、云游戲加速傳輸?shù)膱鼍爸校拿襟w數(shù)據(jù)是由自己處理的,僅需要我們提供網(wǎng)絡(luò)傳輸加速的能力。但像 4K 音視頻的實時傳輸,從采集、編碼、渲染到傳輸,都需要 SDK 來完成。所以對于不同場景,SDK 就需要提供不同層次和不同模塊的接口。第二是面向?qū)ο蟮?API 設(shè)計。關(guān)于 WebRTC 有個小故事,P2P 連接的協(xié)商過程是通過 SDP 協(xié)議做的,而整個能力協(xié)商的過程通過交換 offer 和 answer 就可以快速握手。最初這種設(shè)計認為協(xié)商過于復(fù)雜,一般的工程師搞不懂,所以并沒有開放接口讓開發(fā)者控制SDP相關(guān)內(nèi)容。微軟在進入 RTC 領(lǐng)域后,基于 WebRTC 貢獻了 ORTC 項目,它 API 設(shè)計則是面向?qū)ο蟮摹K麄冊?jīng)有過這樣一個看法,如果可以開放更多面向底層、面向?qū)ο蟮?API,開發(fā)者可以根據(jù)自己的場景需要來搭建。這也是面向?qū)ο?API 設(shè)計的重要性。現(xiàn)在很多提供 API 的公司都強調(diào)一點,叫做易用性,十幾行代碼就可以讓你實現(xiàn)某個功能。因為以前開發(fā)者的能力普遍還沒有那么強,也不清楚 RTC 場景是怎樣的,所以我們通過這種簡單的方式,讓任何一個小白開發(fā)者都可以輕松做出一個 App。隨著這些年的發(fā)展,場景變得越來越復(fù)雜,開發(fā)者的能力也越來越強,我們完全可以提供面向?qū)ο蟮?API,讓開發(fā)者自己通過它們構(gòu)建自己想要的場景。除了合理的架構(gòu)設(shè)計,還要支持豐富的媒體傳輸能力,具備低延時、高性能、高并發(fā)的特性等。這些我稍后會詳細分析。
架構(gòu)與API設(shè)計
先說一下傳輸 SDK 的分層。如上圖,SDK 的分層最底下是網(wǎng)絡(luò)層。最早之前的一些網(wǎng)絡(luò)傳輸都是基于 TCP 的,TCP 和 UDP 之間的區(qū)別,我就不說了,但是對于媒體的實時傳輸來講,在有網(wǎng)絡(luò)丟包時,TCP 的延時會非常大,完全不能滿足實時互動的要求,所以最核心的是說媒體其實是不需要,就是在網(wǎng)絡(luò)上丟包的情況下,TCP現(xiàn)在幾乎所有的媒體實時傳輸都是基于 UDP 實現(xiàn)的,包括比較新的 QUIC 協(xié)議,底層也是基于 UDP的。Transport(UDP)上面是擁塞控制與網(wǎng)絡(luò)連接控制,這是 RTC 領(lǐng)域最重要的一個技術(shù)環(huán)節(jié)和算法模塊。目的是要在比較復(fù)雜錯綜的網(wǎng)絡(luò)環(huán)境下,實現(xiàn)更靈活的網(wǎng)絡(luò)控制。然后是 Media stream 層,它類似于一個 RTP 的協(xié)議,更多是面向媒體流,這一層有時間戳和一些標準的協(xié)議。再上面就是 Media Engine。Media Engine有兩層,一層是編解碼器,一層是輸出編碼后的數(shù)據(jù),比如 VP8、VP9,也包括一些傳統(tǒng)的編碼碼率。再往上是 Frame YUV/PCM。WebRTC 一般只能傳YUV和PCM的數(shù)據(jù)。這里講一個小的故事,很多中國的開發(fā)者會把 WebRTC 當(dāng)成一個 SDK 用,其實 WebRTC 根本算不上是一個 SDK,它僅僅是一個 Media Engine。Media Engine 和 SDK 最主要的差別是什么呢?Media Engine僅僅是提供了一個功能,比如說像谷歌自己也有 RTC 的功能,它僅僅是把 WebRTC 的代碼當(dāng)成一個功能模塊來使用,Chromium 才是一個真正的 SDK。說完網(wǎng)絡(luò)與對象的簡單分層,我們來一起看一下對象的建模。?
我們?nèi)シ治鲆粋€業(yè)務(wù)場景,或者是去設(shè)計一個 API,最重要是要了解你控制的對象是什么。首先,我們一般的輸入源有攝像頭、屏幕共享、錄音設(shè)備,以及文件或客戶自定義數(shù)據(jù),對于這些對象,我們通過 Audio Source 和 Video Source 作為管理,既可以管理 YUV/PCM 這種原始采集數(shù)據(jù),也可以管理類似 H264/VP8 這種編碼后數(shù)據(jù)。這些數(shù)據(jù)源可以產(chǎn)生媒體流,對于媒體流對象,我們用 Video Track 或者 Audio Track 來管理,對于本地發(fā)布流和遠端訂閱的流,用 local 和 remote 作為區(qū)分。而最重要的模塊自然就是網(wǎng)絡(luò),我們抽象為一個叫 RTC Connection 的對象,負責(zé)網(wǎng)絡(luò)連接到我們的 SD-RTN? 上。每一個 Connection 都有且只有一個 local user 負責(zé)媒體流的發(fā)布和訂閱。除此以外,video 和 audio 的處理模塊也都對象化處理,如 video filter、audio filter、audio device manager 等。把媒體流發(fā)布到這個 Connection 上,你可以進行遠端的通話了。在這里我們可以看到面向?qū)ο?API 的一些優(yōu)點。你可以在其中創(chuàng)建多個對象,對應(yīng)這個圖來講就是可以創(chuàng)建多個 Local Video Track,能同時有幾個或幾千個 RTC Connection,可以同時與多人建立連接,或者創(chuàng)建更多頻道。從我們的理解來講,API 的設(shè)計還有一個非常重要的地方。很多初級開發(fā)者都會覺得 API 僅僅是把 SDK 的功能體現(xiàn)給使用者。而在我們看來,好的 API 設(shè)計“能自己講故事”。當(dāng)別人看過你 30%的 API 之后,就能知道你整個架構(gòu)和設(shè)計理念是什么,它能成為架構(gòu)師與開發(fā)者對話的一個渠道。如果發(fā)送編碼數(shù)據(jù)和發(fā)送原始數(shù)據(jù) 是完全兩套API的style,就會給開發(fā)者帶來困惑。所以在 API 的設(shè)計之中,架構(gòu)要做的不僅僅是展現(xiàn)功能,還將你的API 設(shè)計理念通過 API 傳達給使用者。?
舉一個例子。我們怎么實現(xiàn)與遠端用戶的通話。首先你要創(chuàng)建一個 Connection,你作為一個 Local User 想要發(fā)布流就需要一個 Local Track,這時候你需要調(diào)用 Publish Track 把 Local Track 發(fā)送到 Connection 上,這樣遠端的用戶就能看到你了。同樣的,你也可以去訂閱遠端用戶(Remote Users)的流,他的 Remote Track 會通過 Connection 發(fā)送到 Local Users 這一端。這就是一個完整的“故事”。在聽完這個“故事”之后,如果有一天你想傳輸你的攝像頭數(shù)據(jù),對你來講,它仍然是一個 Track,只是 Source 不同了。只有會講“故事”的 API,才能讓用戶理解如何去靈活使用。另外,還有很重要的一點,就是不要創(chuàng)造新的名詞,應(yīng)該符合全球定義的標準。我們在定義 API 的時候,就會大量地翻閱一些國際標準,比如 W3C 的,這些都是符合開發(fā)者認知體系的。
媒體和網(wǎng)絡(luò)控制
接下來,我們講講架構(gòu)設(shè)計里面的一些具體實現(xiàn)。我不知道大家是否聽過 SOLID 法則。在講它之前,我們要講講為什么說 WebRTC 只是一個功能模塊。當(dāng)你去玩一些開源項目,谷歌提供的能力也好,WebRTC 的開源代碼也罷,你可能會發(fā)現(xiàn)它的適用場景非常單一,它只是適合 P2P 或者跟一些服務(wù)器打交道。作為一個 SDK,要講功能開放給開發(fā)者,就必須要實現(xiàn)一個 Pipeline。從最簡單的 Pipeline 來講,有 5 個 SOLID 法則:
單一責(zé)任法則。假如你有一個 100 人的團隊,每個團隊都有自己的任務(wù),有做降噪的,有做視頻編碼的,好的架構(gòu)是讓這些人只需要專注于自身的功能模塊的實現(xiàn),代碼如何寫,算法如何改進,而不需要去考慮其它模塊中的業(yè)務(wù)。
開閉法則。當(dāng)你需要開發(fā)一個新功能的時候,不需要去修改之前的代碼,這是好的架構(gòu)。
模塊可替換。作為一個好的 SDK 架構(gòu),SDK 中的任何接口和模塊都是可以被無縫替換的。
接口隔離。用戶可以清楚找到控制對象或者接口,而不需要理解很多不感興趣的接口。
最后,依賴反轉(zhuǎn)是特別重要的一點。任何API 都需要面向接口編程,這樣一來,用戶就不需要去理解模塊內(nèi)部是如何實現(xiàn)的,只需要看接口就行了。
我們的 Pipeline 如上圖所示。綠色的是接收端,中間通過 Agora SD-RTN?進行傳輸。我們會將一些算法、引擎等用 Pipeline 的方式進行組織。基于 SOLID 法則,我們面向各種場景的應(yīng)用,代碼會變得越來越快、越來越方便,算法專家也不用去了解其他模塊,只專注于手上的工作。
舉個例子,我們有一個叫做 Media Player Kit 的組件,它支持本地媒體播放和多流互動(詳見我們此前的文章),如上圖是它的架構(gòu)。Media Player 可以支持本地媒體播放,也可以將本地視頻流發(fā)送到遠端。如果你還記得“API需要講統(tǒng)一的故事“,就能想象到,Media Player 是一個媒體數(shù)據(jù)源,可以提供 video track 和 audio track,如果將這些 track 加上 renderer,就可以本地播放,如果把這個 track 發(fā)布到 RTC Connection 就可以和遠端用戶共享了。
Pipeline 就像一個管道一樣,一般來說 Pipeline 都是單向的,從管道的入口到出口,但其實Pipeline 里最核心的一些控制是通過負向反饋來做的,這也是控制理論經(jīng)典的話題。?
在 RTC 領(lǐng)域里,有一個很核心的 Pipeline 叫“帶寬估計”,它可以實時監(jiān)控當(dāng)前網(wǎng)絡(luò)是否有擁塞,當(dāng)發(fā)現(xiàn)有擁塞的之后,會立即反饋預(yù)估的帶寬值到 Video Quality Controller 模塊,動態(tài)調(diào)整碼流、幀率,以保證音視頻流的實時體驗。如上圖所示,Video Quality Controller模塊同時還會監(jiān)聽 CPU 狀態(tài),因為低端手機,遇到較高幀率、分辨率的視頻會容易遇到 CPU 的性能瓶頸,從而出現(xiàn)卡頓。Video Quality Controller 模塊會基于收到的帶寬估計和 CPU 狀態(tài)信息來動態(tài)改變編碼碼率,比如你現(xiàn)在發(fā)送的是 2M 的碼流,但是遇到了網(wǎng)絡(luò)擁塞,那么就會降低一些畫質(zhì),改為發(fā)送 1M 的碼流,能保證通話是流暢的。
在架構(gòu)中,策略層和功能層是要嚴格區(qū)分的。從上圖來講,實線的部分就是數(shù)據(jù)通道,它提供了視頻的采集、編碼、傳輸功能,而下方的模塊則是策略層,負責(zé)根據(jù)網(wǎng)絡(luò)及設(shè)備情況來反饋給功能模塊,調(diào)整其中的碼率、幀率這樣的參數(shù)。
低延時、高性能、高并發(fā)
除此弱網(wǎng)對抗的算法等常規(guī)方法以外,我們還可以在開發(fā)工具層面來進一步優(yōu)化網(wǎng)絡(luò)延時。就好像萊特兄弟造飛機一樣。他們做的最重要的一項設(shè)計就是風(fēng)洞。這飛機真正試飛前就可以進行充分的測試。我們也一樣,在此方面也花費了很多精力。我們做了配套的性能調(diào)查工具、系統(tǒng)工具,比如perf性能瓶頸的查找,熱點代碼的定位等,以此來做到 SDK 的白盒化。我們通過這些工具來不斷優(yōu)化SDK 的各項指標,包括延時、弱網(wǎng)對抗、內(nèi)存優(yōu)化、CPU 優(yōu)化等。以分段延時為例,如果以光的速度來計算,從中國到美國直線傳輸大概需要 30ms。我們聲網(wǎng)在全球的平均延時可以達到 76ms。下圖是一個傳輸?shù)姆侄窝訒r示意圖。我們通過工具來對每段延時生成清晰的報表。這些監(jiān)測數(shù)據(jù)讓我們能有針對性地優(yōu)化不同的模塊。?
同時,我們還要對弱網(wǎng)對抗算法進行不斷的驗證和優(yōu)化。我們會模擬丟包、模擬延遲,我們在算法上會關(guān)注碼率跟蹤速度、帶寬預(yù)估準確度。如下圖所示,紅線是我們的預(yù)估值,黑線是驗證的數(shù)值,兩者越接近,說明碼率控制得越好。?
在高性能方面,我們提出了內(nèi)存池和線程池的概念。 我們需要根據(jù)系統(tǒng)內(nèi)存情況,自動調(diào)整內(nèi)存池的大小,不同大小的空閑隊列需要自動進行負載均衡,同時要有效地減少 malloc/free 調(diào)用次數(shù)、頁錯誤數(shù)量。確保 SDK 在低內(nèi)存環(huán)境中的可用性。在某些服務(wù)器推流的場景下,高并發(fā)可以極大的降低用戶的服務(wù)器使用成本。如果每一路通話或者推流都需要一個進程實例的話,在并發(fā)情況下,CPU 會消耗在線程切換上。在我們的 SDK 中,我們可以通過線程的方式多開實例,可以極大地降低線程梳理,從而提高并發(fā)量。我們也進行了一些測試,業(yè)界其他產(chǎn)品在相同機器環(huán)境下,并發(fā)路只有 600 路,而我們聲網(wǎng)的最大并發(fā)數(shù)可以到達 3400 路。?
在我們的SDK中,線程是通過統(tǒng)一的線程池管理的,這種做法既讓研發(fā)功能模塊中,降低并發(fā)編程模型的復(fù)雜度,有可以讓我們的線程數(shù)目受控,比如如果模塊或者功能團隊需要新的線程,需要提出申請,SDK通過注入的方式,將線程給予模塊使用。這對于 SDK 的性能改善會很有幫助。
評論
查看更多