原文:cyningsun.com/03-31-2019/live-streaming-danmaku.html

背景
為了更好的支持東南亞直播業務,產品設計為直播業務增加了彈幕,第一期彈幕使用騰訊云支持,效果并不理想,經常出現卡頓、彈幕偏少等問題,最終促使我們開發自己的彈幕系統,性能要求是需要支持,單房間百萬用戶同時在線,
問題分析
按照背景來分析,系統將主要面臨以下問題:
-
帶寬壓力
假如說每3秒促達用戶一次,那么每次內容至少需要有15條才能做到視覺無卡頓,15條彈幕+http包頭的大小將超過3k,那么每秒的資料大小約為8Gbps,而運維同學通知我們所有服務的可用帶寬僅為10Gbps,
-
弱網導致的彈幕卡頓、丟失
該問題已在線上環境
-
性能與可靠性
百萬用戶同時在線,按照上文的推算,具體QPS將超過30w QPS,如何保證在雙十一等重要活動中不出問題,至關重要,性能也是另外一個需要著重考慮的點,
帶寬優化
為了降低帶寬壓力,我們主要采用了以下方案:
-
啟用Http壓縮
通過查閱資料,http gzip壓縮比率可以達到40%以上(gzip比deflate要高出4%~5%),
-
Response結構簡化
-

-
內容排列順序優化
根據gzip的壓縮的壓縮原理可以知道,重復度越高,壓縮比越高,因此可以將字串和數字內容放在一起擺放
-
頻率控制
- 帶寬控制:通過添加請求間隔引數(下次請求時間),保證客戶端的請求頻率服務端可控,以應對突發的流量增長問題,提供有損的服務,
- 稀疏控制:在彈幕稀疏和空洞的時間段,通過控制下次請求時間,避免客戶端的無效請求,
彈幕卡頓、丟失分析
在開發彈幕系統的的時候,最常見的問題是該怎么選擇促達機制,推送 vs 拉取 ?
Long Polling via AJAX
客戶端打開一個到服務器端的 AJAX 請求,然后等待回應,服務器端需要一些特定的功能來允許請求被掛起,只要一有事件發生,服務器端就會在掛起的請求中送回回應,如果打開Http的Keepalived開關,還可以節約握手的時間,

優點: 減少輪詢次數,低延遲,瀏覽器兼容性較好,缺點: 服務器需要保持大量連接,
WebSockets
長輪詢雖然省去了大量無效請求,減少了服務器壓力和一定的網路帶寬的占用,但是還是需要保持大量的連接,那么人們就在考慮了,有沒有這樣一個完美的方案,即能雙向通信,又可以節約請求的 header 網路開銷,并且有更強的擴展性,最好還可以支持二進制幀,壓縮等特性呢?于是人們就發明了這樣一個目前看似“完美”的解決方案 —— WebSocket,它的最大特點就是,服務器可以主動向客戶端推送資訊,客戶端也可以主動向服務器發送資訊,是真正的雙向平等對話,

優點:較少的控制開銷,在連接創建后,服務器和客戶端之間交換資料時,用于協議控制的資料包頭部相對較小,在不包含擴展的情況下,對于服務器到客戶端的內容,此頭部大小只有2至10位元組(和資料包長度有關);對于客戶端到服務器的內容,此頭部還需要加上額外的4位元組的掩碼,相對于 HTTP 請求每次都要攜帶完整的頭部,此項開銷顯著減少了,更強的實時性,由于協議是全雙工的,所以服務器可以隨時主動給客戶端下發資料,相對于HTTP請求需要等待客戶端發起請求服務端才能回應,延遲明顯更少;即使是和Comet等類似的長輪詢比較,其也能在短時間內更多次地傳遞資料,長連接,保持連接狀態,
Long Polling vs Websockets
無論是以上哪種方式,都使用到TCP長連接,那么TCP的長連接是如何發現連接已經斷開了呢?
TCP Keepalived會進行連接狀態探測,探測間隔主要由三個配置控制,
keepalive_probes:探測次數(默認:7次)
keepalive_time 探測的超時(默認:2小時)
keepalive_intvl 探測間隔(默認:75s)
但是由于在東南亞的弱網情況下,TCP長連接會經常性的斷開:
Long Polling 能發現連接例外的最短間隔為:min(keepalive_intvl, polling_interval)
Websockets能發現連接例外的最短間隔為:Websockets: min(keepalive_intvl, client_sending_interval)
如果下次發送資料包的時候可能連接已經斷開了,所以使用TCP長連接對于兩者均意義不大,并且弱網情況下Websockets其實已經不能作為一個候選項了
- 即使Websockets服務端已經發現連接斷開,仍然沒有辦法推送資料,只能被動等待客戶端重新建立好連接才能推送,在此之前資料將可能會被采取丟棄的措施處理掉,
- 在每次斷開后均需要再次發送應用層的協議進行連接建立,
根據了解騰訊云的彈幕系統,在300人以下使用的是推送模式,300人以上則是采用的輪訓模式,但是考慮到資源消耗情況,他們可能使用的是Websocket來實作的彈幕系統,所以才會出現彈幕卡頓、丟失的情況,綜上所述,Long Polling和Websockets都不適用我們面臨的環境,所以我們最終采取了短輪訓 的方案來實作彈幕促達

