深度剖析「圈組」訊息系統設計 | 「圈組」技術系列文章

網易雲信發表於2022-04-06

導讀:

網易雲信新晉的 IM 頂流產品「圈組」出道後獲取到了極大的關注,很多雲信的客戶在接入的同時對於「圈組」的底層技術細節和原理也非常關注,為此,我們決定推出雲信「圈組」相關的系列技術文章,分享網易雲信在「圈組」技術設計上的一些思考。

文|曹佳俊 網易雲信資深伺服器開發工程師

一、技術選型

在介紹「圈組」的技術細節之前,我們先分析一下圈組的技術特點(瞭解「圈組」可閱讀下面兩篇文章:行業洞見丨Discord 狂奔的背後與網易雲信「圈組」的長期主義 或 正式出「圈」丨網易雲信圈組的近謀與遠慮),「圈組」產品最大的特點是什麼?首先是 server/channel 的二級結構;其次是構建在二級結構之上的大規模社群(單個 server 數十萬甚至上百萬成員),以及使用複雜的身份組系統來管理如此規模的社群組織和成員。

那麼對於這樣一個新穎的 IM 系統,在技術上應該如何實現呢?

一種簡單的思路是改造已有的 IM 系統,對於「圈組」這樣的類 Discord 社群,第一個思路是擴充我們的群組功能,猛一看在很多方面確實挺像的,我們做了個簡單的對比:

從上面的表格可以看到,「圈組」和群組最大的不同,一個是容量的區別,一個是二級結構。 其他的諸如身份組、個性化推送策略,似乎只要適配的做一下就可以了,那麼是不是隻要想辦法提升一下群組的容量,再在業務層封裝一下二級結構就可以了呢?答案顯然是否定的,或者至少說基於群組去擴充套件不是一個很好的想法。

首先是二級結構,在類 Discord 的二級結構中,成員的管理在 server 層,而 channel 成員是繼承自 server 的,而且在 channel 之上還有很多可見性的配置(雲信「圈組」提供了黑白名單機制,Discord 則提供了檢視頻道許可權),在這種機制之下,任何 server 層面的成員變動,都可能影響全部或者部分頻道的成員列表;面對這種複雜的結構,群組有兩種思路去實現,一種是 N 個群,邏輯上隸屬於同一個 server;還有一種是一個群對映為一個 server。不管哪種方式,先不說訊息投遞這塊的邏輯,僅成員管理上邏輯的耦合和交織的複雜性,足以勸退任何人。

其次是容量,常規的群組的容量一般只有數百,最多可以擴充套件到數千,對於群組成員的管理,我們一般採取全量+增量同步相結合的方案,客戶端和伺服器對映到相同的群組映象(群資訊+群成員等),此時很多操作,例如群成員的展示、檢索,訊息的艾特等,都可以基於純客戶端進行。而「圈組」要求幾十萬甚至上百萬的容量,顯然客戶端無法一次性獲取到所有成員,如果你一次性加入多個 server,那成員的數量將更加膨脹。因此在「圈組」這種大規模社群的設計中,很多邏輯都會轉向雲端,此時不管是 SDK 還是伺服器,均需要修改原有的設計邏輯。

此外,大規模社群帶來的是訊息爆炸,在原有的群組設計中,假設一個人同時加入了 1000 個群,那麼這 1000 個群內的所有訊息均會在第一時間下發給給客戶端,但是在一般的業務場景中,不會所有的群都同時活躍,假設這 1000 個群變成了 1000 個伺服器/頻道,作為一種社群組織,同時活躍的可能性將大大增加,而且每個伺服器/頻道的人數遠遠超過普通的群組,疊加之後帶來的訊息爆炸現象在原有的群組體系中將帶來極大的壓力,壓力包括多方面:首先是海量訊息的儲存壓力,其次是海量訊息線上廣播/離線訊息推送帶來的頻寬和伺服器壓力,以及客戶端在面對大量訊息衝擊時如何有效地接受和合理的展示。

除了容量和二級結構,包括身份組、成員管理、個性化推送策略等等,是否真的適合在群組中新增這些複雜邏輯呢,強行繫結在一起會不會既沒有一個好用的類 Discord 平臺,也使得原始的群組功能繁雜,反而降低了易用性呢?

經過上面的一些分析,我們基本可以得出一個結論,在已有的群組基礎上擴充套件來實現一個類 Discord 功能的社群,顯然不是一個很好的思路,那麼還有其他“捷徑”嗎?聊天室也是一個潛在的選項,聊天室的一大特點就是支援超大規模同時線上(參考文章:網易實踐|千萬級線上直播彈幕方案),容量似乎已經不是問題,但是當考慮新增其他一些強社交關係的特性時(如成員、身份組等)就顯得有點為難了,聊天室本身就是來去自如的一個開放空間,這個和圈組的產品本身定位互相沖突的,因此基於聊天室擴充套件的方案也基本 pass 掉了。

二、技術難點

