100w人在線的 彈幕 系統(tǒng),是怎么架構的?
Shopee是東南亞及中國臺灣地區(qū)的電商平臺 。2015年于新加坡成立并設立總部,隨后拓展至馬來西亞、泰國、中國臺灣地區(qū)、印度尼西亞、越南及菲律賓共七大市場。
Shopee擁有商品種類,包括電子消費品、家居、美容保健、母嬰、服飾及健身器材等。
2022年第二季度,Shopee保持業(yè)績增長,其中總訂單數(shù)20億,同比增長41.6%。最新財報數(shù)據(jù)顯示,Shopee電商平臺在今年第二季度的GMV為190億美元,同比增長27.2%;總營收為17億美元,同比增長51.4%。
據(jù)data.ai, Shopee取得了2022年Q1全球購物類App總下載量第一、谷歌應用商店全球購物類App用戶使用總時長第一的佳績。
100w用戶同時在線的彈幕系統(tǒng)背景
為了更好的支持 shopee 東南亞直播業(yè)務,Shopee 平臺產(chǎn)品設計為直播業(yè)務增加了彈幕。
第一期彈幕使用騰訊云支持,效果并不理想,
主要問題是:
經(jīng)常卡頓、
彈幕偏少等問題。
最終促使Shopee團隊,定制開發(fā)自己的彈幕系統(tǒng)。
其性能規(guī)劃是:單房間百萬用戶同時在線。
沒有看錯:「百萬用戶同時在線,而且是單房間」。
假如說每3秒促達用戶一次,百萬用戶同時在線,單房間具體QPS將超過30w QPS
沒有看錯:「單房間具體QPS將超過30w QPS」
問題分析
按照背景來分析,系統(tǒng)將主要面臨以下問題:
帶寬壓力
?
假如說每3秒促達用戶一次,那么每次內(nèi)容至少需要有15條才能做到視覺無卡頓。
15條彈幕+http包頭的大小將超過3k,那么每秒的數(shù)據(jù)大小約為8Gbps,
而運維同學通知我們所有服務的可用帶寬僅為10Gbps。
?弱網(wǎng)導致的彈幕卡頓、丟失
?
該問題已在線上環(huán)境
?性能與可靠性
?
百萬用戶同時在線,按照上文的推算,具體QPS將超過30w QPS。
如何保證在雙十一等重要活動中不出問題,至關重要。性能也是另外一個需要著重考慮的點。
?
架構設計和優(yōu)化
那么,該如何做架構設計和優(yōu)化呢?
主要的架構優(yōu)化有:
業(yè)務解耦+服務拆分
引入本地緩存,優(yōu)化高并發(fā)讀
引入限流,優(yōu)化高并發(fā)寫
使用滑動窗口,實現(xiàn)無鎖化讀寫
通過短輪訓實現(xiàn)彈幕促達
傳輸優(yōu)化、節(jié)約帶寬
業(yè)務解耦+服務拆分
為了保證服務的穩(wěn)定性我們對服務進行了拆分,進行業(yè)務解耦+服務拆分
業(yè)務解耦+服務拆分的具體架構方案
將邏輯較為復雜、調(diào)用較少的發(fā)送彈幕業(yè)務與邏輯簡單、調(diào)用量高的彈幕拉取服務拆分開來。
將復雜的邏輯收攏到發(fā)送彈幕的一端。

服務拆分主要考慮因素是為了不讓服務間相互影響,
對于這種系統(tǒng)服務,不同服務的QPS往往是不對等的,
例如像拉取彈幕的服務的請求頻率和負載,通常會比發(fā)送彈幕服務高1到2個數(shù)量級,

解耦之后的優(yōu)勢:
實現(xiàn)一個小3高的目標:高可用、高擴展、高協(xié)同

