這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
今天簡單記錄一下彈幕伺服器的設計思路,希望對大家有所協助。
業務特點
彈幕典型的進少出多情境,一個房間如果有10W觀眾,每秒提交的彈幕也許只有1000次,但是廣播彈幕給所有觀眾需要1000 * 10W次。
單機模型
為了推送訊息,長串連幾乎是必然的選擇。
每個房間有若干觀眾,所有房間的觀眾都串連在1個服務進程上。
當彈幕提交上來,根據房間找出所有房間內的線上使用者,迴圈將彈幕推送給他們。
假設1個服務進程的訊息網路吞吐能力是50萬次/秒,那麼一個10萬觀眾的房間,每秒提交5次彈幕就會達到服務端極限效能。
多機模型
假設一個直播間仍舊有10萬人線上,希望解決每秒5次彈幕就達到效能瓶頸的問題,很容易想到能否橫向擴充解決。
假設現在有2台伺服器,10萬人均勻串連在2台伺服器上,也就是一台有5萬人線上。
現在任意使用者發送1條彈幕到A伺服器,那麼A伺服器推送5萬次,並且將彈幕轉寄給B伺服器,B伺服器也只需要推送5萬次。
假設要達到A,B伺服器的極限,只需要每秒有10條彈幕即可。。。那麼橫向擴充的收益就這麼小嗎?
顯然有更不為人知的秘密存在。
批量模型
實際上,如果每個彈幕推送都作為一個獨立的tcp包發送,那麼網卡將很快達到瓶頸,因為幾乎每個包都要經過一次核心中斷交付給網卡,從而送出到網路中,這對網卡是不友好的。
根據實測經驗,萬兆網卡的每秒發包量大約在100-200萬之間,之前我舉例說50萬次/秒實際說的比實際情況少一些。
如果可以減少網路發包的次數,那麼就可以解決網卡的瓶頸,從而突破每秒廣播50萬人次彈幕的瓶頸。
思路就是批量,可以將原本要立即發送給觀眾的彈幕緩衝起來,每間隔1秒將這些緩衝的彈幕作為一個整體,發送給各個觀眾的tcp串連。
在這樣的實現下,無論每秒是50條彈幕還是10000條彈幕提交到服務端,每秒的推送網路調用次數都是10萬人次,也就是按秒為單位彙總彈幕,這樣網路調用次數只與線上觀眾數相關了。
依舊是之前的伺服器效能(極限每秒50萬次網路調用),那麼可以支撐50萬觀眾同時線上,每秒將1秒內緩衝的彈幕作為整體發送給50萬觀眾,也就是這一秒只有50萬次的網路調用,在網卡的承受範圍之內。
在網路這一塊來說,通過增加單次發送包的大小可以減少發包數量,提升頻寬利用率,每秒送達的彈幕數量並沒有受到影響(雖然每秒能夠發出的包個數少了,但是單個包內有很多彈幕,總體保持不變),即延遲換吞吐。
接下來呢
從bilibili的彈幕服務壓測資料來看,單個伺服器單機每秒下發3500萬+彈幕,承載大約100萬的線上觀眾,單次發送打包的彈幕數量大約是3500萬/100萬=35條的樣子,也就是每秒35條彈幕提交到服務端,然後打包廣播給所有觀眾,頻寬成為了瓶頸。
在觀眾數量不變的情況下,即便每秒提交10000條彈幕,那麼瓶頸也只在頻寬(推送的打包變大了)和CPU(彙總訊息),只要觀眾數量變成80萬,那麼頻寬又將有更多的冗餘,CPU也會相應降低。
整個彈幕系統的瓶頸再也並不是每秒的彈幕數量了,而是觀眾數量決定:
每秒網路發送的次數=觀眾數量!=彈幕數量
這是很重要的結論!通過增加伺服器均分觀眾,那麼每台伺服器每秒的推送次數就可以減少,只要叢集有足夠大的出口頻寬,那麼一切都不是問題!
架構
設計系統都講究無狀態,這樣做複雜度最低。
其實彈幕本質算是IM系統,IM系統一般支援1對1,1:N的聊天方式。
IM系統在擴充性方面有一些慣例,下面基於bilibili的彈幕架構簡單說說原理。
comet是網關,無狀態,負責用戶端長串連和訊息收發。
logic是邏輯伺服器,無狀態,做商務邏輯用,保持comet邏輯單一高效能。
router有狀態(記憶體狀態),logic通過使用者uid一致性雜湊儲存使用者的會話資訊在某個router。
一個uid可以進入多個房間,建立多條串連到不同comet,而每個直播間在不同伺服器上都可能有使用者線上。
使用者向房間發彈幕直接通過HTTP協議調用logic服務,而logic直接發給kafka,由job服務消費廣播給所有Comet。
使用者也可以定向發送訊息,可能直接發給個人,發給某個房間,發給全部房間。
發訊息給個人的話logic查詢router,擷取uid在哪些server上的哪些room。然後向kafka上推送一條記錄,由job服務將訊息廣播給這些server上的room,這樣無論該使用者在N個直播間裡任意一個都可以看到推送。
發送全部房間和發送一個房間類似,就是由job廣播給所有comet,然後每個Comet給所有使用者發訊息。
效能
其實推送有個程式設計問題,就是要推送的使用者量很大,而使用者頻繁的在上線與下線,因此線上使用者集合是上鎖的。
推送就要遍曆集合,所以很矛盾。
這個問題只能通過拆分集合實現,每個集合只維護部分使用者,通過UID雜湊分區。
推送訊息時,逐個遍曆每個小集合上鎖處理。推送某個房間也是類似的,只需要遍曆每個小集合,在小集合裡找出對應房間的使用者即可。
擴充閱讀
bilibili的彈幕系統是開源的,感興趣可以詳細分析他的代碼,用的是golang標準庫的rpc,額外依賴了kafka,整體設計還是不算複雜的。
github:https://github.com/Terry-Mao/goim/
部落格:http://geek.csdn.net/news/detail/96232