本文由公眾號“后臺技術匯”分享,原題“基于實踐,設計一個百萬級別的高可用 & 高可靠的 IM 消息系統”,原文鏈接在文末。由于原文存在較多錯誤和不準確內容,有大量修訂和改動。
1、引言
大家好,我是公眾號“后臺技術匯”的博主“一枚少年”。
本人從事后臺開發工作 3 年有余了,其中讓我感觸最深刻的一個項目,就是在兩年前從架構師手上接過來的 IM 消息系統。
本文內容將從開發者的視角出發(主要是我自已的開發體會),圍繞項目背景、業務需求、技術原理、開發方案等主題,一步一步的與大家一起剖析:設計一套百萬消息量的小規模IM系統架構設計上需要注意的技術要點。
(本文同步發布于:http://www.52im.net/thread-3752-1-1.html)
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)消息消費:進行消息獲取響應與 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就舒服多了),有興趣好一讀一下下面這些文章:
- 《全面盤點當前Android后臺保活方案的真實運行效果(截止2019年前)》
- 《融云技術分享:融云安卓端IM產品的網絡鏈路保活技術實踐》
- 《2020年了,Android后臺保活還有戲嗎?看我如何優雅的實現!》
- 《史上最強Android保活思路:深入剖析騰訊TIM的進程永生技術》
- 《Android進程永生技術終極揭密:進程被殺底層原理、APP應對被殺技巧》
- 《Android保活從入門到放棄:乖乖引導用戶加白名單吧(附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聊天消息序列號生成實踐(算法原理篇)》。
另外,以下幾篇關于消息有序性問題的總結也非常好,可以進行參考:
- 《如何保證IM實時消息的“時序性”與“一致性”?》
- 《一個低成本確保IM消息時序的方法探討》
- 《一套億級用戶的IM架構技術干貨(下篇):可靠性、有序性、弱網優化等》
9.3 可靠性
IM里所謂的可靠性,說直白一點就是保證消息不丟失,這看似理所當然、稀松平常的技術點,在IM系統中又是另一個很大的話題,鑒于本人水平有限,就不班門弄斧,IM初學者可以能過《零基礎IM開發入門(三):什么是IM系統的可靠性?》這篇來理解可靠性這個概念。
然后再讀讀《IM消息送達保證機制實現(一):保證在線實時消息的可靠投遞》、《IM消息送達保證機制實現(二):保證離線消息的可靠投遞》這兩篇,基本上就能對IM可靠性這個技術要點有了比較深刻的認識了。
下面這幾篇實戰性的總結,適合有一定IM經驗的同行們學習,可以借鑒學習一下:
- 《融云技術分享:全面揭秘億級IM消息的可靠投遞機制》
- 《IM開發干貨分享:如何優雅的實現大量離線消息的可靠投遞》
- 《從客戶端的角度來談談移動端IM的消息可靠性和送達機制》
- 《阿里IM技術分享(四):閑魚億級IM消息系統的可靠投遞優化實踐》
9.4 實時性
IM實時性這個技術點,就回歸到了“即時通訊”這個技術的立身之本了,可以說,沒有實時性,也就不存在“即時通訊”這個技術范疇了,可以見它的重要性。關于實時性這個概念,初學者可以通過《零基礎IM開發入門(二):什么是IM系統的實時性?》這篇去學習一下,我就不啰嗦了,人家比我說的好。
筆者公司的項目里實時通信用方案都是采用 WebSocket(如果你不了解WebSocket,可以讀一下《WebSocket從入門到精通,半小時就夠!》,以及《搞懂現代Web端即時通訊技術一文就夠:WebSocket、socket.io、SSE》),但是某些低版本的瀏覽器可能不支持 WebSocket,所以實際開發時,要兼容前端所能提供的能力進行方案設計。
以下兩篇關于實時性的同行實踐性總結也不錯:
- 《移動端IM中大規模群消息的推送如何保證效率、實時性?》
- 《阿里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