高可用
最?度地保證系統(tǒng)的可用性,
在這種情況下,不能讓拉彈幕服務把發(fā)彈幕服務搞垮,
反之亦然,不能讓 發(fā)彈服務把拉彈幕服務 搞垮
高擴展
方便擴容和縮容
更加方便對各個服務做Scale-Up和Scale-Out。
高協(xié)同
方便協(xié)同開發(fā)
服務拆分也劃清了業(yè)務邊界,方便協(xié)同開發(fā)。
引入本地緩存優(yōu)化高并發(fā)讀
「在拉取彈幕服務的一端:引入本地緩存」
數(shù)據(jù)更新的策略是:
服務會定期發(fā)起RPC調(diào)?,從彈幕服務拉取數(shù)據(jù),拉取到的彈幕緩存到內(nèi)存中,
這樣后續(xù)的請求過來時便能直接?走本地內(nèi)存的讀取,大大幅降低了調(diào)用時延。
這樣做還有另外一個好處就是縮短調(diào)?鏈路,把數(shù)據(jù)放到離?戶最近的地?
同時還能降低外部依賴的服務故障對業(yè)務的影響,
?尼恩提示:本地緩存非常重要,大家需要做到架構級、源碼級精通
建議大家穿透 400Wqps本地緩存 caffeine的核心架構、核心源碼,這個非常有價值,
具體,可以去學習第25章視頻《穿透400Wqps caffeine源碼》里邊有caffeine的起底式、穿透式介紹
?
引入限流,優(yōu)化高并發(fā)寫
「在發(fā)送彈幕的一端: 限流(有損服務)」,
因為用戶一定時間能看得過來彈幕總量是有限的,
所以可以對彈幕進行限流,有選擇的丟棄多余的彈幕。
同時,采用柔性的處理方式,拉取用戶頭像、敏感詞過濾等分支在調(diào)用失敗的情況下,仍然能保證服務的核心流程不受影響,即彈幕能夠正常發(fā)送和接收,提供有損的服務。
使用滑動窗口,實現(xiàn)無鎖化讀寫
彈幕數(shù)據(jù)的讀寫,如果使用阻塞隊列,那么需要加鎖
如果加鎖,在超高并發(fā)場景,會性能非常低
如何實現(xiàn)無鎖化讀寫呢
基于滑動窗口技術,實現(xiàn)無鎖化讀寫,保證在「超高并發(fā)場景并發(fā)讀寫的性能」

為了數(shù)據(jù)拉取方便,我們將數(shù)據(jù)按照時間進行分片,將時間作為數(shù)據(jù)切割的單位,按照時間存儲、拉取、緩存數(shù)據(jù)(RingBuffer),簡化了數(shù)據(jù)處理流程。
與傳統(tǒng)的Ring Buffer不一樣的是,我們只保留了尾指針,
它隨著時間向前移動,每?秒向前移動一格,把時間戳和對應彈幕列表并寫到一個區(qū)塊當中,因此最多保留60秒的數(shù)據(jù)。
同時,如果此時來了一個讀請求,那么緩沖環(huán)會根據(jù)客戶端傳入的時間戳計算出指針的索引位置,并從尾指針的副本區(qū)域往回遍歷直至跟索引重疊,收集到一定數(shù)量的彈幕列表返回,
這種機制保證了緩沖區(qū)的區(qū)塊是整體有序的,因此在讀取的時候只需要簡單地遍歷一遍即可,加上使用的是數(shù)組作為存儲結構,帶來的讀效率是相當高的。
再來考慮可能出現(xiàn)數(shù)據(jù)競爭的情況。
先來說寫操作,由于在這個場景下,寫操作是單線程的,因此?可不必關心并發(fā)寫帶來的數(shù)據(jù)一致性問題。
再來說讀操作,由圖可知寫的方向是從尾指針以順時針?向移動,而讀方向是從尾指針以逆時針方向移動,
?決定讀和寫的位置是否出現(xiàn)重疊取決于index的位置,
由于我們保證了讀操作最多只能讀到30秒內(nèi)的數(shù)據(jù),因此緩沖環(huán)完全可以做到無鎖讀寫
?尼恩提示:滑動窗口的原理和源碼非常重要
具體,可以去學習第26章視頻 《100wQps三級緩存組件實操》里邊有滑動窗口的起底式、穿透式介紹
?
通過短輪訓實現(xiàn)彈幕促達
Long Polling和Websockets都不適用弱環(huán)境,
所以我們最終采取了「短輪訓」的方案來實現(xiàn)彈幕促達
彈幕卡頓、丟失分析
在開發(fā)彈幕系統(tǒng)的的時候,最常見的問題是該怎么選擇促達機制,
推送 vs 拉取 ?
長輪詢 vs 短 輪詢
基于AJAX的長輪詢方案 ?(Long Polling via AJAX)
客戶端打開一個到服務器端的 AJAX 請求,然后等待響應,
服務器端需要一些特定的功能來允許請求被掛起,只要一有事件發(fā)生,服務器端就會在掛起的請求中送回響應。
如果打開Http的Keepalived開關,還可以節(jié)約握手的時間。

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

