本文由融雲技術團隊原創分享,有修訂和改動。
1、引言
在視訊直播場景中,彈幕互動、與主播的聊天、各種業務指令等等,組成了普通使用者與主播之間的互動方式。
從技術的角度來看,這些實時互動手段,底層邏輯都是實時聊天訊息或指令的分發,技術架構類比於IM應用的話,那就相當於IM聊天室功能。
本系列文章的上篇《百萬人線上的直播間實時聊天訊息分發技術實踐》主要分享的是訊息分發和丟棄策略。本文將主要從高可用、彈性擴縮容、使用者管理、訊息分發、客戶端優化等角度,分享直播間海量聊天訊息的架構設計技術難點的實踐經驗。
學習交流:
- 移動端IM開發入門文章:《新手入門一篇就夠:從零開發移動端IM》
- 開源IM框架原始碼:https://github.com/JackJiang2...
(本文已同步釋出於:http://www.52im.net/thread-38...)
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-38...)