基於實踐:一套百萬消息量小規模IM系統技術要點總結

本文由公衆號“後臺技術匯”分享,原題“基於實踐,設計一個百萬級別的高可用 & 高可靠的 IM 消息系統”,原文鏈接在文末。由於原文存在較多錯誤和不準確內容,有大量修訂和改動。

1、引言

大家好,我是公衆號“後臺技術匯”的博主“一枚少年”。

本人從事後臺開發工作 3 年有餘了,其中讓我感觸最深刻的一個項目,就是在兩年前從架構師手上接過來的 IM 消息系統。

本文內容將從開發者的視角出發(主要是我自已的開發體會),圍繞項目背景、業務需求、技術原理、開發方案等主題,一步一步的與大家一起剖析:設計一套百萬消息量的小規模IM系統架構設計上需要注意的技術要點。

學習交流:

- 移動端IM開發入門文章:《新手入門一篇就夠:從零開發移動端IM

- 開源IM框架源碼:https://github.com/JackJiang2011/MobileIMSDK 

2、項目背景

我們仔細觀察就能發現,生活中的任何類型互聯網服務都有 IM 系統的存在。

比如:

  • 1)基礎性服務類-騰訊新聞(評論消息);
  • 2)商務應用類-釘釘(審批工作流通知);
  • 3)交流娛樂類-QQ/微信(私聊羣聊 &討論組 &朋友圈);
  • 4)互聯網自媒體-抖音快手(點贊打賞通知)。

在這些林林總總的互聯網生態產品裏,即時消息系統作爲底層能力,在確保業務正常與用戶體驗優化上,始終扮演了至關重要的角色。

所以,現如今的互聯網產品中,即時通訊技術已經不僅限於傳統IM聊天工具本身,它早已通過有形或無形的方式嵌入到了各種形式的互聯網應用當中。IM技術(或者說即時通訊技術)對於很多開發者來說,確實是必不好可少的領域知識,不可或缺。

3、系統能力

典型的IM系統通常需要滿足四點能力:高可靠性、高可用性、實時性和有序性。

 

這幾個概念我就不詳細展開,如果你是IM開發入門者,可以詳讀下面這幾篇:

4、架構設計

以我的這個項目來說,架構設設計要點主要是:

  • 1)微服務:拆分爲用戶微服務 &消息連接服務 &消息業務服務;
  • 2)存儲架構:兼容性能與資源開銷,選擇 reids&mysql;
  • 3)高可用:可以支撐起高併發場景,選擇 Spring 提供的 websocket;
  • 4)支持多端消息同步:app 端、web 端、微信公衆號、小程序消息;
  • 5)支持在線與離線消息場景。

業務架構圖主要是這樣:

技術模塊分層架構大概是這樣:

5、消息存儲技術要點

5.1 理解讀擴散和寫擴散

5.1.1)基本概念:

我們舉個例子說明什麼是讀擴散,什麼是寫擴散:

一個羣聊“相親相愛一家人”,成員:爸爸、媽媽、哥哥、姐姐和我(共 5 人)。

因爲你最近交到女朋友了,所以發了一條消息“我脫單了”到羣裏面,那麼自然希望爸爸媽媽哥哥姐姐四個親人都能收到了。

正常邏輯下,羣聊消息發送的流程應該是這樣:

  • 1)遍歷羣聊的成員併發送消息;
  • 2)查詢每個成員的在線狀態;
  • 3)成員不在線的存儲離線;
  • 4)成員在線的實時推送。

數據分發模型如下:

問題在於:如果第4步發生異常,羣友會丟失消息,那麼會導致有家人不知道“你脫單了”,造成催婚的嚴重後果。

所以優化的方案是:不管羣員是否在線,都要先存儲消息。

按照上面的思路,優化後的羣消息流程如下:

  • 1)遍歷羣聊的成員併發送消息;
  • 2)羣聊所有人都存一份;
  • 3)查詢每個成員的在線狀態;
  • 4)在線的實時推送。

以上優化後的方案,便是所謂的“寫擴散”了。

問題在於:每個人都存一份相同的“你脫單了”的消息,對磁盤和帶寬造成了很大的浪費(這就是寫擴散的最大弊端)。

