本文由網易云信資深服務器開發工程師曹佳俊分享,原題“深度剖析“圈組”消息系統設計 | “圈組”技術系列文章”,為了提升內容品質,本文有修訂和刪節。
1、引言
鑒于實時社群產品Discord在IM垂直應用領域的爆火,類似的需求越來越多,云信的“圈組”就是針對這種應用場景的技術產品。
“圈組”產品發布后獲得了很大的關注,很多云信用戶在接入SDK的同時對于“圈組”的底層技術細節和原理也非常關注,為此我們決定推出“圈組”相關的技術文章,分享云信在“圈組”技術設計上的一些思考和實踐。
本文是序列文章的第2篇,將要分享的是云信的實時社群產品“圈組”(“圈組”云信的類Discord產品實現方案)的消息系統技術設計實踐。
技術交流:
(本文已同步發布于:http://www.52im.net/thread-4321-1-1.html)
2、系列文章
本文是系列文章中的第 2 篇:
3、作者介紹
曹佳?。?/strong>網易云信資深服務器開發工程師,畢業于中國科學院,碩士畢業后加入網易,負責云信 IM/RTC 信令等業務的服務器開發。專注于即時通訊、RTC 信令以及相關中間件等技術,是云信開源項目 Camellia 的作者。
4、“圈組”的技術特點
在介紹“圈組”的技術細節之前,我們先了解一下圈組的技術特點。
“圈組”產品最大的特點是什么?
- 1)首先:是 server/channel 的二級結構;
- 2)其次:是構建在二級結構之上的大規模社群(單個 server 數十萬甚至上百萬成員);
- 3)以及:使用復雜的身份組系統來管理如此規模的社群組織和成員。
那么對于這樣一個新穎的 IM 系統,在技術上應該如何實現呢?
5、“圈組”和傳統IM群組的技術差異
5.1概述
一種簡單的思路是改造已有的 IM 系統,對于“圈組”這樣的類 Discord 社群,第一個思路是拓展我們的群組功能,猛一看在很多方面確實挺像的。
我們做了個簡單的對比:
從上面的表格可以看到,“圈組”和群組最大的不同:
其他的諸如身份組、個性化推送策略,似乎只要適配的做一下就可以了。
那么是不是只要想辦法提升一下群組的容量,再在業務層封裝一下二級結構就可以了呢?
答案顯然是否定的,或者至少說基于群組去擴展不是一個很好的想法。
5.2二級結構的差異
首先是二級結構。
在類 Discord 的二級結構中,成員的管理在 server 層,而 channel 成員是繼承自 server 的,而且在 channel 之上還有很多可見性的配置(我們的“圈組”提供了黑白名單機制,而Discord 則提供了查看頻道權限)。
在這種機制之下,任何 server 層面的成員變動,都可能影響全部或者部分頻道的成員列表。
面對這種復雜的結構,群組有兩種思路去實現:
- 1)一種是 N 個群,邏輯上隸屬于同一個 server;
- 2)一種是一個群映射為一個 server。
不管哪種方式,先不說消息投遞這塊的邏輯,僅成員管理上邏輯的耦合和交織的復雜性,足以勸退任何人。
5.3容量的差異
常規IM群組的容量一般只有數百,最多可以擴展到數千。
對于IM群組成員的管理,我們一般采取全量+增量同步相結合的方案,客戶端和服務器映射到相同的群組鏡像(群信息+群成員等)。此時很多操作,例如群成員的展示、檢索,消息的艾特等,都可以基于純客戶端進行。
而“圈組”要求幾十萬甚至上百萬的容量,顯然客戶端無法一次性獲取到所有成員,如果你一次性加入多個 server,那成員的數量將更加膨脹。
因此在“圈組”這種大規模社群的設計中,很多邏輯都會轉向云端,此時不管是 SDK 還是服務器,均需要修改原有的設計邏輯。
5.4消息規模差異
此外,大規模社群帶來的是消息爆炸。
在原有的IM群組設計中,假設一個人同時加入了 1000 個群,那么這 1000 個群內的所有消息均會在第一時間下發給給客戶端。
但是在一般的業務場景中,不會所有的群都同時活躍,假設這 1000 個群變成了 1000 個服務器/頻道,作為一種社群組織,同時活躍的可能性將大大增加,而且每個服務器/頻道的人數遠遠超過普通的群組,疊加之后帶來的消息爆炸現象在原有的群組體系中將帶來極大的壓力。
壓力包括多方面:
- 1)首先是海量消息的存儲壓力;
- 2)其次是海量消息在線廣播/離線消息推送帶來的帶寬和服務器壓力;
- 3)以及客戶端在面對大量消息沖擊時如何有效地接受和合理的展示。
5.5小結
除了容量、二級結構、消息規模,包括身份組、成員管理、個性化推送策略等等都存在巨大差異。
是否真的適合在群組中添加這些復雜邏輯呢,強行綁定在一起會不會既沒有一個好用的類 Discord 平臺,也使得原始的群組功能繁雜,反而降低了易用性呢?
經過上面的一些分析,我們基本可以得出一個結論:在已有的群組基礎上擴展來實現一個類 Discord 功能的社群,顯然不是一個很好的思路。
那么還有其他“捷徑”嗎?
IM聊天室也是一個潛在的選項,聊天室的一大特點就是支持超大規模同時在線(參見《千萬級實時直播彈幕的技術實踐》),容量似乎已經不是問題,但是當考慮添加其他一些強社交關系的特性時(如成員、身份組等)就顯得有點為難了,聊天室本身就是來去自如的一個開放空間,這個和圈組的產品本身定位互相沖突的。
因此基于聊天室擴展的方案也基本 pass 掉了。
6、“圈組”的技術難點
基于上述種種的思考和討論,最終選擇脫離已有 IM 體系,從零研發一套全新的社群方案“圈組”,“圈組”不是一個簡單的 IM 功能,而是一套可以獨立運行的 IM 系統。
經過上面的討論,相信大家對“圈組”本身的技術特點和難點也有所理解。
可以歸納為以下幾點:
- 1)二級結構下成員無上限的社交關系系統設計;
- 2)超大社群下消息系統設計;
- 3)復雜高效的身份組系統設計;
7、“圈組”技術實現之整體架構
“圈組”整體架構:
上面展示了“圈組”服務整體的架構。
可以看到整個“圈組”服務是一個分層的架構:
- 1)首先是接入層,包括 LBS 服務和長鏈接服務器以及 API 網關,對應客戶端 SDK 和用戶服務器;
- 2)后面是網絡層,包括大網 WE-CAN 和協議路由服務;
- 3)其次是服務層,劃分了多個服務模塊,每個模塊都包括多個微服務;
- 4)最后是基礎設施。
8、“圈組”技術實現之消息系統架構
這其中和消息系統相關聯的包括接入層、網絡層、以及后端的登錄/訂閱/消息/檢索等模塊。
基本架構如下:
消息系統中第一個要討論的點就是消息的存儲和分發方式,包括在線廣播、離線推送、歷史消息三個維度。
下面幾節我們將對消息系統中各模塊分別展開介紹。
9、“圈組”消息系統技術實現1:在線廣播
對于一般的IM群組來說,在線廣播的一般過程是這樣的:依次查詢群組里的所有人的在線狀態,如果在線,則將消息發送給對應的長鏈接服務器。
顯然這種機制無法復制到“圈組”,因為在“圈組”的一個服務器里可能存在超過 100w 的人。
此外:IM聊天室的廣播模式也不能直接復用,因為在聊天室架構中,每個長鏈接映射到一個聊天室,因此當你登錄到某個聊天室的時候,你只會收到該聊天室的消息。而對于“圈組”來說,每個用戶會同時加入多個服務器/頻道,而且會同時收到多個服務器/頻道的消息。
針對“圈組”的上述特點:我們設計了消息訂閱模式,也就是用戶登錄之后,需要訂閱感興趣的相關服務器/頻道,服務器會記錄下這個訂閱信息。當有新消息的時候,服務器通過訂閱關系(而不是在線狀態)查詢到需要廣播的列表,通過這種方式就不再需要遍歷服務器/頻道里的所有用戶。
但是當一個服務器/頻道里在線人數非常多的時候,這個訂閱關系仍然是巨大的。
為此:我們設計了一種兩層訂閱模型,即所有的訂閱關系會保存在長鏈接服務器上(QChatLink/QChatWebLink),同時長鏈接服務器會定時發送心跳給后端的訂閱服務器,心跳信息相比原始的訂閱信息會大大簡化,比如長鏈接服務器上會記錄賬號 A 訂閱了某個頻道 A 的消息,如果有 1w 個賬號,則有 1w 條訂閱記錄,而心跳信息里只會上報有 1w 個人訂閱了某個頻道 A 的消息,具體的賬號列表則被精簡掉了。當一條消息需要廣播時,消息服務會訪問訂閱服務,獲取到該服務器/頻道被訂閱的長鏈接服務器列表,并依次給該列表中的長鏈接服務器發送消息下發通知,長鏈接服務器收到通知后會根據訂閱詳情再廣播給所有客戶端。
此外:我們還提供了多種訂閱類型,當你非常關心某個頻道消息時(比如頁面正停留在該頻道),此時你可以訂閱該頻道的消息。對于其他頻道,如果你僅僅需要知道該頻道有多少條未讀消息(或者有無未讀消息),則可以選擇訂閱該頻道的未讀計數(或者未讀狀態),此時服務下發時僅會廣播精簡的消息體用于維護客戶端未讀計數,并且當未讀計數達到一定閾值之后(比如 99+),服務器可以選擇不再下發任何通知消息而不影響用戶體驗。
通過上文介紹的消息訂閱模型,極大地提高了超大型的圈組頻道/服務器消息在線廣播的效率,降低了服務器壓力。
除此之外:我們還設計了針對小型頻道的特殊策略,對于小型頻道,即使不訂閱,服務器也會下發消息通知給頻道里所有人,從而減輕端側消息訂閱模型的維護成本。針對消息訂閱機制本身,后續我們也會根據不同的業務場景,提供更多一站式的策略來幫助降低接入成本,提升整體的易用性。
10、“圈組”消息系統技術實現2:離線推送
在強社交的場景下,離線消息推送對于維持用戶粘性+提升產品體驗有很大的作用。
從技術角度看的話,主要解決2個問題:
1)第一個是超大型服務器/頻道的消息推送的效率問題;
2)另一個是提供足夠豐富的推送策略來幫助 C 端用戶,避免被過量的推送消息給打擾。
針對第一個問題,我們針對不同規模的服務器/頻道采取了不同的策略:
- 1)對于小型頻道:采用類似于群組的消息推送模型;
- 2)對于大型頻道:對于每一條需要推送的消息,會根據目標用戶的 ID 進行任務分片,多個節點并行操作,提高推送效率。
此外:分片會采用一致性策略,保證單個用戶固定為某些節點,從而提高緩存命中效率。
針對第二個問題,推送策略可以用以下幾句話來描述:
- 1)既關注促活,又保證不打擾;
- 2)大型 server 是游樂場,只推送與用戶相關的重要消息(如 @消息);
- 3)小型 server 是與朋友相處的小天地,支持消息的全部推送。
并且:未來用戶還可以自定義消息的高低優先級,并搭配不同的推送配置(如不同的免打擾配置等),如下圖所示。
11、“圈組”消息系統技術實現3:歷史消息
歷史消息的存儲在“圈組”的場景中也需要一些特別的設計。
同樣以傳統IM群組為例,一般來說消息的存儲方式有兩種,寫擴散和讀擴散。在小型的IM群組或者多人會話中,寫擴散模式可以簡化設計,但是當群組規模擴大到一定程度(如萬人群),讀擴散就成了選擇。
而對于“圈組”這種單個服務器可能上百萬人的“群組”中,除了常規的讀擴散之外,我們還設計了多級緩存的結構來應對海量的讀請求。
基本的存儲架構大致如下:
消息的存儲主要包括兩部分:
首先是寫入:對于上述兩者,我們都會使用中心化的緩存服務器來存儲最近的數據,并使用異步+批量+聚合等手段,通過 MQ 異步落庫,從而平衡寫入效率(單條寫入性能低)和寫入讀取延遲(異步寫入有延遲)的問題,并且針對不同數據類型的特點,我們也選擇了不同的存儲方案(歷史消息使用分布式時間序列數據庫,未讀計數使用分布式 k-v 數據庫),最大化地提升消息存儲和查詢的性能和效率。
有寫就有讀,針對讀取操作:
- 1)所有最近的消息和未讀計數均會存儲在中心化緩存中,并通過先進先出和緩存過期等不同的策略來確保緩存中存儲的永遠是最新和最熱的數據;
- 2)對于消息 ID 和消息內容本身,中心化緩存中也會有不同的數據結構和過期策略,來平衡緩存命中率和緩存容量消耗;
- 3)當緩存過期了,如果有關聯的讀寫請求,將會觸發緩存的重建,以保證緩存的命中率始終保持在較高水位;
- 4)當有高頻的讀請求,還會觸發熱點 cache 的檢測,并將一部分讀請求下沉到各個計算節點的內存中,以應對突發流量的沖擊。
上述針對“圈組”的特別設計,消息存儲系統可以應對幾十數百人的小型圈組頻道,也可以從容應對上百萬的超大型頻道。
12、相關資料
[1] IM群聊消息究竟是存1份(即擴散讀)還是存多份(即擴散寫)?
[2] 網易云信技術分享:IM中的萬人群聊技術方案實踐總結
[3] 企業微信的IM架構設計揭秘:消息模型、萬人群、已讀回執、消息撤回等
[4] 融云IM技術分享:萬人群聊消息投遞方案的思考和實踐
[5] 微信直播聊天室單房間1500萬在線的消息架構演進之路
[6] 百萬人在線的直播間實時聊天消息分發技術實踐
[7] 千萬級實時直播彈幕的技術實踐
[8] 深度解密釘釘即時消息服務DTIM的技術設計
[9] 深度揭密RocketMQ在釘釘IM系統中的應用實踐
[10] 一套億級用戶的IM架構技術干貨(上篇):整體架構、服務拆分等
[11] 一套億級用戶的IM架構技術干貨(下篇):可靠性、有序性、弱網優化等
[12] 從新手到專家:如何設計一套億級消息量的分布式IM系統
(本文已同步發布于:http://www.52im.net/thread-4321-1-1.html)