直播系統(tǒng)聊天技術(shù)(七):直播間海量聊天消息的架構(gòu)設(shè)計難點實踐

本文由融云技術(shù)團隊原創(chuàng)分享,有修訂和改動。
1、引言
在視頻直播場景中,彈幕交互、與主播的聊天、各種業(yè)務(wù)指令等等,組成了普通用戶與主播之間的互動方式。
從技術(shù)的角度來看,這些實時互動手段,底層邏輯都是實時聊天消息或指令的分發(fā),技術(shù)架構(gòu)類比于IM應(yīng)用的話,那就相當(dāng)于IM聊天室功能。
本系列文章的上篇《百萬人在線的直播間實時聊天消息分發(fā)技術(shù)實踐》主要分享的是消息分發(fā)和丟棄策略。本文將主要從高可用、彈性擴縮容、用戶管理、消息分發(fā)、客戶端優(yōu)化等角度,分享直播間海量聊天消息的架構(gòu)設(shè)計技術(shù)難點的實踐經(jīng)驗。

學(xué)習(xí)交流:
- 移動端IM開發(fā)入門文章:《新手入門一篇就夠:從零開發(fā)移動端IM》
- 開源IM框架源碼:https://github.com/JackJiang2011/MobileIMSDK?
(本文已同步發(fā)布于:http://www.52im.net/thread-3835-1-1.html)
2、系列文章
本文是系列文章中的第7篇:
《直播系統(tǒng)聊天技術(shù)(一):百萬在線的美拍直播彈幕系統(tǒng)的實時推送技術(shù)實踐之路》
《直播系統(tǒng)聊天技術(shù)(二):阿里電商IM消息平臺,在群聊、直播場景下的技術(shù)實踐》
《直播系統(tǒng)聊天技術(shù)(三):微信直播聊天室單房間1500萬在線的消息架構(gòu)演進(jìn)之路》
《直播系統(tǒng)聊天技術(shù)(四):百度直播的海量用戶實時消息系統(tǒng)架構(gòu)演進(jìn)實踐》
《直播系統(tǒng)聊天技術(shù)(五):微信小游戲直播在Android端的跨進(jìn)程渲染推流實踐》
《直播系統(tǒng)聊天技術(shù)(六):百萬人在線的直播間實時聊天消息分發(fā)技術(shù)實踐》
《直播系統(tǒng)聊天技術(shù)(七):直播間海量聊天消息的架構(gòu)設(shè)計難點實踐》(* 本文)
3、直播間的主要功能和技術(shù)特征
如今的視頻直播間早已不單純是視頻流媒體技術(shù)問題,它還包含了用戶可感知的多類型消息發(fā)送和管理、用戶管理等任務(wù)。在萬物皆可直播的當(dāng)下,超大型直播場景屢見不鮮,甚至出現(xiàn)了人數(shù)無上限的場景,面對如此海量實時消息和指令的并發(fā)挑戰(zhàn),帶來的技術(shù)難度已非常規(guī)手段所能解決。
我們先來歸納一下如今的典型視頻直播間,相較于傳統(tǒng)直播間所包含的主要功能特征、技術(shù)特征等。
豐富的消息類型和進(jìn)階功能:
1)可發(fā)送文字、語音、圖片等傳統(tǒng)聊天功能;
2)可實現(xiàn)點贊、禮物等非傳統(tǒng)聊天功能的消息類型;
3)可管理內(nèi)容安全,包括敏感詞設(shè)置,聊天內(nèi)容反垃圾處理等。
聊天管理功能:
1)用戶管理:包括創(chuàng)建、加入、銷毀、禁言、查詢、封禁(踢人)等;
2)用戶白名單:白名單用戶處于被保護(hù)狀態(tài)不會被自動踢出,且發(fā)送消息優(yōu)先級別最高;
3)消息管理:包括消息優(yōu)先級、消息分發(fā)控制等;
4)實時統(tǒng)計及消息路由等能力。
人數(shù)上限和行為特征:
1)人數(shù)沒有上限:一些大型直播場景,如春晚、國慶大閱兵等,直播間累計觀看動輒上千萬人次,同時觀看人數(shù)也可達(dá)數(shù)百萬;
2)用戶進(jìn)退行為:用戶進(jìn)出直播間非常頻繁,高熱度直播間的人員進(jìn)出秒并發(fā)可能上萬,這對服務(wù)支撐用戶上下線以及用戶管理的能力提出了非常大的挑戰(zhàn)。
海量消息并發(fā):
1)消息并發(fā)量大:直播聊天室人數(shù)沒有明顯上限,帶來了海量并發(fā)消息的問題(一個百萬人數(shù)的聊天室,消息的上行已是巨量,消息分發(fā)量更是幾何級上升);
2)消息實時性高:如果服務(wù)器只做消息的消峰處理,峰值消息的堆積會造成整體消息延時增大。
針對上述第?2)?點,延時的累積效應(yīng)會導(dǎo)致消息與直播視頻流在時間線上產(chǎn)生偏差,進(jìn)而影響用戶觀看直播時互動的實時性。所以,服務(wù)器的海量消息快速分發(fā)能力十分重要。
4、直播間聊天室的架構(gòu)設(shè)計
高可用系統(tǒng)需要支持服務(wù)故障自動轉(zhuǎn)移、服務(wù)精準(zhǔn)熔斷降級、服務(wù)治理、服務(wù)限流、服務(wù)可回滾、服務(wù)自動擴容 / 縮容等能力。
以服務(wù)高可用為目標(biāo)的直播間聊天室系統(tǒng)架構(gòu)如下:

如上圖所示,系統(tǒng)架構(gòu)主要分三層:
1)連接層:主要管理服務(wù)跟客戶端的長鏈接;
2)存儲層:當(dāng)前使用的是 Redis,作為二級緩存,主要存儲聊天室的信息(比如人員列表、黑白名單、封禁列表等,服務(wù)更新或重啟時,可以從 Redis 中加載出聊天室的備份信息);
3)業(yè)務(wù)層:這是整個聊天室的核心,為了實現(xiàn)跨機房容災(zāi),將服務(wù)部署在多個可用區(qū),并根據(jù)能力和職責(zé),將其分為聊天室服務(wù)和消息服務(wù)。
聊天室服務(wù)和消息服務(wù)的具體職責(zé):
1)聊天室服務(wù):主要負(fù)責(zé)處理管理類請求,比如聊天室人員的進(jìn)出、封禁 / 禁言、上行消息處理審核等;
2)消息服務(wù):主要緩存本節(jié)點需要處理的用戶信息以及消息隊列信息,并負(fù)責(zé)聊天室消息的分發(fā)。
在海量用戶高并發(fā)場景下,消息分發(fā)能力將決定著系統(tǒng)的性能。以一個百萬級用戶量的直播間聊天室為例,一條上行消息對應(yīng)的是百萬倍的分發(fā)。這種情況下,海量消息的分發(fā),依靠單臺服務(wù)器是無法實現(xiàn)的。
我們的優(yōu)化思路是:將一個聊天室的人員分拆到不同的消息服務(wù)上,在聊天室服務(wù)收到消息后向消息服務(wù)擴散,再由消息服務(wù)分發(fā)給用戶。
以百萬在線的直播間聊天室為例:假設(shè)聊天室消息服務(wù)共 200 臺,那平均每臺消息服務(wù)管理 5000 人左右,每臺消息服務(wù)在分發(fā)消息時只需要給落在本臺服務(wù)器上的用戶分發(fā)即可。
服務(wù)落點的選擇邏輯:
1)在聊天室服務(wù)中:聊天室的上行信令是依據(jù)聊天室 ID 使用一致性哈希算法來選擇節(jié)點的;
2)在消息服務(wù)中:依據(jù)用戶 ID 使用一致性哈希算法來決定用戶具體落在哪個消息服務(wù)。
一致性哈希選擇的落點相對固定,可以將聊天室的行為匯聚到一個節(jié)點上,極大提升服務(wù)的緩存命中率。
聊天室人員進(jìn)出、黑 / 白名單設(shè)置以及消息發(fā)送時的判斷等處理直接訪問內(nèi)存即可,無須每次都訪問第三方緩存,從而提高了聊天室的響應(yīng)速度和分發(fā)速度。
最后:Zookeeper 在架構(gòu)中主要用來做服務(wù)發(fā)現(xiàn),各服務(wù)實例均注冊到 Zookeeper。
5、直播間聊天室的擴縮容能力
5.1 概述
隨著直播這種形式被越來越多人接受,直播間聊天室面對人數(shù)激增致使服務(wù)器壓力逐步增大的情況越來越多。所以,在服務(wù)壓力逐步增大 / 減少的過程中能否進(jìn)行平滑的擴 / 縮容非常重要。
在服務(wù)的自動擴縮容方面,業(yè)內(nèi)提供的方案大體一致:即通過壓力測試了解單臺服務(wù)器的瓶頸點?→?通過對業(yè)務(wù)數(shù)據(jù)的監(jiān)控來判斷是否需要進(jìn)行擴縮?→?觸發(fā)設(shè)定的條件后報警并自動進(jìn)行擴縮容。
鑒于直播間聊天室的強業(yè)務(wù)性,具體執(zhí)行中應(yīng)該保證在擴縮容中整體聊天室業(yè)務(wù)不受影響。
5.2 聊天室服務(wù)擴縮容
聊天室服務(wù)在進(jìn)行擴縮容時,我們通過 Redis 來加載成員列表、封禁 / 黑白名單等信息。
需要注意的是:在聊天室進(jìn)行自動銷毀時,需先判斷當(dāng)前聊天室是否應(yīng)該是本節(jié)點的。如果不是,跳過銷毀邏輯,避免 Redis 中的數(shù)據(jù)因為銷毀邏輯而丟失。
聊天室服務(wù)擴縮容方案細(xì)節(jié)如下圖所示:

5.3 消息服務(wù)擴縮容
消息服務(wù)在進(jìn)行擴縮容時,大部分成員需要按照一致性哈希的原則路由到新的消息服務(wù)節(jié)點上。這個過程會打破當(dāng)前的人員平衡,并做一次整體的人員轉(zhuǎn)移。
1)在擴容時:我們根據(jù)聊天室的活躍程度逐步轉(zhuǎn)移人員。
2)在有消息時:[消息服務(wù)會遍歷緩存在本節(jié)點上的所有用戶進(jìn)行消息的通知拉取,在此過程中判斷此用戶是否屬于這臺節(jié)點(如果不是,將此用戶同步加入到屬于他的節(jié)點)。
3)在拉消息時:用戶在拉取消息時,如果本機緩存列表中沒有該用戶,消息服務(wù)會向聊天室服務(wù)發(fā)送請求確認(rèn)此用戶是否在聊天室中(如果在則同步加入到消息服務(wù),不在則直接丟掉)。
4)在縮容時:消息服務(wù)會從公共 Redis 獲得全部成員,并根據(jù)落點計算將本節(jié)點用戶篩選出來并放入用戶管理列表中。
6、海量用戶的上下線和管理
聊天室服務(wù):管理了所有人員的進(jìn)出,人員的列表變動也會異步存入 Redis 中。
消息服務(wù):則維護(hù)屬于自己的聊天室人員,用戶在主動加入和退出房間時,需要根據(jù)一致性哈希算出落點后同步給對應(yīng)的消息服務(wù)。
聊天室獲得消息后:聊天室服務(wù)廣播給所有聊天室消息服務(wù),由消息服務(wù)進(jìn)行消息的通知拉取。消息服務(wù)會檢測用戶的消息拉取情況,在聊天室活躍的情況下,30s 內(nèi)人員沒有進(jìn)行拉取或者累計 30 條消息沒有拉取,消息服務(wù)會判斷當(dāng)前用戶已經(jīng)離線,然后踢出此人,并且同步給聊天室服務(wù)對此成員做下線處理。
7、海量聊天消息的分發(fā)策略
直播間聊天室服務(wù)的消息分發(fā)及拉取方案如下圖:

7.1 消息通知的拉取
在上圖中:用戶 A 在聊天室中發(fā)送一條消息,首先由聊天室服務(wù)處理,聊天室服務(wù)將消息同步到各消息服務(wù)節(jié)點,消息服務(wù)向本節(jié)點緩存的所有成員下發(fā)通知拉取(圖中服務(wù)器向用戶 B 和用戶 Z 下發(fā)了通知)。
在消息分發(fā)過程中,server 做了通知合并。
通知拉取的詳細(xì)流程為:
1)客戶端成功加入聊天,將所有成員加入到待通知隊列中(如已存在則更新通知消息時間);
2)下發(fā)線程,輪訓(xùn)獲取待通知隊列;
3)向隊列中用戶下發(fā)通知拉取。
通過這個流程可保障下發(fā)線程一輪只會向同一用戶發(fā)送一個通知拉取(即多個消息會合并為一個通知拉?。?,有效提升了服務(wù)端性能且降低了客戶端與服務(wù)端的網(wǎng)絡(luò)消耗。
7.2 消息的拉取
用戶的消息拉取流程如下圖:

?
如上圖所示,用戶 B 收到通知后向服務(wù)端發(fā)送拉取消息請求,該請求最終將由消息節(jié)點 1 進(jìn)行處理,消息節(jié)點 1 將根據(jù)客戶端傳遞的最后一條消息時間戳,從消息隊列中返回消息列表(參考下圖?)。
客戶端拉取消息示例:

用戶端本地最大時間為?1585224100000,從 server 端可以拉取到比這個數(shù)大的兩條消息。
7.3 消息控速
服務(wù)器應(yīng)對海量消息時,需要做消息的控速處理。
這是因為:在直播間聊天室中,大量用戶在同一時段發(fā)送的海量消息,一般情況下內(nèi)容基本相同。如果將所有消息全部分發(fā)給客戶端,客戶端很可能出現(xiàn)卡頓、消息延遲等問題,嚴(yán)重影響用戶體驗。
所以服務(wù)器對消息的上下行都做了限速處理。
消息控速原理:

具體的限速控制策略如下:
1)服務(wù)器上行限速控制(丟棄)策略:針對單個聊天室的消息上行的限速控制,我們默認(rèn)為 200 條 / 秒,可根據(jù)業(yè)務(wù)需要調(diào)整。達(dá)到限速后發(fā)送的消息將在聊天室服務(wù)丟棄,不再向各消息服務(wù)節(jié)點同步;
2)服務(wù)器下行限速(丟棄)策略:服務(wù)端的下行限速控制,主要是根據(jù)消息環(huán)形隊列的長度進(jìn)行控制,達(dá)到最大值后最“老”的消息將被淘汰丟棄。
每次下發(fā)通知拉取后服務(wù)端將該用戶標(biāo)記為拉取中,用戶實際拉取消息后移除該標(biāo)記。
如果產(chǎn)生新消息時用戶有拉取中標(biāo)記:
1)距設(shè)置標(biāo)記時間在 2 秒內(nèi),則不會下發(fā)通知(降低客戶端壓力,丟棄通知未丟棄消息);
2)超過 2 秒則繼續(xù)下發(fā)通知(連續(xù)多次通知未拉取則觸發(fā)用戶踢出策略,不在此贅述)。
因此:消息是否被丟棄取決于客戶端拉取速度(受客戶端性能、網(wǎng)絡(luò)影響),客戶端及時拉取消息則沒有被丟棄的消息。
8、直播間聊天室的消息優(yōu)先級
消息控速的核心是對消息的取舍,這就需要對消息做優(yōu)先級劃分。
劃分邏輯大致如下:
1)白名單消息:這類消息最為重要,級別最高,一般系統(tǒng)類通知或者管理類信息會設(shè)置為白名單消息;
2)高優(yōu)先級消息:僅次于白名單消息,沒有特殊設(shè)置過的消息都為高優(yōu)先級;
3)低優(yōu)先級消息:最低優(yōu)先級的消息,這類消息大多是一些文字類消息。
具體如何劃分,應(yīng)該是可以開放出方便的接口進(jìn)行設(shè)置的。
服務(wù)器對三種消息執(zhí)行不同的限速策略,在高并發(fā)時,低優(yōu)先級消息被丟棄的概率最大。
服務(wù)器將三種消息分別存儲在三個消息桶中:客戶端在拉取消息時按照白名單消息?>?高優(yōu)先級消息?>?低優(yōu)先級消息的順序拉取。
9、客戶端針對大量消息的接收和渲染優(yōu)化
9.1 消息的接收優(yōu)化
在消息同步機制方面,如果直播間聊天室每收到一條消息都直接下發(fā)到客戶端,無疑會給客戶端帶來極大性能挑戰(zhàn)。特別是在每秒幾千或上萬條消息的并發(fā)場景下,持續(xù)的消息處理會占用客戶端有限的資源,影響用戶其它方面的互動。
考慮到以上問題,為聊天室單獨設(shè)計了通知拉取機制,由服務(wù)端進(jìn)行一系列分頻限速聚合等控制后,再通知客戶端拉取。
具體分為以下幾步:
1)客戶端成功加入聊天室;
2)服務(wù)端下發(fā)通知拉取信令;
3)客戶端根據(jù)本地存儲的消息最大時間戳,去服務(wù)端拉取消息。
這里需要注意的是:首次加入直播間聊天室時,本地并沒有有效時間戳,此時會傳 0 給服務(wù)拉取最近 50 條消息并存庫。后續(xù)再次拉取時才會傳遞數(shù)據(jù)庫里存儲的消息的最大時間戳,進(jìn)行差量拉取。
客戶端拉取到消息后:會進(jìn)行排重處理,然后將排重后的數(shù)據(jù)上拋業(yè)務(wù)層,以避免上層重復(fù)顯示。
另外:直播間聊天室中的消息即時性較強,直播結(jié)束或用戶退出聊天室后,之前拉取的消息大部分不需要再次查看,因此在用戶退出聊天室時,會清除數(shù)據(jù)庫中該聊天室的所有消息,以節(jié)約存儲空間。
9.2 消息的渲染優(yōu)化
在消息渲染方面,客戶端也通過一系列優(yōu)化保證在直播間聊天室大量消息刷屏的場景下仍有不俗的表現(xiàn)。
以Andriod端為例,具體的措施有:
1)采用 MVVM 機制:將業(yè)務(wù)處理和 UI 刷新嚴(yán)格區(qū)分。每收到一條消息,都在 ViewModel 的子線程將所有業(yè)務(wù)處理好,并將頁面刷新需要的數(shù)據(jù)準(zhǔn)備完畢后,才通知頁面刷新;
2)降低主線程負(fù)擔(dān):精確使用 LiveData 的 setValue() 和 postValue() 方法:已經(jīng)在主線程的事件通過??setValue() 方式通知 View 刷新,以避免過多的 postValue() 造成主線程負(fù)擔(dān)過重;
3)減少非必要刷新:比如在消息列表滑動時,并不需要將接收到的新消息刷新出來,僅進(jìn)行提示即可;
4)識別數(shù)據(jù)的更新:通過谷歌的數(shù)據(jù)對比工具 DiffUtil 識別數(shù)據(jù)是否有更新,僅更新有變更的部分?jǐn)?shù)據(jù);
5)控制全局刷新次數(shù):盡量通過局部刷新進(jìn)行 UI 更新。
通過以上機制:從壓測結(jié)果看,在中端手機上,直播間聊天室中每秒 400 條消息時,消息列表仍然表現(xiàn)流暢,沒有卡頓。
10、針對傳統(tǒng)聊天消息外的自定義屬性優(yōu)化
10.1 概述
在直播間聊天室場景中,除了傳統(tǒng)的聊天消息收發(fā)以外,業(yè)務(wù)層經(jīng)常需要有自己的一些業(yè)務(wù)屬性,如在語音直播聊天室場景中的主播麥位信息、角色管理等,還有狼人殺等卡牌類游戲場景中記錄用戶的角色和牌局狀態(tài)等。
相對于傳統(tǒng)聊天消息,自定義屬性有必達(dá)和時效的要求,比如麥位、角色等信息需要實時同步給聊天室的所有成員,然后客戶端再根據(jù)自定義屬性刷新本地的業(yè)務(wù)。
10.2 自定義屬性的存儲
自定義屬性是以 key 和 value 的形式進(jìn)行傳遞和存儲的。自定義屬性的操作行為主要有兩種:即設(shè)置和刪除。
服務(wù)器存儲自定義屬性也分兩部分:
1)全量的自定義屬性集合;
2)自定義屬性集合變更記錄。
自定義屬性存儲結(jié)構(gòu)如下圖所示:

針對這兩份數(shù)據(jù),應(yīng)該提供兩種查詢接口,分別是查詢?nèi)繑?shù)據(jù)和查詢增量數(shù)據(jù)。這兩種接口的組合應(yīng)用可以極大提升聊天室服務(wù)的屬性查詢響應(yīng)和自定義分發(fā)能力。
10.3 自定義屬性的拉取
內(nèi)存中的全量數(shù)據(jù),主要給從未拉取過自定義屬性的成員使用。剛進(jìn)入聊天室的成員,直接拉取全量自定義屬性數(shù)據(jù)然后展示即可。
對于已經(jīng)拉取過全量數(shù)據(jù)的成員來說,若每次都拉取全量數(shù)據(jù),客戶端想獲得本次的修改內(nèi)容,就需要比對客戶端的全量自定義屬性與服務(wù)器端的全量自定義屬性,無論比對行為放在哪一端,都會增加一定的計算壓力。
所以:為了實現(xiàn)增量數(shù)據(jù)的同步,構(gòu)建一份屬性變更記錄集合十分必要。這樣:大部分成員在收到自定義屬性有變更來拉取時,都可以獲得增量數(shù)據(jù)。
屬性變更記錄采用的是一個有序的 map 集合:key 為變更時間戳,value 里存著變更的類型以及自定義屬性內(nèi)容,這個有序的 map 提供了這段時間內(nèi)所有的自定義屬性的動作。
自定義屬性的分發(fā)邏輯與消息一致:均為通知拉取。即客戶端在收到自定義屬性變更拉取的通知后,帶著自己本地最大自定義屬性的時間戳來拉取。比如:如果客戶端傳的時間戳為 4,則會拉取到時間戳為 5 和時間戳為 6 的兩條記錄??蛻舳死〉皆隽績?nèi)容后在本地進(jìn)行回放,然后對自己本地的自定義屬性進(jìn)行修改和渲染。
11、多人群聊參考資料
[1]?IM單聊和群聊中的在線狀態(tài)同步應(yīng)該用“推”還是“拉”?
[2]?IM群聊消息如此復(fù)雜,如何保證不丟不重?
[3]?移動端IM中大規(guī)模群消息的推送如何保證效率、實時性?
[4]?現(xiàn)代IM系統(tǒng)中聊天消息的同步和存儲方案探討
[5]?關(guān)于IM即時通訊群聊消息的亂序問題討論
[6]?IM群聊消息的已讀回執(zhí)功能該怎么實現(xiàn)?
[7]?IM群聊消息究竟是存1份(即擴散讀)還是存多份(即擴散寫)?
[8]?一套高可用、易伸縮、高并發(fā)的IM群聊、單聊架構(gòu)方案設(shè)計實踐
[9]?IM群聊機制,除了循環(huán)去發(fā)消息還有什么方式?如何優(yōu)化?
[10]?網(wǎng)易云信技術(shù)分享:IM中的萬人群聊技術(shù)方案實踐總結(jié)
[11]?阿里釘釘技術(shù)分享:企業(yè)級IM王者——釘釘在后端架構(gòu)上的過人之處
[12]?IM群聊消息的已讀未讀功能在存儲空間方面的實現(xiàn)思路探討
[13]?企業(yè)微信的IM架構(gòu)設(shè)計揭秘:消息模型、萬人群、已讀回執(zhí)、消息撤回等
[14]?融云IM技術(shù)分享:萬人群聊消息投遞方案的思考和實踐
(本文已同步發(fā)布于:http://www.52im.net/thread-3835-1-1.html)