所以優化的方案是:羣消息實體存儲一份,用戶只存消息 ID 索引。

於是再次優化後的發送羣消息流程如下:

  • 1)遍歷羣聊的成員併發送消息;
  • 2)先存一份消息實體;
  • 3)然後羣聊所有人都存一份消息實體的 ID 引用;
  • 4)查詢每個成員的在線狀態;
  • 5)在線的實時推送。

二次優化後的方案,便是所謂的“讀擴散”了。

5.1.2)小結一下:

  • 1)讀擴散:讀取操作很重,寫入操作很輕,資源消耗相對小一些;
  • 2)寫擴散:讀取操作很輕,寫入操作很重,資源消耗相對大一些。

從公開的技術資料來看,微信和釘釘的羣聊消息應該使用的是寫擴散方式,具體可以參看這兩篇:《微信後臺團隊:微信後臺異步消息隊列的優化升級實踐分享》、《阿里IM技術分享(四):閒魚億級IM消息系統的可靠投遞優化實踐》(注意“5.5 服務端存儲模型優化”這一節)。

5.2 “消息”所關聯的對象

5.2.1)消息實體模型:

常見的消息業務,可以抽象爲幾個實體模型概念:用戶/用戶關係/用戶設備/用戶連接狀態/消息/消息隊列。

在IM系統中的實體模型關係大致如下:

5.2.2)實體模型概念解釋:

用戶實體:

  • 1)用戶->用戶終端設備:每個用戶能夠多端登錄並收發消息;
  • 2)用戶->消息:考慮到讀擴散,每個用戶與消息的關係都是 1:n;
  • 3)用戶->消息隊列:考慮到讀擴散,每個用戶都會維護自己的一份“消息列表”(1:1),如果考慮到擴容,甚至可以開闢一份消息溢出列表接收超出“消息列表”容量的消息數據(此時是 1:n);
  • 4)用戶->用戶連接狀態:考慮到用戶能夠多端登錄,那麼 app/web 都會有對應的在線狀態信息(1:n);
  • 5)用戶->聯繫人關係:考慮到用戶最終以某種業務聯繫到一起,組成多份聯繫人關係,最終形成私聊或者羣聊(1:n);

聯繫人關係(主要由業務決定用戶與用戶之間的關係),比如說:

  • 1)某個家庭下有多少人,這個家庭羣聊就有多少人;
  • 2)在 ToB 場景,在釘釘企業版裏,我們往往有企業羣聊這個存在。

消息實體:

消息->消息隊列:考慮到讀擴散,消息最終歸屬於一個或多個消息隊列裏,因此羣聊場景它會分佈在不同的消息隊列裏。

消息隊列實體:

消息隊列:確切說是消息引用隊列,它裏面的索引元素最終指向具體的消息實體對象。

用戶連接狀態:

  • 1)對於 app 端:網絡原因導致斷線,或者用戶手動 kill 掉應用進程,都屬於離線;
  • 2)對於 web 端:網絡原因導致瀏覽器斷網,或者用戶手動關閉標籤頁,都屬於離線;
  • 3)對於公衆號:無法分別離線在線;
  • 4)對於小程序:無法分別離線在線。

用戶終端設備:

客戶端一般是 Android&IOS,web 端一般是瀏覽器,還有其他靈活的 WebView(公衆號/小程序)。

5.3 消息的存儲方案

對於消息存儲方案,本質上只有三種方案:要麼放在內存、要麼放在磁盤、要麼兩者結合存儲(據說大公司爲了優化性能,活躍的消息數據都是放在內存裏面的,畢竟有錢~)。

下面分別解析主要方案的優點與弊端:

  • 1)方案一:考慮性能,數據全部放到 redis 進行存儲;
  • 2)方案二:考慮資源,數據用 redis + mysql 進行存儲。

5.3.1)對於方案一:redis

前提:用戶 & 聯繫人關係,由於是業務數據,因此統一默認使用關係型數據庫存儲。

流程圖:

解釋如下:

  • 1)用戶發消息;
  • 2)redis 創建一條實體數據 &一個實體數據計時器;
  • 3)redis 在 B 用戶的用戶隊列 添加實體數據引用;
  • 4)B 用戶拉取消息(後續 5.2 會提及拉模式)。

實現方案:

  • 1)用戶隊列,zset(score 確保有序性);
  • 2)消息實體列表,hash(msg_id 確保唯一性);
  • 3)消息實體計數器,hash(支持羣聊消息的引用次數,倒計時到零時則刪除實體列表的對應消息,以節省資源)。

優點是:內存操作,響應性能好

弊端是:

  • 1)內存消耗巨大,eg:除非大廠,小公司的服務器的寶貴內存資源是耗不起業務的,隨着業務增長,不想拓展資源,就需要手動清理數據了;
  • 2)受 redis 容災性策略影響較大,如果 redis 宕機,直接導致數據丟失(可以使用 redis 的集羣部署/哨兵機制/主從複製等手段解決)。

5.3.2)方案二:redis+mysql

前提:用戶 & 聯繫人關係,由於是業務數據,因此統一默認使用關係型數據庫存儲。

流程圖:

解釋如下:

  • 1)用戶發消息;
  • 2)mysql 創建一條實體數據;
  • 3)redis 在 B 用戶的用戶隊列 添加實體數據引用;
  • 4)B 用戶拉取消息(下文會提及拉模式)。

實現方案:

  • 1)用戶隊列,zset(score 確保有序性);
  • 2)消息實體列表,轉移到 mysql(表主鍵 id 確保唯一性);
  • 3)消息實體計數器,hash(刪除這個概念,因爲磁盤可用總資源遠遠高於內存總資源,哪怕一直存放 mysql 數據庫,在業務量百萬級別時也不會有大問題,如果是巨大體量業務就需要考慮分表分庫處理檢索數據的性能了)。

優點是:

  • 1)抽離了數據量最大的消息實體,大大節省了內存資源;
  • 2)磁盤資源易於拓展 ,便宜實用。

弊端是:磁盤讀取操作,響應性能較差(從產品設計的角度出發,你維護的這套 IM 系統究竟是強 IM 還是弱 IM)。

6、消息的消費模式

6.1 拉模式

選用消息拉模式的原因:

  • 1)由於用戶數量太多(觀察者),服務器無法一一監控客戶端的狀態,因此消息模塊的數據交互使用拉模式,可以節約服務器資源;
  • 2)當用戶有未讀消息時,由客戶器主動發起請求的方式,可以及時刷新客戶端狀態。

6.2 ack 機制

技術原理:

  • 1)基於拉模式實現的數據拉取請求(第一次 fetch 接口)與數據拉取確認請求(第二次 fetch 接口)是成對出現的;
  • 2)客戶端二次調用 fetch 接口,需要將上次消息消費的錨點告訴服務端,服務器進而刪除已讀消息。

請求模型原理圖如下:

實現方案1:基於每一條消息編號 ACK:

  • 1)實現:客戶端在接收到消息之後,發送 ACK 消息編號給服務端,告知已經收到該消息。服務端在收到 ACK 消息編號的時候,標記該消息已經發送成功;
  • 2)弊端:這種方案,因爲客戶端逐條 ACK 消息編號,所以會導致客戶端和服務端交互次數過多。當然,客戶端可以異步批量 ACK 多條消息,從而減少次數。

實現方案2:基於滑動窗口 ACK:

1)客戶端在接收到消息編號之後,和本地的消息編號進行比對:

 - 如果比本地的小,說明該消息已經收到,忽略不處理;

 - 如果比本地的大,使用本地的消息編號,向服務端拉取大於本地的消息編號的消息列表,即增量消息列表。

 - 拉取完成後,更新消息列表中最大的消息編號爲新的本地的消息編號;

2)服務端在收到 ack 消息時,進行批量標記已讀或者刪除。

這種方式,在業務被稱爲推拉結合的方案,在分佈式消息隊列、配置中心、註冊中心實現實時的數據同步,經常被採用。

6.3 基於ack 機制的好處

第一次獲取消息完成之後,如果沒有 ack 機制,流程是:

  • 1)服務器刪除已讀消息數據;
  • 2)服務端把數據包響應給客戶端。

