IM的消息,如何保障可靠性


文章很長,建議收藏起來,慢慢讀! 瘋狂創客圈為小伙伴奉上以下珍貴的學習資源:


推薦: 瘋狂創客圈 高質量 博文

高並發 必讀 的精彩博文
nacos 實戰(史上最全) sentinel (史上最全+入門教程)
Zookeeper 分布式鎖 (圖解+秒懂+史上最全) Webflux(史上最全)
SpringCloud gateway (史上最全) TCP/IP(圖解+秒懂+史上最全)
10分鍾看懂, Java NIO 底層原理 Feign原理 (圖解)
更多精彩博文 ..... 請參見【 瘋狂創客圈 高並發 總目錄

IM的消息,如何保障可靠性

IM App 是我做過 App 類型里復雜度最高的一類,里面可供深究探討的技術難點非常之多。這篇文章和大家聊下消息可靠抵達機制。

如何確保 IM 不丟消息是個相對復雜的話題,從客戶端發送數據到服務器,再從服務器抵達目標客戶端,最終在 UI 成功展示,其間涉及的環節很多,這里只取其中一環「接收端如何確保消息不丟失」來探討,粗略聊下我接觸過的兩種設計思路。

說到可靠抵達,第一反應會聯想到 TCP 的 reliability。數據可靠抵達是個通用性的問題,無論是網絡二進制流數據,還是上層的業務數據,都有可靠性保障問題,TCP 作為網絡基礎設施協議,其可靠性設計的可靠性是毋庸置疑的,我們就從 TCP 的可靠性說起。

在 TCP 這一層,所有 Sender 發送的數據,每一個 byte 都有標號(Sequence Number),每個 byte 在抵達接收端之后都會被接收端返回一個確認信息(Ack Number), 二者關系為 Ack = Seq + 1。簡單來說,如果 Sender 發送一個 Seq = 1,長度為 100 bytes 的包,那么 receiver 會返回一個 Ack = 101 的包,如果 Sender 收到了這個Ack 包,說明數據確實被 Receiver 收到了,否則 Sender 會采取某種策略重發上面的包。

第一個問題是:現在的 IM App 幾乎都是走 TCP 通道,既然 TCP 本身是具備可靠性的,為什么還會出現消息接收端(Receiver)丟失消息的情況,看下圖一目了然:

在這里插入圖片描述

一句話總結上圖的含義:網絡層的可靠性不等同於業務層的可靠性

數據可靠抵達網絡層之后,還需要一層層往上移交處理,可能的處理有:安全性校驗,binary 解析,model 創建,寫 db,存入 cache,UI 展示,以及一些 edge cases(斷網,用戶 logout,disk full,OOM,crash,關機。。) 等等,項目的 feature 越多,網絡層往上的處理出錯的可能性就越大。

舉個最簡單的場景為例子,消息可靠抵達網絡層之后,寫 db 之前 App crash(不稀奇,是 App 都會 crash),雖然數據在網絡層可靠抵達了,但沒存進 db,下次用戶打開 App 消息自然就丟失了,如果不在業務層再增加可靠性保障,網絡層面不會重發,那么意味着這條消息對於 Receiver 永遠丟失了。業務層保障可以采取兩種方案:


簡單的方案:應用層 Ack 消息

這個方案可以簡單理解為,將 TCP 的 Ack 流程再走一遍,在應用層也構建一個 Ack 消息,在應用層可靠性得到確認之后,再發送這個 Ack 消息。

發送端收到接收端Ack 消息之后,才認為 接收端 已收到,否則也采取某種策略重發消息(一個重發隊列,進行timer定時掃描)。

具體實現:

一、報文類型

im的客戶端與服務器通過發送報文(也就是網絡包)來完成消息的傳遞,報文分為三種

請求報文(request,后簡稱為為R)

應答報文(acknowledge,后簡稱為A)

通知報文(notify,后簡稱為N),這三種報文的解釋如下:
img
R:客戶端主動發送給服務器的報文
A:服務器被動應答客戶端的報文,一個A對應一個R
N:服務器主動發送給客戶端的報文

二、普通消息投遞流程

