彈幕系統設計實踐

背景

爲了更好的支持東南亞直播業務,產品設計爲直播業務增加了彈幕。第一期彈幕使用騰訊雲支持,效果並不理想,經常出現卡頓、彈幕偏少等問題。最終促使我們開發自己的彈幕系統。性能要求是需要支持,單房間百萬用戶同時在線。

問題分析

按照背景來分析,系統將主要面臨以下問題:

  1. 帶寬壓力

    假如說每 3 秒促達用戶一次,那麼每次內容至少需要有 15 條才能做到視覺無卡頓。15 條彈幕 + http 包頭的大小將超過 3k,那麼每秒的數據大小約爲 8Gbps,而運維同學通知我們所有服務的可用帶寬僅爲 10Gbps。

  2. 弱網導致的彈幕卡頓、丟失

    該問題已在線上環境

  3. 性能與可靠性

    百萬用戶同時在線,按照上文的推算,具體 QPS 將超過 30w QPS。如何保證在雙十一等重要活動中不出問題,至關重要。性能也是另外一個需要着重考慮的點。

帶寬優化

爲了降低帶寬壓力,我們主要採用了以下方案:

  1. 啓用 Http 壓縮

    通過查閱資料,http gzip 壓縮比率可以達到 40% 以上(gzip 比 deflate 要高出 4%~5%)。

  2. Response 結構簡化

  3. 內容排列順序優化

    根據 gzip 的壓縮的壓縮原理可以知道,重複度越高,壓縮比越高,因此可以將字符串和數字內容放在一起擺放

  4. 頻率控制

彈幕卡頓、丟失分析

在開發彈幕系統的的時候,最常見的問題是該怎麼選擇促達機制,推送 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 其實已經不能作爲一個候選項了

根據瞭解騰訊雲的彈幕系統,在 300 人以下使用的是推送模式,300 人以上則是採用的輪訓模式。但是考慮到資源消耗情況,他們可能使用的是 Websocket 來實現的彈幕系統,所以纔會出現彈幕卡頓、丟失的情況。綜上所述,Long Polling 和 Websockets 都不適用我們面臨的環境,所以我們最終採取了短輪訓的方案來實現彈幕促達

可靠與性能

爲了保證服務的穩定性我們對服務進行了拆分,將複雜的邏輯收攏到發送彈幕的一端。同時,將邏輯較爲複雜、調用較少的發送彈幕業務與邏輯簡單、調用量高的彈幕拉取服務拆分開來。服務拆分主要考慮因素是爲了不讓服務間相互影響,對於這種系統服務,不同服務的 QPS 往往是不對等的,例如像拉取彈幕的服務的請求頻率和負載通常會比發送彈幕服務高 1 到 2 個數量級,在這種情況下不能讓拉彈幕服務把發彈幕服務搞垮,反之亦然,最⼤度地保證系統的可用性,同時也更更加方便對各個服務做 Scale-Up 和 Scale-Out。服務拆分也劃清了業務邊界,方便協同開發。

在拉取彈幕服務的一端,引入了本地緩存。數據更新的策略是服務會定期發起 RPC 調⽤從彈幕服務拉取數據,拉取到的彈幕緩存到內存中,這樣後續的請求過來時便能直接⾛走本地內存的讀取,⼤大幅降低了調用時延。這樣做還有另外一個好處就是縮短調⽤鏈路,把數據放到離⽤戶最近的地⽅,同時還能降低外部依賴的服務故障對業務的影響,

爲了數據拉取方便,我們將數據按照時間進行分片,將時間作爲數據切割的單位,按照時間存儲、拉取、緩存數據(RingBuffer),簡化了數據處理流程。與傳統的 Ring Buffer 不一樣的是,我們只保留了尾指針,它隨着時間向前移動,每⼀秒向前移動一格,把時間戳和對應彈幕列表並寫到一個區塊當中,因此最多保留 60 秒的數據。同時,如果此時來了一個讀請求,那麼緩衝環會根據客戶端傳入的時間戳計算出指針的索引位置,並從尾指針的副本區域往回遍歷直至跟索引重疊,收集到一定數量的彈幕列表返回,這種機制保證了緩衝區的區塊是整體有序的,因此在讀取的時候只需要簡單地遍歷一遍即可,加上使用的是數組作爲存儲結構,帶來的讀效率是相當高的。

再來考慮可能出現數據競爭的情況。先來說寫操作,由於在這個場景下,寫操作是單線程的,因此⼤可不必關心併發寫帶來的數據一致性問題。再來說讀操作,由圖可知寫的方向是從尾指針以順時針⽅向移動,⽽讀⽅向是從尾指針以逆時針方向移動,⽽決定讀和寫的位置是否出現重疊取決於 index 的位置,由於我們保證了讀操作最多隻能讀到 30 秒內的數據,因此緩衝環完全可以做到無鎖讀寫

在發送彈幕的一端,因爲用戶一定時間能看得過來彈幕總量是有限的,所以可以對彈幕進行限流,有選擇的丟棄多餘的彈幕。同時,採用柔性的處理方式,拉取用戶頭像、敏感詞過濾等分支在調用失敗的情況下,仍然能保證服務的核心流程不受影響,即彈幕能夠正常發送和接收,提供有損的服務。

總結

最終該服務在雙十二活動中,在 Redis 出現短暫故障的背景下,高效且穩定的支撐了 70w 用戶在線,成功完成了既定的目標

參考鏈接:

作者:cyningsun

來源:www.cyningsun.com/03-31-2019/live-streaming-danmaku.html

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/4-ok75meS1fGgjrSCihc9g