如果由於網絡延遲,導致客戶端長時間取不到數據,這時客戶端會斷開該次 HTTP 請求,進而忽略這次響應數據的處理,最終導致消息數據被刪除而後續無法恢復。

有了 ack 機制,哪怕第一次獲取消息失敗,客戶端還是可以繼續請求消息數據,因爲在 ack 確認之前,消息數據都不會刪除掉。

7、微服務設計

一般來說 IM 微服務,能拆分爲基礎的三個微服務:

  • 1)用戶服務;
  • 2)業務服務;
  • 3)連接管理服務。

參考架構圖:

他們分工合作如下。

用戶微服務(用戶設備的登錄 & 登出):

  • 1)設備號存庫;
  • 2)連接狀態更新;
  • 3)其他登錄端用戶踢出等。

連接管理微服務:

  • 1)狀態保存:保存用戶設備長連接對象;
  • 2)剔除無效連接:輪訓已有長連接對象狀態,超時刪除對象;
  • 3)接受客戶端的心跳包:刷新長連接對象的狀態。

消息業務微服務:

  • 1)消息存儲:進行私聊/羣聊的消息存儲策略(請參看“消息存儲模型”一節);
  • 2)消息消費:進行消息獲取響應與 ack 確認刪除(請參看“消息消費模式”一節);
  • 3)消息路由:用戶在線時,路由消息通知包到“消息連接管理微服務”,以通知用戶客戶端來取消息。

最後提一下消息的路由:

微服務之間也有通信手段,比如業務服務到連接管理服務,兩者之間可以通過 RPC 實現實時消息的路由通知。

8、離線消息推送

離線推送方案上,大家一般都會考慮採用兩種方案:

  • 1)企業自研後臺離線 PUSH 系統;
  • 2)企業自行對接第三方手機廠商 PUSH 系統。

8.1 企業自研後臺離線 PUSH 系統

技術原理:

在應用級別,客戶端與後臺離線 PUSH 系統保持長連接,當用戶狀態被檢測爲離線時,通過這個長連接告知客戶端“有新消息”,進而喚醒手機彈窗標題。

弊端就是:

隨着安卓和蘋果系統的限制越來越嚴格,一般客戶端的活動週期被限制的死死的,一旦客戶端進程被挪到後臺就立馬被 kill 掉了,導致客戶端保活特別難做好(這也是很多中小企業頭疼的地方,畢竟只有微信或者 QQ 這種體量的一級市場 APP,手機系統願意給他們留後門來做保活)。具體可以讀一下《Android P正式版即將到來:後臺應用保活、消息推送的真正噩夢》這篇。

8.2 企業自行對接第三方廠商 PUSH 系統

技術原理:

在系統級別,每個硬件系統都會與對應的手機廠商保持長連接,當用戶狀態被檢測爲離線時,後臺將推送報文通過 HTTP 請求,告知第三方手機廠商服務器,進而通過系統喚醒 app 的彈窗標題。

弊端就是:

  • 1)作爲應用端,消息是否確切送達給用戶側,是未知的;推送的穩定性也取決於第三方手機廠商的服務穩定性;
  • 2)額外進行 sdk 的對接工作,增加了工作量;
  • 3)第三方廠商隨時可能升級 sdk 版本,導致沒有升級 sdk 的服務器出現推送失敗的情況,給 Sass 系統部署帶來困難;
  • 4)推送證書配置也要考慮到維護成本。

總之,IM裏離線消息推送是個很頭疼的問題(當然這裏主要說是Andriod了,iOS裏蘋果官方的APNs就舒服多了),有興趣好一讀一下下面這些文章:

  1. 全面盤點當前Android後臺保活方案的真實運行效果(截止2019年前)
  2. 融雲技術分享:融雲安卓端IM產品的網絡鏈路保活技術實踐
  3. 2020年了,Android後臺保活還有戲嗎?看我如何優雅的實現!
  4. 史上最強Android保活思路:深入剖析騰訊TIM的進程永生技術
  5. Android進程永生技術終極揭密:進程被殺底層原理、APP應對被殺技巧
  6. Android保活從入門到放棄:乖乖引導用戶加白名單吧(附7大機型加白示例)
  7. 阿里IM技術分享(六):閒魚億級IM消息系統的離線推送到達率優化