基於上述種種的思考和討論,最終網易雲信選擇脫離已有 IM 體系,從零研發一套全新的社群方案「圈組」,「圈組」不是一個簡單的 IM 功能,而是一套可以獨立執行的 IM 系統,經過上面的討論,相信大家對「圈組」本身的技術特點和難點也有所理解,可以歸納為以下幾點:

  1. 二級結構下成員無上限的社交關係系統設計。
  2. 超大社群下訊息系統設計。
  3. 複雜高效的身份組系統設計。

三、「圈組」訊息系統技術剖析

本文將針對上述三點之中的第二點:超大社群下的訊息系統設計,分享我們的一些技術設計原理和經驗。

(一) 「圈組」整體架構

 

上面展示了「圈組」服務整體的架構,可以看到整個「圈組」服務是一個分層的架構,首先是接入層,包括 LBS 服務和長連結伺服器以及 API 閘道器,對應客戶端 SDK 和使用者伺服器;後面是網路層,包括大網 WE-CAN 和協議路由服務;其次是服務層,劃分了多個服務模組,每個模組都包括多個微服務;最後是基礎設施

(二)訊息系統架構

這其中和訊息系統相關聯的包括接入層、網路層、以及後端的登入/訂閱/訊息/檢索等模組,基本架構如下:

下文我們將對訊息系統中各模組分別展開介紹。

(三)訊息系統技術細節

訊息系統中第一個要討論的點就是訊息的儲存和分發方式,包括線上廣播、離線推送、歷史訊息三個維度。

  • 線上廣播

對於一般的群組來說,線上廣播的一般過程是這樣的:依次查詢群組裡的所有人的線上狀態,如果線上,則將訊息傳送給對應的長連結伺服器。顯然這種機制無法複製到「圈組」,因為在「圈組」的一個伺服器裡可能存在超過 100w 的人;此外,聊天室的廣播模式也不能直接複用,因為在聊天室架構中,每個長連結對映到一個聊天室,因此當你登入到某個聊天室的時候,你只會收到該聊天室的訊息,而對於「圈組」來說,每個使用者會同時加入多個伺服器/頻道,而且會同時收到多個伺服器/頻道的訊息。

針對「圈組」的上述特點,雲信設計了訊息訂閱模式,也就是使用者登入之後,需要訂閱感興趣的相關伺服器/頻道,伺服器會記錄下這個訂閱資訊,當有新訊息的時候,伺服器通過訂閱關係(而不是線上狀態)查詢到需要廣播的列表,通過這種方式就不再需要遍歷伺服器/頻道里的所有使用者;

但是當一個伺服器/頻道里線上人數非常多的時候,這個訂閱關係仍然是巨大的,為此雲信設計了一種兩層訂閱模型,所有的訂閱關係會儲存在長連結伺服器上(QChatLink/QChatWebLink),同時長連結伺服器會定時傳送心跳給後端的訂閱伺服器,心跳資訊相比原始的訂閱資訊會大大簡化,比如長連結伺服器上會記錄賬號 A 訂閱了某個頻道 A 的訊息,如果有 1w 個賬號,則有 1w 條訂閱記錄,而心跳資訊裡只會上報有 1w 個人訂閱了某個頻道 A 的訊息,具體的賬號列表則被精簡掉了;當一條訊息需要廣播時,訊息服務會訪問訂閱服務,獲取到該伺服器/頻道被訂閱的長連結伺服器列表,並依次給該列表中的長連結伺服器傳送訊息下發通知,長連結伺服器收到通知後會根據訂閱詳情再廣播給所有客戶端。

此外,我們還提供了多種訂閱型別,當你非常關心某個頻道訊息時(比如頁面正停留在該頻道),此時你可以訂閱該頻道的訊息;對於其他頻道,如果你僅僅需要知道該頻道有多少條未讀訊息(或者有無未讀訊息),則可以選擇訂閱該頻道的未讀計數(或者未讀狀態),此時服務下發時僅會廣播精簡的訊息體用於維護客戶端未讀計數,並且當未讀計數達到一定閾值之後(比如 99+),伺服器可以選擇不再下發任何通知訊息而不影響使用者體驗。

通過上文介紹的訊息訂閱模型,極大地提高了超大型的圈組頻道/伺服器訊息線上廣播的效率,降低了伺服器壓力;除此之外,我們還設計了針對小型頻道的特殊策略,對於小型頻道,即使不訂閱,伺服器也會下發訊息通知給頻道里所有人,從而減輕端側訊息訂閱模型的維護成本;針對訊息訂閱機制本身,後續我們也會根據不同的業務場景,提供更多一站式的策略來幫助降低接入成本,提升整體的易用性。

  • 離線推送

在強社交的場景下,離線推送對於維持使用者粘性+提升產品體驗有很大的作用。從技術角度看的話,主要解決2個問題,第一個是超大型伺服器/頻道的訊息推送的效率問題; 另外一個是提供足夠豐富的推送策略來幫助 C 端使用者,避免被過量的推送訊息給打擾。