可靠與性能
為了保證服務的穩定性我們對服務進行了拆分,將復雜的邏輯收攏到發送彈幕的一端,同時,將邏輯較為復雜、呼叫較少的發送彈幕業務與邏輯簡單、呼叫量高的彈幕拉取服務拆分開來,服務拆分主要考慮因素是為了不讓服務間相互影響,對于這種系統服務,不同服務的QPS往往是不對等的,例如像拉取彈幕的服務的請求頻率和負載通常會比發送彈幕服務高1到2個數量級,在這種情況下不能讓拉彈幕服務把發彈幕服務搞垮,反之亦然,最?度地保證系統的可用性,同時也更更加方便對各個服務做Scale-Up和Scale-Out,服務拆分也劃清了業務邊界,方便協同開發,
在拉取彈幕服務的一端 ,引入了本地快取,資料更新的策略是服務會定期發起RPC調?從彈幕服務拉取資料,拉取到的彈幕快取到記憶體中,這樣后續的請求過來時便能直接?走本地記憶體的讀取,?大幅降低了呼叫時延,這樣做還有另外一個好處就是縮短調?鏈路,把資料放到離?戶最近的地?,同時還能降低外部依賴的服務故障對業務的影響,

為了資料拉取方便,我們將資料按照時間進行分片,將時間作為資料切割的單位,按照時間存盤、拉取、快取資料(RingBuffer),簡化了資料處理流程,與傳統的Ring Buffer不一樣的是,我們只保留了尾指標,它隨著時間向前移動,每?秒向前移動一格,把時間戳和對應彈幕串列并寫到一個區塊當中,因此最多保留60秒的資料,同時,如果此時來了一個讀請求,那么緩沖環會根據客戶端傳入的時間戳計算出指標的索引位置,并從尾指標的副本區域往回遍歷直至跟索引重疊,收集到一定數量的彈幕串列回傳,這種機制保證了緩沖區的區塊是整體有序的,因此在讀取的時候只需要簡單地遍歷一遍即可,加上使用的是陣列作為存盤結構,帶來的讀效率是相當高的,
再來考慮可能出現資料競爭的情況,先來說寫操作,由于在這個場景下,寫操作是單執行緒的,因此?可不必關心并發寫帶來的資料一致性問題,再來說讀操作,由圖可知寫的方向是從尾指標以順時針?向移動,?讀?向是從尾指標以逆時針方向移動,?決定讀和寫的位置是否出現重疊取決于index的位置,由于我們保證了讀操作最多只能讀到30秒內的資料,因此緩沖環完全可以做到無鎖讀寫
在發送彈幕的一端 ,因為用戶一定時間能看得過來彈幕總量是有限的,所以可以對彈幕進行限流,有選擇的丟棄多余的彈幕,同時,采用柔性的處理方式,拉取用戶頭像、敏感詞過濾等分支在呼叫失敗的情況下,仍然能保證服務的核心流程不受影響,即彈幕能夠正常發送和接收,提供有損的服務,
總結

最終該服務在雙十二活動中,在Redis出現短暫故障的背景下,高效且穩定的支撐了70w用戶在線,成功完成了既定的目標,
近期熱文推薦:
1.1,000+ 道 Java面試題及答案整理(2022最新版)
2.勁爆!Java 協程要來了,,,
3.Spring Boot 2.x 教程,太全了!
4.別再寫滿屏的爆爆爆炸類了,試試裝飾器模式,這才是優雅的方式!!
5.《Java開發手冊(嵩山版)》最新發布,速速下載!
覺得不錯,別忘了隨手點贊+轉發哦!
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/549212.html
標籤:Java
上一篇:JVM的垃圾收集演算法
下一篇:C語言結構體大小分析