9、其它需要考慮的技術要點

9.1 安全性

關於IM安全性,我個人的體會是這樣:

  • 1)業務數據傳輸安全性使用 https 訪問;
  • 2)實時消息使用SSL/TLS對長連接進行加密;
  • 3)使用私有協議,不容易解析;
  • 4)內容安全性端到端加密,中間任何環節都不能解密(即發送和接收端交換互相的密鑰來解密,服務器端解密不了);
  • 5)服務器端不存儲消息。

以上要點中:IM中的長連接安全性是比較重要且不容易處理的,因爲它需要在安全性和性能上作平衡和取捨(不能光顧着安全而損失IM長連接的高吞吐性能),這方面可以參考微信團隊分享的這篇《微信新一代通信安全解決方案:基於TLS1.3的MMTLS詳解》。

另外:更高安全性的場景可以考慮組合加密方案,詳情可以參考《探討組合加密算法在IM中的應用》。

9.2 一致性

IM消息一致性難題,主要是保證消息不亂序的問題。這個話題,初學者可以讀讀這篇《零基礎IM開發入門(四):什麼是IM系統的消息時序一致性?》,我就不再贅述了。

解決一致性問題的切入點有很多,最常見的是使用有序的消息唯一id,關於有序且唯一的ID生成問題,微信團隊的思路就很好,可以借鑑一下《微信技術分享:微信的海量IM聊天消息序列號生成實踐(算法原理篇)》。

另外,以下幾篇關於消息有序性問題的總結也非常好,可以進行參考:

  1. 如何保證IM實時消息的“時序性”與“一致性”?
  2. 一個低成本確保IM消息時序的方法探討
  3. 一套億級用戶的IM架構技術乾貨(下篇):可靠性、有序性、弱網優化等

9.3 可靠性

IM裏所謂的可靠性,說直白一點就是保證消息不丟失,這看似理所當然、稀鬆平常的技術點,在IM系統中又是另一個很大的話題,鑑於本人水平有限,就不班門弄斧,IM初學者可以能過《零基礎IM開發入門(三):什麼是IM系統的可靠性?》這篇來理解可靠性這個概念。

然後再讀讀《IM消息送達保證機制實現(一):保證在線實時消息的可靠投遞》、《IM消息送達保證機制實現(二):保證離線消息的可靠投遞》這兩篇,基本上就能對IM可靠性這個技術要點有了比較深刻的認識了。

下面這幾篇實戰性的總結,適合有一定IM經驗的同行們學習,可以借鑑學習一下:

  1. 融雲技術分享:全面揭祕億級IM消息的可靠投遞機制
  2. IM開發乾貨分享:如何優雅的實現大量離線消息的可靠投遞
  3. 從客戶端的角度來談談移動端IM的消息可靠性和送達機制
  4. 阿里IM技術分享(四):閒魚億級IM消息系統的可靠投遞優化實踐

9.4 實時性

IM實時性這個技術點,就回歸到了“即時通訊”這個技術的立身之本了,可以說,沒有實時性,也就不存在“即時通訊”這個技術範疇了,可以見它的重要性。關於實時性這個概念,初學者可以通過《零基礎IM開發入門(二):什麼是IM系統的實時性?》這篇去學習一下,我就不囉嗦了,人家比我說的好。

筆者公司的項目裏實時通信用方案都是採用 WebSocket(如果你不瞭解WebSocket,可以讀一下《WebSocket從入門到精通,半小時就夠!》,以及《搞懂現代Web端即時通訊技術一文就夠:WebSocket、socket.io、SSE),但是某些低版本的瀏覽器可能不支持 WebSocket,所以實際開發時,要兼容前端所能提供的能力進行方案設計。

以下兩篇關於實時性的同行實踐性總結也不錯:

  1. 移動端IM中大規模羣消息的推送如何保證效率、實時性?
  2. 阿里IM技術分享(五):閒魚億級IM消息系統的及時性優化實踐

10、我在項目實踐中的體會