針對第一個問題,雲信「圈組」針對不同規模的伺服器/頻道採取了不同的策略,對於小型頻道,採用類似於群組的訊息推送模型;而對於大型頻道,對於每一條需要推送的訊息,會根據目標使用者的 ID 進行任務分片,多個節點並行操作,提高推送效率。此外分片會採用一致性策略,保證單個使用者固定為某些節點,從而提高快取命中效率。

針對第二個問題,雲信「圈組」的推送策略可以用以下幾句話來描述:

  • 既關注促活,又保證不打擾
  • 大型 server 是遊樂場,只推送與使用者相關的重要訊息(如 @訊息)
  • 小型 server 是與朋友相處的小天地,支援訊息的全部推送

並且未來使用者還可以自定義訊息的高低優先順序,並搭配不同的推送配置(如不同的免打擾配置等):

 

  • 歷史訊息

歷史訊息的儲存在「圈組」的場景中也需要一些特別的設計。同樣以群組為例,一般來說訊息的儲存方式有兩種,寫擴散和讀擴散,在小型的群組或者多人會話中,寫擴散模式可以簡化設計,但是當群組規模擴大到一定程度(如萬人群),讀擴散就成了選擇,而對於「圈組」這種單個伺服器可能上百萬人的“群組”中,除了常規的讀擴散之外,我們還設計了多級快取的結構來應對海量的讀請求,基本的儲存架構大致如下:

訊息的儲存主要包括兩部分,一部分是訊息本身,還有一部分是未讀計數

首先是寫入,對於上述兩者,我們都會使用中心化的快取伺服器來儲存最近的資料,並使用非同步+批量+聚合等手段,通過 MQ 非同步落庫,從而平衡寫入效率(單條寫入效能低)和寫入讀取延遲(非同步寫入有延遲)的問題,並且針對不同資料型別的特點,我們也選擇了不同的儲存方案(歷史訊息使用分散式時間序列資料庫,未讀計數使用分散式 k-v 資料庫),最大化地提升訊息儲存和查詢的效能和效率。

有寫就有讀,針對讀取操作,所有最近的訊息和未讀計數均會儲存在中心化快取中,並通過先進先出和快取過期等不同的策略來確保快取中儲存的永遠是最新和最熱的資料;此外對於訊息 ID 和訊息內容本身,中心化快取中也會有不同的資料結構和過期策略,來平衡快取命中率和快取容量消耗;當快取過期了,如果有關聯的讀寫請求,將會觸發快取的重建,以保證快取的命中率始終保持在較高水位;最後,當有高頻的讀請求,還會觸發熱點 cache 的檢測,並將一部分讀請求下沉到各個計算節點的記憶體中,以應對突發流量的衝擊。

上述針對「圈組」的特別設計,訊息儲存系統可以應對幾十數百人的小型圈組頻道,也可以從容應對上百萬的超大型頻道。

  • 特色功能

說完了訊息系統的核心儲存和分發模式,「圈組」的訊息系統還提供了很多額外的特色功能:

  • 訊息更新: 所謂的訊息更新是指訊息傳送之後還允許修改,訊息撤回和刪除被認為是訊息更新的一種特殊狀態,這在管理一個大型社群中的訊息時有著重要的作用。
  • 訊息互動(即將推出):「圈組」提供了比 Discord 的訊息回覆更強的 thread 聊天功能,不同於子區是單獨開闢一塊空間,thread 聊天可以讓你在複雜的訊息流中自動篩選出關於某一個話題的所有關聯訊息。「圈組」也提供了快捷評論的功能,你可以給一條訊息新增各種自定義的表情,這在大型的頻道中幾乎是剛需,因為你再也不用忍受訊息爆炸了。雲信針對大型頻道的快捷評論也做了特殊的優化,當一條訊息獲得大家的一致喜愛時,頻道里所有人都可以給他點一個?或者?,數量不設上限。
  • 訊息檢索(敬請期待):「圈組」的檢索系統也是一大特色,訊息檢索自然是其中重要的一環,訊息檢索將助你在繁雜的訊息流中尋找到你想要的訊息
  • 第三方回撥和抄送: 這充分體現了「圈組」作為雲信 PaaS 平臺產品的一大特點,通過第三方回撥和抄送,你可以在各種各樣的操作(如發訊息、拉人踢人、修改資訊等)的 before 和 after,植入任何你想要的邏輯,譬如機器人、內容審計等(當然雲信也有安全通一站式內容安全解決方案,可以按需選擇)。

四、總結

說了這麼多,雲信「圈組」作為一款全新設計的產品,沒有任何歷史包袱的限制(但是卻可以充分吸收歷史優點),你可以使用它構建一個類 Discord 產品,或者任何你想得到的社交/娛樂/遊戲產品,歡迎大家選擇。

作者介紹

曹佳俊,網易雲信資深伺服器開發工程師,畢業於中國科學院,碩士畢業後加入網易,負責雲信 IM/RTC 信令等業務的伺服器開發。專注於即時通訊、RTC 信令以及相關中介軟體等技術,是雲信開源專案 Camellia 的作者。

相關文章