本文由公眾號“後臺技術匯”分享,原題“基於實踐,設計一個百萬級別的高可用 & 高可靠的 IM 訊息系統”,原文連結在文末。由於原文存在較多錯誤和不準確內容,有大量修訂和改動。
1、引言
大家好,我是公眾號“後臺技術匯”的博主“一枚少年”。
本人從事後臺開發工作 3 年有餘了,其中讓我感觸最深刻的一個專案,就是在兩年前從架構師手上接過來的 IM 訊息系統。
本文內容將從開發者的視角出發(主要是我自已的開發體會),圍繞專案背景、業務需求、技術原理、開發方案等主題,一步一步的與大家一起剖析:設計一套百萬訊息量的小規模IM系統架構設計上需要注意的技術要點。
學習交流:
- 即時通訊/推送技術開發交流5群:215477170 [推薦]
- 移動端IM開發入門文章:《新手入門一篇就夠:從零開發移動端IM》
- 開源IM框架原始碼:https://github.com/JackJiang2...
(本文同步釋出於:http://www.52im.net/thread-37...)
2、專案背景
我們仔細觀察就能發現,生活中的任何型別網際網路服務都有 IM 系統的存在。
比如:
1)基礎性服務類-騰訊新聞(評論訊息);
2)商務應用類-釘釘(審批工作流通知);
3)交流娛樂類-QQ/微信(私聊群聊 &討論組 &朋友圈);
4)網際網路自媒體-抖音快手(點贊打賞通知)。
在這些林林總總的網際網路生態產品裡,即時訊息系統作為底層能力,在確保業務正常與使用者體驗優化上,始終扮演了至關重要的角色。
所以,現如今的網際網路產品中,即時通訊技術已經不僅限於傳統IM聊天工具本身,它早已通過有形或無形的方式嵌入到了各種形式的網際網路應用當中。IM技術(或者說即時通訊技術)對於很多開發者來說,確實是必不好可少的領域知識,不可或缺。
3、系統能力
典型的IM系統通常需要滿足四點能力:高可靠性、高可用性、實時性和有序性。
這幾個概念我就不詳細展開,如果你是IM開發入門者,可以詳讀下面這幾篇:
《零基礎IM開發入門(一):什麼是IM系統?》
《零基礎IM開發入門(二):什麼是IM系統的實時性?》
《零基礎IM開發入門(三):什麼是IM系統的可靠性?》
《零基礎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就舒服多了),有興趣好一讀一下下面這些文章:
《全面盤點當前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-37...