作爲研發者,有兩年多的時間都在維護迭代公司的 IM 消息系統,以下是我自已的小小體會。

我體會到的重點難點有以下幾方面:

  • 1)業務閉環:消息是如何寫入存儲、消息是如何消費掉、在線消息是如何實現、離線消息是如何實現、羣聊/私聊有何不一樣、多端消息如何實現;
  • 2)解 Bug 填坑:在線消息收不到,第三方推送證書如何配置;
  • 3)代碼優化:單體架構拆分微服務;
  • 4)存儲優化:1.0 版本的 redis 存儲到 2.0 版本的 redis+mysql;
  • 5)性能優化:未讀提醒等接口性能優化。

項目還存在可優化的地方:

  • 1)高可用方案之一:是部署多部連接管理服務器,以支撐更多的用戶連接;
  • 2)高可用方案之二:是對單部連接管理服務,使用 Netty 進行框架層優化,讓一個服務器支撐更多的用戶連接;
  • 3)消息量劇增時:可以考慮對消息存儲作進一步優化;
  • 4)消息冷熱部署:不同的地區會存在業務量差異,比如在某些經濟發達的省份,IM 系統面臨的壓力會比較大,一些欠發達省份,服務壓力會低一點,所以這塊可以考慮數據的冷熱部署。

11、寫在最後

兩年前從架構師手上接過來的 IM 消息系統模塊,讓我逐步培養了架構思維,見賢思齊,感謝恩師。

IM技術是個經久不衰的領域,但同時可直接使用的技術資產也非常匱乏,必竟傳統的IM巨頭們的產品通常都是私有化協議、私有化方案,很難有業界共同的方案可以直接使用(包括資料或開源代碼),正是這種不通用、不準,間接導致IM技術門檻的提高。所以通常公司要搞IM的話,如果沒有技術積累,就只能從零開始造輪子。

爲了改變這種局面,也希望搞IM開發的同學不要悶頭造車,應該多多借鑑同行的思路,同時也能積極分享自已的經驗,讓IM開發不再痛苦。

以上拋磚引玉,歡迎留言討論,一起進步。

12、參考資料

[1] 新手入門一篇就夠:從零開發移動端IM

[2] 爲何基於TCP協議的移動端IM仍然需要心跳保活機制?

[3] Android P正式版即將到來:後臺應用保活、消息推送的真正噩夢

[4] WebSocket從入門到精通,半小時就夠!

[5] 搞懂現代Web端即時通訊技術一文就夠:WebSocket、socket.io、SSE

[6] 一套海量在線用戶的移動端IM架構設計實踐分享(含詳細圖文)

[7] 一套原創分佈式即時通訊(IM)系統理論架構方案

[8] 一套高可用、易伸縮、高併發的IM羣聊、單聊架構方案設計實踐

[9] 微信技術分享:微信的海量IM聊天消息序列號生成實踐(算法原理篇)

[10] 阿里IM技術分享(四):閒魚億級IM消息系統的可靠投遞優化實踐

[11] 阿里IM技術分享(五):閒魚億級IM消息系統的及時性優化實踐

[12] 一套億級用戶的IM架構技術乾貨(下篇):可靠性、有序性、弱網優化等

[13] 從新手到專家:如何設計一套億級消息量的分佈式IM系統

[14] 企業微信的IM架構設計揭祕:消息模型、萬人羣、已讀回執、消息撤回等

[15] 融雲技術分享:全面揭祕億級IM消息的可靠投遞機制

[16] 即時通訊安全篇(六):非對稱加密技術的原理與應用實踐

[17] 通俗易懂:一篇掌握即時通訊的消息傳輸安全原理

[18] 微信新一代通信安全解決方案:基於TLS1.3的MMTLS詳解

[19] 零基礎IM開發入門(二):什麼是IM系統的實時性?

[20] 零基礎IM開發入門(三):什麼是IM系統的可靠性?

[21] 零基礎IM開發入門(四):什麼是IM系統的消息時序一致性?

本文已同步發佈於“即時通訊技術圈”公衆號。

同步發佈鏈接是:http://www.52im.net/thread-3752-1-1.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章