用戶A給用戶B發送一個“你好”,流程如下:
img
1)client-A向im-server發送一個消息請求包,即msg:R
2)im-server在成功處理后,回復client-A一個消息響應包,即msg:A
3)如果此時client-B在線,則im-server主動向client-B發送一個消息通知包,即msg:N(當然,如果client-B不在線,則消息會存儲離線)

三、上述消息投遞流程出現的問題

從流程圖中容易看到,發送方client-A收到msg:A后,只能說明im-server成功接收到了消息,並不能說明client-B接收到了消息。在若干場景下,可能出現msg:N包丟失,且發送方client-A完全不知道,例如:
1)服務器崩潰,msg:N包未發出
2)網絡抖動,msg:N包被網絡設備丟棄
3)client-B崩潰,msg:N包未接收
結論是悲觀的:接收方client-B是否有收到msg:N,發送方client-A完全不可控,那怎么辦呢?

四、應用層確認+im消息可靠投遞的六個報文

upd是一種不可靠的傳輸層協議,tcp是一種可靠的傳輸層協議,tcp是如何做到可靠的?答案是:超時、重傳、確認。
要想實現應用層的消息可靠投遞,必須加入應用層的確認機制,即:要想讓發送方client-A確保接收方client-B收到了消息,必須讓接收方client-B給一個消息的確認,這個應用層的確認的流程,與消息的發送流程類似:
img
4)client-B向im-server發送一個ack請求包,即ack:R
5)im-server在成功處理后,回復client-B一個ack響應包,即ack:A
6)則im-server主動向client-A發送一個ack通知包,即ack:N
至此,發送“你好”的client-A,在收到了ack:N報文后,才能確認client-B真正接收到了“你好”。
會發現,一條消息的發送,分別包含(上)(下)兩個半場,即msg的R/A/N三個報文,ack的R/A/N三個報文,一個應用層即時通訊消息的可靠投遞,共涉及6個報文,這就是im系統中消息投遞的最核心技術。

五、可靠消息投遞存在什么問題

期望六個報文完成消息的可靠投遞,但實際情況,msg:N,ack:N這兩個報文都可能丟失(原因如第二點所述,可能是服務器奔潰、網絡抖動、或者客戶端奔潰),此時client-A都收不到期待的ack:N報文,即client-A不能確認client-B是否收到“你好”,但這兩個報文的丟失對應的業務影響又大有不同:

1)msg:N包丟失,業務結果是client-B沒有收到消息

2)ack:N包丟失,業務結果是client-B收到了消息,只是client-A不知道而已

那怎么辦呢?

六、消息的超時與重傳

client-A發出了msg:R,收到了msg:A之后,在一個期待的時間內,如果沒有收到ack:N,client-A會嘗試將msg:R重發。可能client-A同時發出了很多消息,故client-A需要在本地維護一個等待ack隊列,並配合timer超時機制,來記錄哪些消息沒有收到ack:N,以定時重發。
img
一旦收到了ack:N,說明client-B收到了“你好”消息,對應的消息將從“等待ack隊列”中移除。

七、消息的重傳存在什么問題

第五點提到過,msg:N,ack:N都有可能丟失:
1)msg:N報文丟失,說明client-B之前壓根沒有收到“你好”報文,超時與重傳機制十分有效
2)ack:N報文丟失,說明client-B之前已經收到了“你好”報文(只是client-A不知道而已),超時與重傳機制將導致client-B收到重復的消息,那怎么辦呢?

八、消息的去重

解決方法也很簡單,由發送方client-A生成一個消息去重的msgid,保存在“等待ack隊列”里,同一條消息使用相同的msgid來重傳,供client-B去重,而不影響用戶體驗。

九、其他

1)上述設計理念,由客戶端重傳,可以保證服務端無狀態性(架構設計基本准則)
2)如果client-B不在線,im-server保存了離線消息后,要偽造ack:N發送給client-A

十、總結

1)im系統是通過超時、重傳、確認、去重的機制來保證消息的可靠投遞,不丟不重
2)一個“你好”的發送,包含上半場msg:R/A/N與下半場ack:R/A/N的6個報文

3)im系統難以做到系統層面的不丟不重,只能做到業務層面的不丟不重

參考文章:

https://blog.csdn.net/wufaliang003/article/details/78638364

https://www.jianshu.com/p/4781cf9ffce8


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM