本文由融云技術團隊原創分享,有修訂和改動。
1、引言
在視頻直播場景中,彈幕交互、與主播的聊天、各種業務指令等等,組成了普通用戶與主播之間的互動方式。
從技術的角度來看,這些實時互動手段,底層邏輯都是實時聊天消息或指令的分發,技術架構類比于IM應用的話,那就相當于IM聊天室功能。
本系列文章的上篇《百萬人在線的直播間實時聊天消息分發技術實踐》主要分享的是消息分發和丟棄策略。本文將主要從高可用、彈性擴縮容、用戶管理、消息分發、客戶端優化等角度,分享直播間海量聊天消息的架構設計技術難點的實踐經驗。
(本文已同步發布于:http://www.52im.net/thread-3835-1-1.html)
2、系列文章
本文是系列文章中的第7篇:
《直播系統聊天技術(一):百萬在線的美拍直播彈幕系統的實時推送技術實踐之路》
《直播系統聊天技術(二):阿里電商IM消息平臺,在群聊、直播場景下的技術實踐》
《直播系統聊天技術(三):微信直播聊天室單房間1500萬在線的消息架構演進之路》
《直播系統聊天技術(四):百度直播的海量用戶實時消息系統架構演進實踐》
《直播系統聊天技術(五):微信小游戲直播在Android端的跨進程渲染推流實踐》
《直播系統聊天技術(六):百萬人在線的直播間實時聊天消息分發技術實踐》
《直播系統聊天技術(七):直播間海量聊天消息的架構設計難點實踐》(* 本文)
3、直播間的主要功能和技術特征
如今的視頻直播間早已不單純是視頻流媒體技術問題,它還包含了用戶可感知的多類型消息發送和管理、用戶管理等任務。在萬物皆可直播的當下,超大型直播場景屢見不鮮,甚至出現了人數無上限的場景,面對如此海量實時消息和指令的并發挑戰,帶來的技術難度已非常規手段所能解決。
我們先來歸納一下如今的典型視頻直播間,相較于傳統直播間所包含的主要功能特征、技術特征等。
豐富的消息類型和進階功能:
- 1)可發送文字、語音、圖片等傳統聊天功能;
- 2)可實現點贊、禮物等非傳統聊天功能的消息類型;
- 3)可管理內容安全,包括敏感詞設置,聊天內容反垃圾處理等。
聊天管理功能:
- 1)用戶管理:包括創建、加入、銷毀、禁言、查詢、封禁(踢人)等;
- 2)用戶白名單:白名單用戶處于被保護狀態不會被自動踢出,且發送消息優先級別最高;
- 3)消息管理:包括消息優先級、消息分發控制等;
- 4)實時統計及消息路由等能力。
人數上限和行為特征:
- 1)人數沒有上限:一些大型直播場景,如春晚、國慶大閱兵等,直播間累計觀看動輒上千萬人次,同時觀看人數也可達數百萬;
- 2)用戶進退行為:用戶進出直播間非常頻繁,高熱度直播間的人員進出秒并發可能上萬,這對服務支撐用戶上下線以及用戶管理的能力提出了非常大的挑戰。
海量消息并發:
- 1)消息并發量大:直播聊天室人數沒有明顯上限,帶來了海量并發消息的問題(一個百萬人數的聊天室,消息的上行已是巨量,消息分發量更是幾何級上升);
- 2)消息實時性高:如果服務器只做消息的消峰處理,峰值消息的堆積會造成整體消息延時增大。
針對上述第 2) 點,延時的累積效應會導致消息與直播視頻流在時間線上產生偏差,進而影響用戶觀看直播時互動的實時性。所以,服務器的海量消息快速分發能力十分重要。
4、直播間聊天室的架構設計
高可用系統需要支持服務故障自動轉移、服務精準熔斷降級、服務治理、服務限流、服務可回滾、服務自動擴容 / 縮容等能力。
以服務高可用為目標的直播間聊天室系統架構如下:

如上圖所示,系統架構主要分三層:
- 1)連接層:主要管理服務跟客戶端的長鏈接;
- 2)存儲層:當前使用的是 Redis,作為二級緩存,主要存儲聊天室的信息(比如人員列表、黑白名單、封禁列表等,服務更新或重啟時,可以從 Redis 中加載出聊天室的備份信息);
- 3)業務層:這是整個聊天室的核心,為了實現跨機房容災,將服務部署在多個可用區,并根據能力和職責,將其分為聊天室服務和消息服務。
聊天室服務和消息服務的具體職責:
- 1)聊天室服務:主要負責處理管理類請求,比如聊天室人員的進出、封禁 / 禁言、上行消息處理審核等;
- 2)消息服務:主要緩存本節點需要處理的用戶信息以及消息隊列信息,并負責聊天室消息的分發。
在海量用戶高并發場景下,消息分發能力將決定著系統的性能。以一個百萬級用戶量的直播間聊天室為例,一條上行消息對應的是百萬倍的分發。這種情況下,海量消息的分發,依靠單臺服務器是無法實現的。
我們的優化思路是:將一個聊天室的人員分拆到不同的消息服務上,在聊天室服務收到消息后向消息服務擴散,再由消息服務分發給用戶。
以百萬在線的直播間聊天室為例:假設聊天室消息服務共 200 臺,那平均每臺消息服務管理 5000 人左右,每臺消息服務在分發消息時只需要給落在本臺服務器上的用戶分發即可。
服務落點的選擇邏輯:
- 1)在聊天室服務中:聊天室的上行信令是依據聊天室 ID 使用一致性哈希算法來選擇節點的;
- 2)在消息服務中:依據用戶 ID 使用一致性哈希算法來決定用戶具體落在哪個消息服務。
一致性哈希選擇的落點相對固定,可以將聊天室的行為匯聚到一個節點上,極大提升服務的緩存命中率。
聊天室人員進出、黑 / 白名單設置以及消息發送時的判斷等處理直接訪問內存即可,無須每次都訪問第三方緩存,從而提高了聊天室的響應速度和分發速度。
最后:Zookeeper 在架構中主要用來做服務發現,各服務實例均注冊到 Zookeeper。
5、直播間聊天室的擴縮容能力
5.1 概述
隨著直播這種形式被越來越多人接受,直播間聊天室面對人數激增致使服務器壓力逐步增大的情況越來越多。所以,在服務壓力逐步增大 / 減少的過程中能否進行平滑的擴 / 縮容非常重要。
在服務的自動擴縮容方面,業內提供的方案大體一致:即通過壓力測試了解單臺服務器的瓶頸點 → 通過對業務數據的監控來判斷是否需要進行擴縮 → 觸發設定的條件后報警并自動進行擴縮容。
鑒于直播間聊天室的強業務性,具體執行中應該保證在擴縮容中整體聊天室業務不受影響。
5.2 聊天室服務擴縮容
聊天室服務在進行擴縮容時,我們通過 Redis 來加載成員列表、封禁 / 黑白名單等信息。
需要注意的是:在聊天室進行自動銷毀時,需先判斷當前聊天室是否應該是本節點的。如果不是,跳過銷毀邏輯,避免 Redis 中的數據因為銷毀邏輯而丟失。
聊天室服務擴縮容方案細節如下圖所示:
5.3 消息服務擴縮容
消息服務在進行擴縮容時,大部分成員需要按照一致性哈希的原則路由到新的消息服務節點上。這個過程會打破當前的人員平衡,并做一次整體的人員轉移。
1)在擴容時:我們根據聊天室的活躍程度逐步轉移人員。
2)在有消息時:[消息服務會遍歷緩存在本節點上的所有用戶進行消息的通知拉取,在此過程中判斷此用戶是否屬于這臺節點(如果不是,將此用戶同步加入到屬于他的節點)。
3)在拉消息時:用戶在拉取消息時,如果本機緩存列表中沒有該用戶,消息服務會向聊天室服務發送請求確認此用戶是否在聊天室中(如果在則同步加入到消息服務,不在則直接丟掉)。
4)在縮容時:消息服務會從公共 Redis 獲得全部成員,并根據落點計算將本節點用戶篩選出來并放入用戶管理列表中。
6、海量用戶的上下線和管理
聊天室服務:管理了所有人員的進出,人員的列表變動也會異步存入 Redis 中。
消息服務:則維護屬于自己的聊天室人員,用戶在主動加入和退出房間時,需要根據一致性哈希算出落點后同步給對應的消息服務。
聊天室獲得消息后:聊天室服務廣播給所有聊天室消息服務,由消息服務進行消息的通知拉取。消息服務會檢測用戶的消息拉取情況,在聊天室活躍的情況下,30s 內人員沒有進行拉取或者累計 30 條消息沒有拉取,消息服務會判斷當前用戶已經離線,然后踢出此人,并且同步給聊天室服務對此成員做下線處理。
7、海量聊天消息的分發策略
直播間聊天室服務的消息分發及拉取方案如下圖:
7.1 消息通知的拉取
在上圖中:用戶 A 在聊天室中發送一條消息,首先由聊天室服務處理,聊天室服務將消息同步到各消息服務節點,消息服務向本節點緩存的所有成員下發通知拉取(圖中服務器向用戶 B 和用戶 Z 下發了通知)。
在消息分發過程中,server 做了通知合并。
通知拉取的詳細流程為:
- 1)客戶端成功加入聊天,將所有成員加入到待通知隊列中(如已存在則更新通知消息時間);
- 2)下發線程,輪訓獲取待通知隊列;
- 3)向隊列中用戶下發通知拉取。
通過這個流程可保障下發線程一輪只會向同一用戶發送一個通知拉取(即多個消息會合并為一個通知拉取),有效提升了服務端性能且降低了客戶端與服務端的網絡消耗。
7.2 消息的拉取
用戶的消息拉取流程如下圖:
如上圖所示,用戶 B 收到通知后向服務端發送拉取消息請求,該請求最終將由消息節點 1 進行處理,消息節點 1 將根據客戶端傳遞的最后一條消息時間戳,從消息隊列中返回消息列表(參考下圖 )。
客戶端拉取消息示例:

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

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

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