微信:我們絕不丟消息!
有水友問我說,總感覺微信不丟消息,它是怎么做到的?
之前做過幾十年IM架構,今天和大家聊聊消息的可靠投遞。
IM系統中,報文分幾種類型?答,三種:
- 請求報文(Request)
- 應答報文(Acknowledge)
- 通知報文(Notify)
- R:客戶端主動發送給服務器的報文;
- A:服務器被動應答客戶端的報文,一個A對應一個R;
- N:服務器主動發送給客戶端的報文;
路人架構師如何設計消息投遞流程?
一個沒做過IM系統的路人架構師,他可能會這么設計消息投遞流程:用戶A給用戶B發送“你好”:
- client-A向im-server發送msg:R;
- im-server回復client-A一個msg:A;
- 如果此時client-B在線,則im-server向client-B發送msg:N;
畫外音:如果client-B不在線,im-server存儲離線消息。
上述消息投遞流程存在什么問題?
流程圖中會發現,發送方client-A收到msg:A后,只能說明im-server成功接收到了消息,并不能說明client-B接收到了消息。
在若干場景下,可能出現msg:N包丟失,例如:
- 服務器崩潰,msg:N包未發出;
- 網絡抖動,msg:N包被網絡設備丟棄;
- client-B崩潰,msg:N包未接收;
結論是悲觀的:接收方client-B是否有收到msg:N,發送方client-A完全不可控。
那怎么辦呢?
應用層的消息可靠投遞,必須通過應用層的超時、重傳、確認來保證。
首先,加入應用層的確認機制。
要想讓發送方client-A確保接收方client-B收到了消息,必須讓接收方client-B回復client-A一個消息的確認。這個應用層的確認流程,與消息的發送流程類似:
- client-B向im-server發送ack:R;
- im-server回復client-B一個ack:A;
- im-server向client-A發送ack:N;
至此,發送“你好”的client-A,在收到了ack:N報文后,才能確認client-B真正接收到了“你好”。
你會發現,一條“你好”的發送,分別包含上下兩個半場,即msg的R/A/N三個報文,ack的R/A/N三個報文,這是IM系統中消息投遞的核心。
還可能存在什么問題?
復雜的網絡環境下,msg:N,ack:N這兩個報文都可能丟失(服務器奔潰、網絡抖動、客戶端奔潰),此時client-A都收不到期待的ack:N報文,即client-A不能確認client-B是否收到“你好”,但這兩個報文的丟失對應的業務影響又大有不同:
- msg:N包丟失,業務結果是client-B沒有收到消息;
- ack:N包丟失,業務結果是client-B收到了消息,只是client-A不知道而已;
結論仍然是悲觀的:client-A無法知曉具體是哪種情況。
那怎么辦呢?
接下來,要加入超時與重傳。
client-A發出了msg:R,收到了msg:A之后,在一個期待的時間內,如果沒有收到ack:N,client-A會嘗試將msg:R重發。
可能client-A同時發出了很多消息,故client-A需要在本地維護一個等待ack隊列,并配合timer超時機制,來記錄哪些消息沒有收到ack:N,以定時重發。
一旦收到了ack:N,說明client-B收到了“你好”消息,對應的消息將從“等待ack隊列”中移除。
消息的重傳會引入什么新的問題?
超時與重傳機制可能導致client-B收到重復的消息。
那怎么辦呢?
最后,client-B要引入消息的去重機制。
發送方client-A生成一個消息去重的msgid,保存在“等待ack隊列”里,同一條消息使用相同的msgid來重傳,供client-B去重,而不影響用戶體驗。
也就是說,系統層面,client-B其實收到了很多消息,而產品體驗層面,用戶并不知道。系統,就是在背后默默保證你體驗的那個人!
總結
- IM系統通過超時、重傳、確認、去重的機制來保證消息的可靠投遞;
- 一個“你好”的發送,包含上半場msg:R/A/N與下半場ack:R/A/N的6個報文;
- 等待ACK隊列,是超時重傳的關鍵,重傳的消息msgid相同;
- 無法做到系統層面的不丟不重,只能做到業務層面的不丟不重;
知其然,知其所以然。
思路比結論更重要。