Jack Jiang

          我的最新工程MobileIMSDK:http://git.oschina.net/jackjiang/MobileIMSDK
          posts - 502, comments - 13, trackbacks - 0, articles - 1

          本文由will分享,個人博客zhangyaoo.github.io,原題“基于Netty的IM系統設計與實現”,有修訂和重新排版。

          1、引言

          本文將要分享的是如何從零實現一套基于Netty框架的分布式高可用IM系統,它將支持長連接網關管理、單聊、群聊、聊天記錄查詢、離線消息存儲、消息推送、心跳、分布式唯一ID、紅包、消息同步等功能,并且還支持集群部署。

          本文中針對這套架構和系統設計,同時還會提供完整的源碼,比較適合有一定Java開發能力和Netty知識的IM初學者。

          * 友情提示:如果你對IM即時通訊的基礎技術理論了解的太少,建議可以先讀:《新手入門一篇就夠:從零開發移動端IM》。

           

          技術交流:

          (本文已同步發布于:http://www.52im.net/thread-4257-1-1.html

          2、配套源碼

          本文配套源碼的開源托管地址是:

          如果你訪問Github太慢,可直接從以下附件打包下載:

           fastim-master(52im.net).zip (1.12 MB , 下載次數: 5 , 售價: 1 金幣)

          完整源碼的目錄結構,如下圖:

          3、知識準備

          關于 Netty 是什么,這里簡單介紹下:

          Netty 是一個 Java 開源框架。Netty 提供異步的、事件驅動的網絡應用程序框架和工具,用以快速開發高性能、高可靠性的網絡服務器和客戶端程序。

          也就是說,Netty 是一個基于 NIO 的客戶、服務器端編程框架,使用Netty 可以確保你快速和簡單的開發出一個網絡應用,例如實現了某種協議的客戶,服務端應用。

          Netty 相當簡化和流線化了網絡應用的編程開發過程,例如,TCP 和 UDP 的 Socket 服務開發。

          有關Netty的入門文章:

          1)新手入門:目前為止最透徹的的Netty高性能原理和框架架構解析

          2)寫給初學者:Java高性能NIO框架Netty的學習方法和進階策略

          3)史上最通俗Netty框架入門長文:基本介紹、環境搭建、動手實戰

          如果你連Java NIO都不知道,下面的文章建議優先讀:

          Netty源碼和API 在線查閱地址:

          4、整體架構設計概覽

          本次的IM系統設計主要基于可擴展性高可用原則,把網關層、邏輯層、數據層進行了分離,并且還要支持分布式部署。

          以下是整體系統的架構設計概覽圖:

          下面將針對整體架構來逐一分享設計的主要思路等。

          5、整體架構設計之客戶端設計

          5.1客戶端設計

          客戶端的設計主要從以下幾點出發:

          • 1)client每個設備會在本地存每一個會話,保留有最新一條消息的順序 ID;
          • 2)為了避免client宕機,也就是退出應用,保存在內存的消息ID丟失,會存到本地的文件中;
          • 3)client需要在本地維護一個等待ack隊列,并配合timer超時機制,來記錄哪些消息沒有收到ack:N,以定時重發;
          • 4)客戶端本地生成一個遞增序列號發送給服務器,用作保證發送順序性。該序列號還用作ack隊列收消息時候的移除。

          5.2客戶端序列號設計

          1)方案一:

          設計思路:

          • 1)數據傳輸中的大小盡量小用int,不用bigint,節省傳輸大小;
          • 2)只保證遞增即可,在用戶重新登錄或者重連后可以進行日期重置,只保證單次;
          • 3)客戶端發號器不需要像類似服務器端發號器那樣集群部署,不需要考慮集群同步問題。

          注:上述生成器可以用18年[(2^29-1)/3600/24/365]左右,一秒內最多產生4個消息。

          優點:可以在斷線重連和重裝APP的情況下,18年之內是有序的。

          缺點:每秒只能發4個消息,限制太大,對于群發場景不合適。

          改進:使用long進行傳輸,年限擴展很久并且有序。

          2)方案二:

          設計思路:

          • 1)每次重新建立鏈接后進行重置,將sequence_id(int表示)從0開始進行嚴格遞增;
          • 2)客戶端發送消息會帶上唯一的遞增sequence_id,同一條消息重復投遞的sequence_id是一樣的;
          • 3)后端存儲每個用戶的sequence_id,當sequence_id歸0,用戶的epoch年代加1存儲入庫,單聊場景下轉發給接收者時候,接收者按照sequence_id和epoch來進行排序。

          優點:可以在斷線重連和重裝APP的情況下,接收者可以按照發送者發送時序來顯示,并且對發送消息的速率沒限制。

          6、整體架構設計之LSB設計

          6.1思路

          IM接入層的高可用、負載均衡、擴展性全部在這里面做。客戶端通過LSB,來獲取gate IP地址,通過IP直連。

          這樣做的目的是:

          • 1)靈活的負載均衡策略 可根據最少連接數來分配IP;
          • 2)做灰度策略來分配IP;
          • 3)AppId業務隔離策略 不同業務連接不同的gate,防止相互影響;
          • 4)單聊和群聊的im接入層通道分開。

          6.2優化

          上述設計存在一個問題:就是當某個實例重啟后,該實例的連接斷開后,客戶端會發起重連,重連就大概率轉移其他實例上,導致最近啟動的實例連接數較少,最早啟動的實例連接數較多。

          解決方法:

          • 1)客戶端會發起重連,跟服務器申請重連的新的服務器IP,系統提供合適的算法來平攤gate層的壓力,防止雪崩效應;
          • 2)gate層定時上報本機的元數據信息以及連接數信息,提供給LSB中心,LSB根據最少連接數負載均衡實現,來計算一個節點供連接。

          7、整體架構設計之GATE層網關設計

          GATE層網關設計主要遵從以下幾點:

          • 1)任何一個gate網關斷掉,用戶端檢測到以后重新連接LSB服務獲取另一個gate網關IP,拿到IP重新進行長連接通信(對整體服務可靠性基本沒有影響);
          • 2)gate可以無狀態的橫向部署,來擴展接入層的接入能力;
          • 3)根據協議分類將入口請求打到不同的網關上去,HTTP網關接收HTTP請求,TCP網關接收tcp長連接請求;
          • 4)長連接網關,提供各種監控功能,比如網關執行線程數、隊列任務數、ByteBuf使用堆內存數、堆外內存數、消息上行和下行的數量以及時間。

          8、整體架構設計之LOGIC和路由SDK設計

          logic按照分布式微服務的拆分思想進行拆分,拆分為多個模塊,集群部署。

          主要包括:

          • 1)消息服務;
          • 2)紅包服務;
          • 3)其他服務。

          消息logic服務集成路由客戶端的SDK,SDK職責主要是:

          • 1)負責和網關底層通信交互;
          • 2)負責網關服務尋址;
          • 3)負責存儲uid和gate層機器ID關系(有狀態:多級緩存避免和中間件多次交互。無狀態:在業務初期可以不用存);
          • 4)配合網關負責路由信息一致性保證。

          針對上述第4)點:

          • 1)如果路由狀態和channel通道不一致,比如有路由狀態,沒有channel通道(已關閉)那么,就會走離線消息流出,并且清除路由信息;
          • 2)動態重啟gate,會及時清理路由信息。

          SDK和網關底層通信設計:

          如上圖所示:網關層到服務層,只需要單向傳輸發請求,網關層不需要關心調用的結果。而客戶端想要的ack或者notify請求是由SDK發送數據到網關層,SDK也不需要關心調用的結果,最后網關層只轉發數據,不做額外的邏輯處理。

          SDK和所有的網關進行長連接,當發送信息給客戶端時,根據路由尋址信息,即可通過長連接推送信息。

          9、通信協議設計

          9.1目標

          通信協議設計的主要目標是:

          • 1)高性能:協議設計緊湊,保證數據包小,并且序列化性能好;
          • 2)可擴展:針對后續業務發展,可以自由的自定義協議,無需較大改動協議結構。

          9.2設計

          IM協議采用二進制定長包頭和變長包體來實現客戶端和服務端的通信,并且采用谷歌protobuf序列化協議。

          設計如下:

          各個字段解釋如下:

          • 1)headData:頭部標識,協議頭標識,用作粘包半包處理。4個字節;
          • 2)version:客戶端版本。4個字節;
          • 3)cmd:業務命令,比如心跳、推送、單聊、群聊。1個字節;
          • 4)msgType:消息通知類型 request response notify。1個字節;
          • 5)logId:調試性日志,追溯一個請求的全路徑。4個字節;
          • 6)sequenceId:序列號,可以用作異步處理。4個字節;
          • 7)dataLength:數據體的長度。4個字節;
          • 8)data:數據。

          PS:如果你對Protobuf不了解,建議詳讀以下系列文章:

          1.《強列建議將Protobuf作為你的即時通訊應用數據傳輸格式

          2.《IM通訊協議專題學習(一):Protobuf從入門到精通,一篇就夠!

          3.《IM通訊協議專題學習(二):快速理解Protobuf的背景、原理、使用、優缺點

          4.《IM通訊協議專題學習(三):由淺入深,從根上理解Protobuf的編解碼原理

          5.《IM通訊協議專題學習(四):從Base64到Protobuf,詳解Protobuf的數據編碼原理

          6.《IM通訊協議專題學習(五):Protobuf到底比JSON快幾倍?全方位實測!

          7.《IM通訊協議專題學習(六):手把手教你如何在Android上從零使用Protobuf

          8.《IM通訊協議專題學習(七):手把手教你如何在NodeJS中從零使用Protobuf

          9.《IM通訊協議專題學習(八):金蝶隨手記團隊的Protobuf應用實踐(原理篇)

          10.《IM通訊協議專題學習(九):手把手教你如何在iOS上從零使用Protobuf

          9.3實踐

          針對數據data,網關gate層不做反序列化,反序列化步驟在service做,避免重復序列化和反序列化導致的性能損失。

          網關層不做業務邏輯處理,只做消息轉發和推送,減少網關層的復雜度。

          10、安全設計

          為防止消息傳輸過程中不被截獲、篡改、偽造,采用TLS傳輸層加密協議(可參考《微信新一代通信安全解決方案:基于TLS1.3的MMTLS詳解》)。

          私有化協議天然具備一定的防竊取和防篡改的能力,相對于使用JSON、XML、HTML等明文傳輸系統,被第三方截獲后在內容破解上相對成本更高,因此安全性上會更好一些。

          消息存儲安全性:將針對賬號密碼的存儲安全可以通過“高強度單向散列算法”和“加鹽”機制來提升加密密碼可逆性;IM消息采用“端到端加密”方式來提供更加安全的消息傳輸保護。

          安全層協議設計:基于動態密鑰,借鑒類似SSL,不需要用證書來管理(可參考《探討組合加密算法在IM中的應用》)。

          11、消息投遞設計

          11.1概述

          一個正常的消息流轉需要如下圖所示的流程:

          如上圖所示:

          • 1)客戶端A發送請求包R;
          • 2)server將消息存儲到DB;
          • 3)存儲成功后返回確認ack;
          • 4)server push消息給客戶端B;
          • 5)客戶端B收到消息后返回確認ack;
          • 6)server收到ack后更新消息的狀態或者刪除消息。

          需要考慮的是:一個健壯的IM系統需要考慮各種異常情況,比如丟消息,重復消息,消息時序問題。

          11.2消息可靠性如何保證(不丟消息)

          我的設計和實現思路是這樣的:

          • 1)應用層ACK;
          • 2)客戶端需要超時與重傳;
          • 3)服務端需要超時與重傳,具體做法就是增加ack隊列和定時器Timer;
          • 4)業務側兜底保證,客戶端拉消息通過一個本地的舊的序列號來拉取服務器的最新消息;
          • 5)為了保證消息必達,在線客戶端還增加一個定時器,定時向服務端拉取消息,避免服務端向客戶端發送拉取通知的包丟失導致客戶端未及時拉取數據。

          相關資料可參考:

          1.《從客戶端的角度來談談移動端IM的消息可靠性和送達機制

          2.《IM消息送達保證機制實現(一):保證在線實時消息的可靠投遞

          3.《IM消息送達保證機制實現(二):保證離線消息的可靠投遞

          4.《IM開發干貨分享:如何優雅的實現大量離線消息的可靠投遞

          5.《理解IM消息“可靠性”和“一致性”問題,以及解決方案探討

          6.《融云技術分享:全面揭秘億級IM消息的可靠投遞機制

          11.3消息重復性如何保證(不重復)

          超時與重傳機制將導致接收的client收到重復的消息,具體做法就是一份消息使用同一個消息ID進行去重處理。

          相關資料可參考:

          1.《IM群聊消息如此復雜,如何保證不丟不重?

          2.《完全自已開發的IM該如何設計“失敗重試”機制?

          11.4消息順序性如何保證(不亂序)

          消息亂序影響的因素:

          • 1)時鐘不一致,分布式環境下每個機器的時間可能是不一致的;
          • 2)多發送方和多接收方,這種情況下,無法保先發的消息被先收到;
          • 3)網絡傳輸和多線程,網絡傳輸不穩定的話可能導致包在數據傳輸過程中有的慢有的快。多線程也可能是會導致時序不一致影響的因素。

          以上:如果保持絕對的實現,那么只能是一個發送方,一個接收方,一個線程阻塞式通訊來實現。那么性能會降低。

          1)如何保證時序:

          單聊:通過發送方的絕對時序seq,來作為接收方的展現時序seq。

          實現方式:可以通過時間戳或者本地序列號方式來實現

          缺點:本地時間戳不準確或者本地序列號在意外情況下可能會清0,都會導致發送方的絕對時序不準確

          群聊:因為發送方多點發送時序不一致,所以通過服務器的單點做序列化,也就是通過ID遞增發號器服務來生成seq,接收方通過seq來進行展現時序。

          實現方式:通過服務端統一生成唯一趨勢遞增消息ID來實現或者通過redis的遞增incr來實現。

          缺點:redis的遞增incr來實現,redis取號都是從主取的,會有性能瓶頸。ID遞增發號器服務是集群部署,可能不同發號服務上的集群時間戳不同,可能會導致后到的消息seq還小。

          群聊時序的優化:按照上面的群聊處理,業務上按照道理只需要保證單個群的時序,不需要保證所有群的絕對時序,所以解決思路就是同一個群的消息落到同一個發號service上面,消息seq通過service本地生成即可。

          2)客戶端如何保證順序:

          為什么要保證順序?因為消息即使按照順序到達服務器端,也會可能出現:不同消息到達接收端后,可能會出現“先產生的消息后到”“后產生的消息先到”等問題。所以客戶端需要進行兜底的流量整形機制

          如何保證順序?可以在接收方收到消息后進行判定,如果當前消息序號大于前一條消息的序號就將當前消息追加在會話里。否則繼續往前查找倒數第二條、第三條等消息,一直查找到恰好小于當前推送消息的那條消息,然后插入在其后展示。

          相關資料可參考:

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

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

          如何保證IM實時消息的“時序性”與“一致性”?

          一個低成本確保IM消息時序的方法探討

          12、消息通知設計

          12.1概述

          整體消息推送和拉取的時序圖如下:

          12.2消息拉取方式的選擇

          本系統是通過推拉結合來進行服務器端消息的推送和客戶端的拉取。我們知道單pull和單push有以下缺點。

          對于單pull:

          • 1)pull要考慮到消息的實時性,不知道消息何時送達;
          • 2)pull要考慮到哪些好友和群收到了消息,要循環每個群和好友拿到消息列表,讀擴散。

          對于單push:

          • 1)push實時性高,只要將消息推送給接收者就ok,但是會集中消耗服務器資源;
          • 2)并且再群聊非常多、聊天頻率非常高的情況下,會增加客戶端和服務端的網絡交互次數。

          對于推拉結合:

          • 1)推拉結合的方式能夠分攤服務端的壓力,能保證時效性,又能保證性能;
          • 2)具體做法就是有新消息時候,推送哪個好友或者哪個群有新消息,以及新消息的數量或者最新消息ID,客戶端按需根據自身數據進行拉取。

          12.3推拉隔離設計

          為什么做隔離?

          如果客戶端一邊正在拉取數據,一邊有新的增量消息push過來。

          如何做隔離?

          本地設置一個全局的狀態,當客戶端拉取完離線消息后設置狀態為1(表示離線消息拉取完畢)。當客戶端收到拉取實時消息,會啟用一個輪詢監聽這個狀態,狀態為1后,再去向服務器拉取消息。

          如果是push消息過來(不是主動拉取),那么會先將消息存儲到本地的消息隊列中,等待客戶端上一次拉取數據完畢,然后將數據進行合并即可。

          相關資料可參考:

          阿里IM技術分享(六):閑魚億級IM消息系統的離線推送到達率優化

          阿里IM技術分享(七):閑魚IM的在線、離線聊天數據同步機制優化實踐

          13、消息ID生成設計

          以下是我設計的場景:

          • 1)單機高峰并發量小于1W,預計未來5年單機高峰并發量小于10W;
          • 2)有2個機房,預計未來5年機房數量小于4個 每個機房機器數小于150臺;
          • 3)目前只有單聊和群聊兩個業務線,后續可以擴展為系統消息、聊天室、客服等業務線,最多8個業務線。

          根據以上業務情況,來設計分布式ID:

          優點:

          • 1)不同機房不同機器不同業務線內生成的ID互不相同;
          • 2)每個機器的每毫秒內生成的ID不同;
          • 3)預留兩位留作擴展位。

          缺點:當并發度不高的時候,時間跨毫秒的消息,區分不出來消息的先后順序。因為時間跨毫秒的消息生成的ID后面的最后一位都是0,后續如果按照消息ID維度進行分庫分表,會導致數據傾斜。

          兩種解決方案:

          • 1)方案一:去掉snowflake最后8位,然后對剩余的位進行取模;
          • 2)方案二:不同毫秒的計數,每次不是歸0,而是歸為隨機數,相比方案一,比較簡單實用。

          相關資料可參考:

          微信的海量IM聊天消息序列號生成實踐(算法原理篇)

          微信的海量IM聊天消息序列號生成實踐(容災方案篇)

          解密融云IM產品的聊天消息ID生成策略

          深度解密美團的分布式ID生成算法

          開源分布式ID生成器UidGenerator的技術實現

          深度解密滴滴的高性能ID生成器(Tinyid)

          14、消息未讀數設計

          14.1基本

          實現思路大致如下:

          • 1)每發一個消息,消息接收者的會話未讀數+1,并且接收者所有未讀數+1;
          • 2)消息接收者返回消息接收確認ack后,消息未讀數會-1;
          • 3)消息接收者的未讀數+1,服務端就會推算有多少條未讀數的通知。

          分布式鎖保證總未讀數和會話未讀數一致:

          • 1)原因:當總未讀數增加,這個時候客戶端來了請求將未知數置0,然后再增加會話未讀數,那么會導致不一致;
          • 2)保證:為了保證總未讀數和會話未讀數原子性,需要用分布式鎖來保證。

          14.2群聊消息未讀數的難點和優化思路

          對于群聊來說,消息未讀數的技術難點主要是:一個群聊每秒幾百的并發聊天,比如消息未讀數,相當于每秒W級別的寫入redis,即便redis做了集群數據分片+主從,但是寫入還是單節點,會有寫入瓶頸。

          我的優化思路是:按群ID分組或者用戶ID分組,批量寫入,寫入的兩種方式:定時flush和滿多少消息進行flush。

          15、網關設計

          15.1概述

          本套IM系統在設計時,將網關分為了接入層網關和應用層網關兩種。

          • 接入層網關和應用層網關區別主要是:
          • 1)接入層網關需要有接收通知包或者下行接收數據的端口,并且需要另外開啟線程池。應用層網關不需要開端口,并且不需要開啟線程池;
          • 2)接入層網關需要保持長連接,接入層網關需要本地緩存channel映射關系。應用層網關無狀態不需要保存。

          15.2接入層網關設計

          我的設計目標是:

          • 1)網關的線程池實現1+8+4+1,減少線程切換;
          • 2)集中實現長連接管理和推送能力;
          • 3)與業務服務器解耦,集群部署縮容擴容以及重啟升級不相互影響;
          • 4)長連接的監控與報警能力;
          • 5)客戶端重連指令一鍵實現。

          主要技術要點:

          • 1)支持自定義協議以及序列化;
          • 2)支持websocket協議;
          • 3)通道連接自定義保活以及心跳檢測;
          • 4)本地緩存channel;
          • 5)責任鏈;
          • 6)服務調用完全異步;
          • 7)泛化調用;
          • 8)轉發通知包或者Push包;
          • 9)容錯網關down機處理。

          設計方案(一個Notify包的數據經網關的線程模型圖):

          15.3應用層API網關設計

          我的設計目標是:

          • 1)基于版本的自動發現以及灰度/擴容 ,不需要關注IP;
          • 2)網關的線程池實現1+8+1,減少線程切換;
          • 3)支持協議轉換實現多個協議轉換,基于SPI來實現;
          • 4)與業務服務器解耦,集群部署縮容擴容以及重啟升級不相互影響;
          • 5)接口錯誤信息統計和RT時間的監控和報警能力;
          • 6)UI界面實現路由算法,服務接口版本管理,灰度策略管理以及接口和服務信息展示能力;
          • 7)基于OpenAPI提供接口級別的自動生成文檔的功能。

          主要技術要點:

          • 1)Http2.0;
          • 2)channel連接池復用;
          • 3)Netty http 服務端編解碼;
          • 4)責任鏈;
          • 5)服務調用完全異步;
          • 6)全鏈路超時機制;
          • 7)泛化調用。

          設計方案(一個請求包的數據經網關的架構圖):

          16、高并發設計

          16.1架構優化

          主要從以下幾個方面入手:

          • 1)水平擴展:各個模塊無狀態部署;
          • 2)線程模型:每個服務底層線程模型遵從Netty主從reactor模型;
          • 3)多層緩存:Gate層二級緩存,Redis一級緩存;
          • 4)長連接:客戶端長連接保持,避免頻繁創建連接消耗。

          16.2萬人群聊優化

          技術難點主要是:消息扇出大,比如每秒群聊有50條消息,群聊2000人,那么光一個群對系統并發就有10W的消息扇出。

          優化思路:

          • 1)批量ACK:每條群消息都ACK,會給服務器造成巨大的沖擊,為了減少ACK請求量,參考TCP的Delay ACK機制,在接收方層面進行批量ACK;
          • 2)群消息和成員批量加載以及懶加載:在真正進入一個群時才實時拉取群友的數據;
          • 3)群離線消息過多:群消息分頁拉取,第二次拉取請求作為第一次拉取請求的ack;
          • 4)對于消息未讀數場景,每個用戶維護一個全局的未讀數和每個會話的未讀數,當群聊非常大時,未讀資源變更的QPS非常大。這個時候應用層對未讀數進行緩存,批量寫+定時寫來保證未讀計數的寫入性能;
          • 5)路由信息存入redis會有寫入和讀取的性能瓶頸,每條消息在發出的時候會查路由信息來發送對應的gate接入層,比如有10個群,每個群1W,那么1s100條消息,那么1000W的查詢會打滿redis,即使redis做了集群。優化的思路就是將集中的路由信息分散到msg層 JVM本地內存中,然后做Route可用,避免單點故障;
          • 6)存儲的優化:擴散寫寫入并發量巨大,另一方面也存在存儲浪費,一般優化成擴散讀的方式存儲;
          • 7)消息路由到相同接入層機器進行合并請求減少網絡包傳輸。

          相關資料:

          1.《網易云信技術分享:IM中的萬人群聊技術方案實踐總結

          2.《企業微信的IM架構設計揭秘:消息模型、萬人群、已讀回執、消息撤回等

          3.《融云IM技術分享:萬人群聊消息投遞方案的思考和實踐

          16.3代碼優化

          具體的代碼優化思路就是:本地會話信息由一個hashmap保持,導致鎖機制嚴重,按照用戶標識進行hash,講會話信息存在多個map中,減少鎖競爭。同時利用雙buffer機制,避免未讀計數寫入阻塞。

          16.4推拉結合優化合并

          背景:消息下發到群聊服務后,需要發送拉取通知給接收者,具體邏輯是群聊服務同步消息到路由層,路由層發送消息給接收者,接收者再來拉取消息。

          問題:如果消息連續發送或者對同一個接收者連續發送消息頻率過高,會有許多的通知消息發送給路由層,消息量過大,可能會導致logic線程堆積,請求路由層阻塞。

          解決:發送者發送消息到邏輯層持久化后,將通知消息先存放一個隊列中,相同的接收者接收消息通知消息后,更新相應的最新消息通知時間,然后輪訓線程會輪訓隊列,將多個消息會合并為一個通知拉取發送至路由層,降低了客戶端與服務端的網絡消耗和服務器內部網絡消耗。

          好處:保證同一時刻,下發線程一輪只會向同一用戶發送一個通知拉取,一輪的時間可以自行控制。

          17、高可用設計

          17.1心跳設計

          主要是:

          • 1)服務端檢測到某個客戶端遲遲沒有心跳過來可以主動關閉通道,讓它下線,并且清除在線信息和路由信息;
          • 2)客戶端檢測到某個服務端遲遲沒有響應心跳也能重連獲取一個新的連接。

          智能心跳策略:比如正在發包的時候,不需要發送心跳。等待發包完畢后在開啟心跳。并且自適應心跳策略調整。

          相關資料:

          為何基于TCP協議的移動端IM仍然需要心跳保活機制?

          一文讀懂即時通訊應用中的網絡心跳包機制:作用、原理、實現思路等

          微信團隊原創分享:Android版微信后臺保活實戰分享(進程保活篇)

          微信團隊原創分享:Android版微信后臺保活實戰分享(網絡保活篇)

          融云技術分享:融云安卓端IM產品的網絡鏈路保活技術實踐

          移動端IM實踐:實現Android版微信的智能心跳機制

          萬字長文:手把手教你實現一套高效的IM長連接自適應心跳保活機制

          17.2系統穩定性設計

          背景:高峰期系統壓力大,偶發的網絡波動或者機器過載,都有可能導致大量的系統失敗。加上IM系統要求實時性,不能用異步處理實時發過來的消息。所以有了柔性保護機制防止雪崩。

          柔性保護機制開啟判斷指標,當每個指標不在平均范圍內的時候就開啟。

          這些判斷指標主要是:

          • 1)每條消息的ack時間 RT時間
          • 2)同時在線人數以及同時發消息的人數
          • 3)每臺機器的負載CPU和內存和網絡IO和磁盤IO以及GC參數

          當開啟了柔性保護機制,那么會返回失敗,用戶端體驗不友好,如何優化?

          以下是我的優化思路:

          • 1)當開啟了柔性保護機制,邏輯層hold住多余的請求,返回前端成功,不顯示發送失敗,后端異步重試,直至成功;
          • 2)為了避免重試加劇系統過載,指數時間延遲重試。

          17.3異常場景設計

          gate層重啟升級或者意外down機有以下問題:

          • 1)客戶端和gate意外丟失長連接,導致 客戶端在發送消息的時候導致消息超時等待以及客戶端重試等無意義操作;
          • 2)發送給客戶端的消息,從Msg消息層轉發給gate的消息丟失,導致消息超時等待以及重試。

          解決方案如下:

          • 1)重啟升級時候,向客戶端發送重新連接指令,讓客戶端重新請求LSB獲取IP直連;
          • 2)當gate層down機異常停止時候,增加hook鉤子,向客戶端發送重新連接指令;
          • 3)額外增加hook,向Msg消息層發送請求清空路由消息和在線狀態,并且清除redis的路由信息。

          17.4Redis宕機高可用設計

          Redis的作用背景:

          • 1)當用戶鏈接上網關后,網關會將用戶的userId和機器信息存入redis,用作這個user接收消息時候,消息的路由;
          • 2)消息服務在發消息給user時候,會查詢Redis的路由信息,用來發送消息給哪個一個網關。

          如果Redis宕機,會造成下面結果:

          • 1)消息中轉不過去,所有的用戶可以發送消息,但是都接收不了消息;
          • 2)如果有在線機制,那么系統都認為是離線狀態,會走手機消息通道推送。

          Redis宕機兜底處理策略:

          • 1)消息服務定時任務同步路由信息到本地緩存,如果redis掛了,從本地緩存拿消息;
          • 2)網關服務在收到用戶側的上線和下線后,會同步廣播本地的路由信息給各個消息服務,消息服務接收后更新本地環境數據;
          • 3)網絡交互次數多,以及消息服務多,可以用批量或者定時的方式同步廣播路由消息給各個消息服務。

          18、核心表結構設計

          核心設計要點:

          • 1)群消息只存儲一份,用戶不需要為每個消息單獨存一份。用戶也無需去刪除群消息;
          • 2)對于在線的用戶,收到群消息后,修改這個last_ack_msg_id;
          • 3)對于離線用戶,用戶上線后,對比最新的消息ID和last_ack_msg_id,來進行拉取(參考Kafka的消費者模型);
          • 4)對應單聊,需要記錄消息的送達狀態,以便在異常情況下來做重試處理。

          群用戶消息表 t_group_user_msg:

          群消息表 t_group_msg:

          參考資料:

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

          2.《基于Netty,從零開發一個IM服務端

          19、紅包設計

          搶紅包的大致核心邏輯如下:

          • 1)銀行快捷支付,保證賬戶余額和發送紅包邏輯的一致性;
          • 2)發送紅包后,首先計算好紅包的個數,個數確定好后,確定好每個紅包的金額,存入存儲層【這里可以是redis的List或者是隊列】方便后續每個人來取;
          • 3)生成一個24小時的延遲任務,檢測紅包是否還有錢方便退回;
          • 4)每個紅包的金額需要保證每個紅包的的搶金額概率是一致的,算法需要考量;
          • 5)存入數據庫表中后,服務器通過長連接,給群里notify紅包消息,供群成員搶紅包;
          • 6)群成員并發搶紅包,在第二步中會將每個紅包的金額放入一個隊列或者其他存儲中,群成員實際是來競爭去隊列中的紅包金額。兜底機制:如果redis掛了,可以重新生成紅包信息到數據庫中;
          • 7)取成功后,需要保證紅包剩余金額、新插入的紅包流水數據、隊列中的紅包數據以及群成員的余額賬戶金額一致性;
          • 8)這里還需要保證一個用戶只能領取一次,并且保持冪等。

          相關資料:

          社交軟件紅包技術解密(一):全面解密QQ紅包技術方案——架構、技術實現等

          社交軟件紅包技術解密(二):解密微信搖一搖紅包從0到1的技術演進

          社交軟件紅包技術解密(三):微信搖一搖紅包雨背后的技術細節

          社交軟件紅包技術解密(四):微信紅包系統是如何應對高并發的

          社交軟件紅包技術解密(五):微信紅包系統是如何實現高可用性的

          社交軟件紅包技術解密(六):微信紅包系統的存儲層架構演進實踐

          社交軟件紅包技術解密(七):支付寶紅包的海量高并發技術實踐

          社交軟件紅包技術解密(八):全面解密微博紅包技術方案

          社交軟件紅包技術解密(九):談談手Q紅包的功能邏輯、容災、運維、架構等

          社交軟件紅包技術解密(十):手Q客戶端針對2020年春節紅包的技術實踐

          社交軟件紅包技術解密(十一):解密微信紅包隨機算法(含代碼實現)

          社交軟件紅包技術解密(十二):解密抖音春節紅包背后的技術設計與實踐

          20、核心業務流程梳理

          20.1單聊流程

          假設是用戶A發消息給用戶B ,以下是完整的業務流程。

          1)A打包數據發送給服務端,服務端接收消息后,根據接收消息的sequence_id來進行客戶端發送消息的去重,并且生成遞增的消息ID,將發送的信息和ID打包一塊入庫,入庫成功后返回ACK,ACK包帶上服務端生成的消息ID。

          2)服務端檢測接收用戶B是否在線,在線直接推送給用戶B。

          3)如果沒有本地消息ID則存入,并且返回接入層ACK信息;如果有則拿本地sequence_id和推送過來的sequence_id大小對比,并且去重,進行展現時序進行排序展示,并且記錄最新一條消息ID。最后返回接入層ack。

          4)服務端接收ACK后,將消息標為已送達。

          5)如果用戶B不在線,首先將消息存入庫中,然后直接通過手機通知來告知客戶新消息到來。

          6)用戶B上線后,拿本地最新的消息ID,去服務端拉取所有好友發送給B的消息,考慮到一次拉取所有消息數據量大,通過channel通道來進行分頁拉取,將上一次拉取消息的最大的ID,作為請求參數,來請求最新一頁的比ID大的數據。

          20.2群聊流程

          假設是用戶A發消息給群G  ,以下是完整的業務流程。

          1)登錄,TCP連接,token校驗,名詞檢查,sequence_id去重,生成遞增的消息ID,群消息入庫成功返回發送方ACK。

          2)查詢群G所有的成員,然后去redis中央存儲中找在線狀態。離線和在線成員分不同的方式處理。

          3)在線成員:并行發送拉取通知,等待在線成員過來拉取,發送拉取通知包如丟失會有兜底機制。

          4)在線成員過來拉取,會帶上這個群標識和上一次拉取群的最小消息ID,服務端會找比這個消息ID大的所有的數據返回給客戶端,等待客戶端ACK。一段時間沒ack繼續推送。如果重試幾次后沒有回ack,那么關閉連接和清除ack等待隊列消息。

          5)客戶端會更新本地的最新的消息ID,然后進行ack回包。服務端收到ack后會更新群成員的最新的消息ID。

          6)離線成員:發送手機通知欄通知。離線成員上線后,拿本地最新的消息ID,去服務端拉取群G發送給A的消息,通過channel通道來進行分頁拉取,每一次請求,會將上一次拉取消息的最大的ID,作為請求參數來拉取消息,這里相當于第二次拉取請求包是作為第一次拉取的ack包。

          7)分頁的情況下,客戶端在收到上一頁請求的的數據后更新本地的最新的消息ID后,再請求下一頁并且帶上消息ID。上一頁請求的的數據可以當作為ack來返回服務端,避免網絡多次交互。服務端收到ack后會更新群成員的最新的消息ID。

          21、設計IM系統時的常見疑問

          21.1相比傳統HTTP請求的業務系統,IM業務系統的有哪些不一樣的設計難點?

          主要是在線狀態維護。

          相比于HTTP請求的業務系統,接入層有狀態,必須維持心跳和會話狀態,加大了系統設計復雜度。

          請求通信模型不一樣。相比于HTTP請求一個request等待一個response通信模型,IM系統則是一個數據包在全雙工長連接通道雙傳輸,客戶端和服務端消息交互的信令數據包設計復雜。

          21.2對于單聊和群聊的實時性消息,是否需要MQ來作為通信的中間件來代替rpc?

          MQ作為解耦可以有以下好處:

          • 1)易擴展:gate層到logic層無需路由,logic層多個有新的業務時候,只需要監聽新的topic即可;
          • 2)解耦:gate層到logic層解耦,不會有依賴關系;
          • 3)節省端口資源:gate層無需再開啟新的端口接收logic的請求,而且直接監聽MQ消息即可。

          但是缺點也有:

          • 1)網絡通信多一次網絡通信,增加RT的時間,消息實時性對于IM即使通信的場景是非常注重的一個點;
          • 2)MQ的穩定性,不管任何系統只要引入中間件都會有穩定性問題,需要考慮MQ不可用或者丟失數據的情況;
          • 3)需要考慮到運維的成本;
          • 4)當用消息中間代替路由層的時候,gate層需要廣播消費消息,這個時候gate層會接收大部分的無效消息,因為這個消息的接收者channel不在本機維護的session中。

          綜上:是否考慮使用MQ需要架構師去考量,比如考慮業務是否允許、或者系統的流量、或者高可用設計等等影響因素。本項目基于使用成本、耦合成本和運維成本考慮,采用Netty作為底層自定義通信方案來實現,也能同樣實現層級調用。

          參考資料:阿里IM技術分享(九):深度揭密RocketMQ在釘釘IM系統中的應用實踐》。

          21.3為什么接入層用LSB返回的IP來做接入呢?

          可以有以下好處:

          • 1)靈活的負載均衡策略 可根據最少連接數來分配IP;
          • 2)做灰度策略來分配IP;
          • 3)AppId業務隔離策略 不同業務連接不同的gate,防止相互影響。

          21.4為什么應用層心跳對連接進行健康檢查?

          因為TCP Keepalive狀態無法反應應用層狀態問題,如進程阻塞、死鎖、TCP緩沖區滿等情況。

          并且要注意心跳的頻率,頻率小則可能及時感知不到應用情況,頻率大可能有一定的性能開銷。

          參考資料:為何基于TCP協議的移動端IM仍然需要心跳保活機制?》、《徹底搞懂TCP協議層的KeepAlive保活機制》。

          21.5MQ的使用場景?

          IM消息是非常龐大的,比如說群聊相關業務、推送,對于一些業務上可以忍受的場景,盡量使用MQ來解耦和通信,來降低同步通訊的服務器壓力。

          21.6群消息存一份還是多份,讀擴散還是寫擴散?

          我的設計是存1份,讀擴散。

          存多份的話(也就是寫擴散)下同一條消息存儲了很多次,對磁盤和帶寬造成了很大的浪費。可以在架構上和業務上進行優化,來實現讀擴散。

          當然,對于IM是使用讀擴散還是寫擴散來實現,這需要根據IM產品的業務定位來決定。比如微信就是寫擴散(詳見《企業微信的IM架構設計揭秘:消息模型、萬人群、已讀回執、消息撤回等》),而釘釘卻是讀擴散(詳見《深度解密釘釘即時消息服務DTIM的技術設計》)。

          21.7消息ID為什么是趨勢遞增就可以,嚴格遞增的不行嗎?

          嚴格遞增會有單點性能瓶頸,比如MySQL auto increments。

          redis性能好但是沒有業務語義,比如缺少時間因素,還可能會有數據丟失的風險,并且集群環境下寫入ID也屬于單點,屬于集中式生成服務。

          小型IM可以根據業務場景需求直接使用redis的incr命令來實現IM消息唯一ID。

          本項目采用snowflake算法實現唯一趨勢遞增ID,即可實現IM消息中,時序性,重復性以及查找功能。

          關于消息ID的生成,可以參考下面的系列文章:

          微信的海量IM聊天消息序列號生成實踐(算法原理篇)

          微信的海量IM聊天消息序列號生成實踐(容災方案篇)

          解密融云IM產品的聊天消息ID生成策略

          深度解密美團的分布式ID生成算法

          開源分布式ID生成器UidGenerator的技術實現

          深度解密滴滴的高性能ID生成器(Tinyid)

          21.8gate層為什么需要開兩個端口?

          gate會接收客戶端的連接請求(被動),需要外網監聽端口;entry會主動給logic發請求(主動);entry會接收服務端給它的通知請求(被動),需要內網監聽端口。一個端口對內,一個端口對外。

          21.9用戶的路由信息,是維護在中央存儲的redis中,還是維護在每個msg層內存中?

          維護在每個msg層內存中有狀態:多級緩存避免和中間件多次交互,并發高。

          維護在中央存儲的redis中,msg層無狀態,redis壓力大,每次交互IO網絡請求大。

          業務初期為了減少復雜度,可以維護在Redis中。

          21.10網關層和服務層以及msg層和網關層請求模型具體是怎樣的?

          網關層到服務層,只需要單向傳輸發請求,網關層不需要關心調用的結果。

          而客戶端想要的ack或者notify請求是由SDK發送數據到網關層,SDK也不需要關心調用的結果,最后網關層只轉發數據,不做額外的邏輯處理。

          SDK和所有的網關進行長連接,當發送信息給客戶端時,根據路由尋址信息,即可通過長連接推送信息

          21.11本地寫數據成功,一定代表對端應用側接收讀取消息了嗎?

          本地TCP寫操作成功,但數據可能還在本地寫緩沖區中、網絡鏈路設備中、對端讀緩沖區中,并不代表對端應用讀取到了數據。

          如果你還不理解,可以讀讀這篇文章《從客戶端的角度來談談移動端IM的消息可靠性和送達機制》。

          21.12為什么用netty做來做http網關, 而不用tomcat?

          主要是從以下方面考慮:

          • 1)netty對象池,內存池,高性能線程模型;
          • 2)netty堆外內存管理,減少GC壓力,jvm管理的只是一個很小的DirectByteBuffer對象引用;
          • 3)tomcat讀取數據和寫入數據都需要從內核態緩沖copy到用戶態的JVM中,多1次或者2次的拷貝會有性能影響。

          21.13為什么消息入庫后,對于在線狀態的用戶,單聊直接推送,群聊通知客戶端來拉取,而不是直接推送消息給客戶端(推拉結合)?

          在保證消息實時性的前提下,對于單聊,直接推送。

          對于群聊,由于群聊人數多,推送的話一份群消息會對群內所有的用戶都產生一份推送的消息,推送量巨大。

          解決辦法是按需拉取,當群消息有新消息時候發送時候,服務端主動推送新的消息數量,然后客戶端分頁按需拉取數據。

          21.14為什么除了單聊、群聊、推送、離線拉取等實時性業務,其他的業務都走http協議?

          IM協議簡單最好,如果讓其他的業務請求混進IM協議中,會讓其IM變的更復雜,比如查找離線消息記錄拉取走http通道避免tcp 通道壓力過大,影響即時消息下發效率。

          在比如上傳圖片和大文件,可以利用HTTP的斷點上傳和分段上傳特性。

          21.15機集群機器要考慮到哪些優化?

          主要有:

          • 1)網絡寬帶;
          • 2)最大文件句柄;
          • 3)每個tcp的內存占用;
          • 4)Linux系統內核tcp參數優化配置;
          • 5)網絡IO模型;
          • 6)網絡網絡協議解析效率;
          • 7)心跳頻率;
          • 8)會話數據一致性保證;
          • 9)服務集群動態擴容縮容。

          22、系列文章

          跟著源碼學IM(一):手把手教你用Netty實現心跳機制、斷線重連機制

          跟著源碼學IM(二):自已開發IM很難?手把手教你擼一個Andriod版IM

          跟著源碼學IM(三):基于Netty,從零開發一個IM服務端

          跟著源碼學IM(四):拿起鍵盤就是干,教你徒手開發一套分布式IM系統

          跟著源碼學IM(五):正確理解IM長連接、心跳及重連機制,并動手實現

          跟著源碼學IM(六):手把手教你用Go快速搭建高性能、可擴展的IM系統

          跟著源碼學IM(七):手把手教你用WebSocket打造Web端IM聊天

          跟著源碼學IM(八):萬字長文,手把手教你用Netty打造IM聊天

          跟著源碼學IM(九):基于Netty實現一套分布式IM系統

          跟著源碼學IM(十):基于Netty,搭建高性能IM集群(含技術思路+源碼)

          跟著源碼學IM(十一):一套基于Netty的分布式高可用IM詳細設計與實現(有源碼)》(* 本文)

          SpringBoot集成開源IM框架MobileIMSDK,實現即時通訊IM聊天功能

          23、參考資料

          [1] 史上最通俗Netty框架入門長文:基本介紹、環境搭建、動手實戰

          [2] 強列建議將Protobuf作為你的即時通訊應用數據傳輸格式

          [3] IM通訊協議專題學習(一):Protobuf從入門到精通,一篇就夠!

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

          [5] 探討組合加密算法在IM中的應用

          [6] 從客戶端的角度來談談移動端IM的消息可靠性和送達機制

          [7] IM消息送達保證機制實現(一):保證在線實時消息的可靠投遞

          [8] 理解IM消息“可靠性”和“一致性”問題,以及解決方案探討

          [9] 融云技術分享:全面揭秘億級IM消息的可靠投遞機制

          [10] IM群聊消息如此復雜,如何保證不丟不重?

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

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

          [13] 如何保證IM實時消息的“時序性”與“一致性”?

          [14] 阿里IM技術分享(六):閑魚億級IM消息系統的離線推送到達率優化

          [15] 微信的海量IM聊天消息序列號生成實踐(算法原理篇)

          [16] 社交軟件紅包技術解密(一):全面解密QQ紅包技術方案——架構、技術實現等

          [17] 網易云信技術分享:IM中的萬人群聊技術方案實踐總結

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

          [19] 融云IM技術分享:萬人群聊消息投遞方案的思考和實踐

          [20] 為何基于TCP協議的移動端IM仍然需要心跳保活機制?

          [21] 一文讀懂即時通訊應用中的網絡心跳包機制:作用、原理、實現思路等

          [22] 微信團隊原創分享:Android版微信后臺保活實戰分享(網絡保活篇)

          [23] 融云技術分享:融云安卓端IM產品的網絡鏈路保活技術實踐

          [24] 阿里IM技術分享(九):深度揭密RocketMQ在釘釘IM系統中的應用實踐

          [25] 徹底搞懂TCP協議層的KeepAlive保活機制

          [26] 深度解密釘釘即時消息服務DTIM的技術設計

          (本文已同步發布于:http://www.52im.net/thread-4257-1-1.html



          作者:Jack Jiang (點擊作者姓名進入Github)
          出處:http://www.52im.net/space-uid-1.html
          交流:歡迎加入即時通訊開發交流群 215891622
          討論:http://www.52im.net/
          Jack Jiang同時是【原創Java Swing外觀工程BeautyEye】【輕量級移動端即時通訊框架MobileIMSDK】的作者,可前往下載交流。
          本博文 歡迎轉載,轉載請注明出處(也可前往 我的52im.net 找到我)。


          只有注冊用戶登錄后才能發表評論。


          網站導航:
           
          Jack Jiang的 Mail: jb2011@163.com, 聯系QQ: 413980957, 微信: hellojackjiang
          主站蜘蛛池模板: 中山市| 云梦县| 淅川县| 酒泉市| 林周县| 蓬莱市| 开原市| 千阳县| 大丰市| 潮安县| 通道| 洪洞县| 乌兰察布市| 淮阳县| 奇台县| 堆龙德庆县| 丰台区| 和林格尔县| 女性| 西贡区| 大连市| 巴彦淖尔市| 新竹市| 定边县| 石柱| 交口县| 个旧市| 信丰县| 曲麻莱县| 洛宁县| 米脂县| 拜泉县| 小金县| 武陟县| 疏勒县| 开化县| 德格县| 宁晋县| 响水县| 文成县| 衡阳市|