天天看點

聊聊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的線程方案,它可以最大程度的降低跨線程的調用與加鎖,整體的性能和代碼的可讀性也都會變高。

繼續閱讀