「優(yōu)點1:較少的控制開銷,」
較少的控制開銷:在連接創(chuàng)建后,?WS用于協(xié)議控制的數(shù)據(jù)包頭部相對較小。
在不包含擴展的情況下,服務端到客戶端WS 頭部大小只有2至10字節(jié)(和數(shù)據(jù)包長度有關);
對于客戶端到服務器的內(nèi)容,此頭部還需要加上額外的4字節(jié)的掩碼。
但是,與?HTTP 頭部比,此項開銷顯著減少了。
「優(yōu)點2:更強的實時性,」
WEBSocket由于協(xié)議是全雙工的,服務器可以隨時推數(shù)據(jù)。
WEBSocket延遲明顯更少;
「優(yōu)點3:長連接,保持連接狀態(tài)」。
Long Polling vs Websockets
無論是以上哪種方式,都使用到TCP長連接,那么TCP的長連接是如何發(fā)現(xiàn)連接已經(jīng)斷開了呢?
TCP Keepalived會進行連接狀態(tài)探測,探測間隔主要由三個配置控制。
?keepalive_probes:探測次數(shù)(默認:7次)
keepalive_time 探測的超時(默認:2小時)
keepalive_intvl 探測間隔(默認:75s)
?
但是由于在東南亞的弱網(wǎng)情況下,TCP長連接會經(jīng)常性的斷開:
?Long Polling 能發(fā)現(xiàn)連接異常的最短間隔為:min(keepalive_intvl, polling_interval)
Websockets能發(fā)現(xiàn)連接異常的最短間隔為:Websockets: min(keepalive_intvl, client_sending_interval)
?
如果下次發(fā)送數(shù)據(jù)包的時候可能連接已經(jīng)斷開了,所以使用TCP長連接對于兩者均意義不大。
并且弱網(wǎng)情況下, Websockets其實已經(jīng)不能作為一個候選項了
即使Websockets服務端已經(jīng)發(fā)現(xiàn)連接斷開,仍然沒有辦法推送數(shù)據(jù),只能被動等待客戶端重新建立好連接才能推送,在此之前數(shù)據(jù)將可能會被采取丟棄的措施處理掉。
在每次斷開后均需要再次發(fā)送應用層的協(xié)議進行連接建立。
根據(jù)了解, 騰訊云的彈幕系統(tǒng):
在300人以下使用的是推送模式,
300人以上則是采用的輪訓模式。
但是考慮到資源消耗情況,他們可能使用的是Websocket來實現(xiàn)的彈幕系統(tǒng),
也就是 300人以上輪訓模式, 騰訊云也是基于Websocket 來實現(xiàn)的,不太可能基于 AJAX來實現(xiàn),
正式因為基于Websocket ,在弱網(wǎng)環(huán)境下,所以才會出現(xiàn)彈幕卡頓、丟失的情況。
綜上所述,Long Polling和Websockets都不適用我們面臨的環(huán)境,
所以我們最終采取了「短輪訓」的方案來實現(xiàn)彈幕促達

傳輸優(yōu)化、節(jié)約帶寬
為了降低帶寬壓力,我們主要采用了以下方案:
啟用Http壓縮
?通過查閱資料,http gzip壓縮比率可以達到40%以上(gzip比deflate要高出4%~5%)。
?
Response結構簡化

內(nèi)容排列順序優(yōu)化
?根據(jù)gzip的壓縮的壓縮原理可以知道,重復度越高,壓縮比越高,
因此 : ?可以將字符串和數(shù)字內(nèi)容放在一起擺放
?
頻率控制
?
通過請求頻率調(diào)整帶寬:通過添加請求間隔參數(shù),實現(xiàn)客戶端的請求頻率服務端可控。間隔時間太長,在突發(fā)流量的時候, 可能會出現(xiàn)有損服務,對于彈幕來說,是可以容忍的。
?
延長請求頻率,可以避免無效請求:在彈幕稀疏和空洞的時間段,通過控制下次請求時間,避免客戶端的無效請求。
總結

最終該服務在雙十二活動中,在Redis出現(xiàn)短暫故障的背景下,
高效且穩(wěn)定的支撐了單房間100w用戶在線,成功完成了既定的100w用戶的目標