IM系統四大基本特性-可靠性(及解決方案)

消息丟失有哪幾種情況?

我們以最常見的“服務端路由中轉”類型的 IM 系統爲例(非 P2P),這裏解釋一下,所謂的“服務端路由中轉”是指:一條消息從用戶 A 發出後,需要先經過 IM 服務器來進行中轉,然後再由 IM 服務器推送給用戶 B,這個也是目前最常見的 IM 系統的消息分發類型。

那麼,我們來假設一個場景:用戶 A 給用戶 B 發送一條消息。接下來我們看看哪些環節可能存在丟消息的風險?

參考上面時序圖,發消息大概整體上分爲兩部分:

1.用戶 A 發送消息到 IM 服務器,服務器將消息暫存,然後返回成功的結果給發送方 A(步驟 1、2、3);

2.IM 服務器接着再將暫存的用戶 A 發出的消息,推送給接收方用戶 B(步驟 4)。 

其中可能丟失消息的場景有下面這些。在第一部分中。步驟 1、2、3 都可能存在失敗的情況。

1.由於用戶 A 發消息是一個“請求”和“響應”的過程,如果用戶 A 在把消息發送到 IM 服務器的過程中,由於網絡不通等原因失敗了;

2.或者 IM 服務器接收到消息進行服務端存儲時失敗了;

3.或者用戶 A 等待 IM 服務器一定的超時時間,但 IM 服務器一直沒有返回結果,那麼這些情況用戶 A 都會被提示發送失敗。

注意:

1.在第一部分中。步驟 1、2、3 都可能存在失敗的情況。由於用戶 A 發消息是一個“請求”和“響應”的過程,如果用戶 A 在把消息發送到 IM 服務器的過程中,由於網絡不通等原因失敗了;或者 IM 服務器接收到消息進行服務端存儲時失敗了;或者用戶 A 等待 IM 服務器一定的超時時間,但 IM 服務器一直沒有返回結果,那麼這些情況用戶 A 都會被提示發送失敗。接下來,他可以通過重試等方式來彌補,注意這裏可能會導致發送重複消息的問題。比如:客戶端在超時時間內沒有收到響應然後重試,但實際上,請求可能已經在服務端成功處理了,只是響應慢了,因此這種情況需要服務端有去重邏輯,一般發送端針對同一條重試消息有一個唯一的 ID,便於服務端去重使用。

2.在第二部分中。消息在 IM 服務器存儲完後,響應用戶 A 告知消息發送成功了,然後 IM 服務器把消息推送給用戶 B 的在線設備。在推送的準備階段或者把消息寫入到內核緩衝區後,如果服務端出現掉電,也會導致消息不能成功推送給用戶 B。這種情況實際上由於連接的 IM 服務器可能已經無法正常運轉,需要通過後期的補救措施來解決丟消息的問題。即使我們的消息成功通過 TCP 連接給到用戶 B 的設備,但如果用戶 B 的設備在接收後的處理過程出現問題,也會導致消息丟失。比如:用戶 B 的設備在把消息寫入本地 DB 時,出現異常導致沒能成功入庫,這種情況下,由於網絡層面實際上已經成功投遞了,但用戶 B 卻看不到消息。所以比較難處理。

一般我們會用下面這些相應的解決方案:

1.針對第一部分,我們通過客戶端 A 的超時重發和 IM 服務器的去重機制,基本就可以解決問題;

2.針對第二部分,業界一般參考 TCP 協議的 ACK 機制,實現一套業務層的 ACK 協議。

解決丟失的方案:業務層 ACK 機制

我們先解釋一下 ACK,ACK 全稱 Acknowledge,是確認的意思。在 TCP 協議中,默認提供了 ACK 機制,通過一個協議自帶的標準的 ACK 數據包,來對通信方接收的數據進行確認,告知通信發送方已經確認成功接收了數據。那麼,業務層 ACK 機制也是類似,解決的是:IM 服務推送後如何確認消息是否成功送達接收方。具體實現如下圖:

IM 服務器在推送消息時,攜帶一個標識 SID(安全標識符,類似 TCP 的 sequenceId),推送出消息後會將當前消息添加到“待 ACK 消息列表”,客戶端 B 成功接收完消息後,會給 IM 服務器回一個業務層的 ACK 包,包中攜帶有本條接收消息的 SID,IM 服務器接收後,會從“待 ACK 消息列表”記錄中刪除此條消息,本次推送纔算真正結束。

1.ACK 機制中的消息重傳

如果消息推給用戶 B 的過程中丟失了怎麼辦?比如:B 網絡實際已經不可達,但 IM 服務器還沒有感知到;用戶 B 的設備還沒從內核緩衝區取完數據就崩潰了;消息在中間網絡途中被某些中間設備丟掉了,TCP 層還一直重傳不成功等。以上的問題都會導致用戶 B 接收不到消息。

解決方案:解決這個問題的常用策略其實也是參考了 TCP 協議的重傳機制。類似的,IM 服務器的“等待 ACK 隊列”一般都會維護一個超時計時器,一定時間內如果沒有收到用戶 B 回的 ACK 包,會從“等待 ACK 隊列”中重新取出那條消息進行重推。

2.消息重複推送的問題

剛纔提到,對於推送的消息,如果在一定時間內沒有收到 ACK 包,就會觸發服務端的重傳。收不到 ACK 的情況有兩種,除了推送的消息真正丟失導致用戶 B 不回 ACK 外,還可能是用戶 B 回的 ACK 包本身丟了。對於第二種情況,ACK 包丟失導致的服務端重傳,可能會讓接收方收到重複推送的消息。

解決方案:

一般的解決方案是:服務端推送消息時攜帶一個 Sequence ID,Sequence ID 在本次連接會話中需要唯一,針對同一條重推的消息 Sequence ID 不變,接收方根據這個唯一的 Sequence ID 來進行業務層的去重,這樣經過去重後,對於用戶 B 來說,看到的還是接收到一條消息,不影響使用體驗。

3.這樣真的就不會丟消息了嗎?

細心的你可能發現,通過“ACK+ 超時重傳 + 去重”的組合機制,能解決大部分用戶在線時消息推送丟失的問題,那是不是就能完全覆蓋所有丟消息的場景呢?設想一下,假設一臺 IM 服務器在推送出消息後,由於硬件原因宕機了,這種情況下,如果這條消息真的丟了,由於負責的 IM 服務器宕機了無法觸發重傳,導致接收方 B 收不到這條消息。這就存在一個問題,當用戶 B 再次重連上線後,可能並不知道之前有一條消息丟失的情況。對於這種重傳失效的情況該如何處理?

補救措施方案:消息完整性檢查

針對服務器宕機可能導致的重傳失效的問題我們來分析一下,這裏的問題在於:服務器機器宕機,重傳這條路走不通了。那如果在用戶 B 在重新上線時,讓服務端有能力進行完整性檢查,發現用戶 B“有消息丟失”的情況,就可以重新同步或者修復丟失的數據。比較常見的消息完整性檢查的實現機制有“時間戳比對”,具體的實現如下圖:

下面我們來看一下“時間戳機制是如何對消息進行完整性檢查的,我用這個例子來解釋一下這個過程。

1.IM 服務器給接收方 B 推送 msg1,順便帶上一個最新的時間戳 timestamp1,接收方 B 收到 msg1 後,更新本地最新消息的時間戳爲 timestamp1。

2.IM 服務器推送第二條消息 msg2,帶上一個當前最新的時間戳 timestamp2,msg2 在推送過程中由於某種原因接收方 B 和 IM 服務器連接斷開,導致 msg2 沒有成功送達到接收方 B。

3.用戶 B 重新連上線,攜帶本地最新的時間戳 timestamp1,IM 服務器將用戶 B 暫存的消息中時間戳大於 timestamp1 的所有消息返回給用戶 B,其中就包括之前沒有成功的 msg2。

4.用戶 B 收到 msg2 後,更新本地最新消息的時間戳爲 timestamp2。

通過上面的時間戳機制,用戶 B 可以成功地讓丟失的 msg2 進行補償發送。需要說明的是,由於時間戳可能存在多機器時鐘不同步的問題,所以可能存在一定的偏差,導致數據獲取上不夠精確。所以在實際的實現上,也可以使用全局的自增序列作爲版本號來代替。

總結:

保證消息的可靠投遞是 IM 系統設計中至關重要的一個環節,“不丟消息”“消息不重複”對用戶體驗的影響較大,我們可以通過以下手段來確保消息下推的可靠性。

1.大部分場景和實際實現中,通過業務層的 ACK 確認和重傳機制,能解決大部分推送過程中消息丟失的情況。

2.通過客戶端的去重機制,屏蔽掉重傳過程中可能導致消息重複的問題,從而不影響用戶體驗。

3.針對重傳消息不可達的特殊場景,我們還可以通過“兜底”的完整性檢查機制來及時發現消息丟失的情況並進行補推修復,消息完整性檢查可以通過時間戳比對,或者全局自增序列等方式來實現。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章