1. 程式人生 > >互聯網面試必殺:如何保證消息中間件全鏈路數據100%不丟失:第四篇

互聯網面試必殺:如何保證消息中間件全鏈路數據100%不丟失:第四篇

技術方案 重新 模式 消費 服務 聯網 並發 怎麽 架構

前情提示

上篇文章:《互聯網面試必殺:如何保證消息中間件全鏈路數據100%不丟失:第三篇》,我們分析了 RabbitMQ 開啟手動ack機制保證消費端數據不丟失的時候,prefetch 機制對消費者的吞吐量以及內存消耗的影響。

通過分析,我們知道了 prefetch 過大容易導致內存溢出,prefetch 過小又會導致消費吞吐量過低,所以在實際項目中需要慎重測試和設置。

這篇文章,我們轉移到消息中間件的生產端,一起來看看如何保證投遞到 MQ 的數據不丟失。

如果投遞出去的消息在網絡傳輸過程中丟失,或者在 RabbitMQ 的內存中還沒寫入磁盤的時候宕機,都會導致生產端投遞到MQ的數據丟失。

而且丟失之後,生產端自己還感知不到,同時還沒辦法來補救。

下面的圖就展示了這個問題。

技術分享圖片

所以本文呢,我們就來逐步分析一下。

保證投遞消息不丟失的 confirm 機制

其實要解決這個問題,相信大家看過之前的消費端 ack 機制之後,也都猜到了。

很簡單,就是生產端(比如上圖的訂單服務)首先需要開啟一個 confirm 模式,接著投遞到 MQ 的消息,如果 MQ 一旦將消息持久化到磁盤之後,必須也要回傳一個 confirm 消息給生產端。

這樣的話,如果生產端的服務接收到了這個 confirm 消息,就知道是已經持久化到磁盤了。

否則如果沒有接收到confirm消息,那麽就說明這條消息半路可能丟失了,此時你就可以重新投遞消息到 MQ 去,確保消息不要丟失。

而且一旦你開啟了confirm模式之後,每次消息投遞也同樣是有一個 delivery tag的,也是起到唯一標識一次消息投遞的作用。

這樣,MQ回傳ack給生產端的時候,會帶上這個 delivery tag。你就知道具體對應著哪一次消息投遞了,可以刪除這條消息。

此外,如果 RabbitMQ 接收到一條消息之後,結果內部出錯發現無法處理這條消息,那麽他會回傳一個 nack 消息給生產端。此時你就會感知到這條消息可能處理有問題,你可以選擇重新再次投遞這條消息到MQ去。

或者另一種情況,如果某條消息很長時間都沒給你回傳 ack/nack,那可能是極端意外情況發生了,數據也丟了,你也可以自己重新投遞消息到 MQ 去。

通過這套 confirm 機制,就可以實現生產端投遞消息不會丟失的效果。大家來看看下面的圖,一起來感受一下。

技術分享圖片

confirm機制的代碼實現

下面,我們再來看看confirm機制的代碼實現:

技術分享圖片

confirm機制投遞消息的高延遲性

這裏有一個很關鍵的點,就是一旦啟用了 confirm 機制投遞消息到 MQ 之後,MQ 是不保證什麽時候會給你一個ack或者nack的。

因為 RabbitMQ 自己內部將消息持久化到磁盤,本身就是通過異步批量的方式來進行的。

正常情況下,你投遞到 RabbitMQ 的消息都會先駐留在內存裏,然後過了幾百毫秒的延遲時間之後,再一次性批量把多條消息持久化到磁盤裏去。

這樣做,是為了兼顧高並發寫入的吞吐量和性能的,因為要是你來一條消息就寫一次磁盤,那麽性能會很差,每次寫磁盤都是一次 fsync 強制刷入磁盤的操作,是很耗時的。

所以正是因為這個原因,你打開了 confirm 模式之後,很可能你投遞出去一條消息,要間隔幾百毫秒之後,MQ 才會把消息寫入磁盤,接著你才會收到 MQ 回傳過來的 ack 消息,這個就是所謂confirm機制投遞消息的高延遲性。

大家看看下面的圖,一起來感受一下。

技術分享圖片

高並發下如何投遞消息才能不丟失

大家可以考慮一下,在生產端高並發寫入 MQ 的場景下,你會面臨兩個問題:

  • 1、你每次寫一條消息到 MQ,為了等待這條消息的ack,必須把消息保存到一個存儲裏。

並且這個存儲不建議是內存,因為高並發下消息是很多的,每秒可能都幾千甚至上萬的消息投遞出去,消息的 ack 要等幾百毫秒的話,放內存可能有內存溢出的風險。

  • 2、絕對不能以同步寫消息 + 等待 ack 的方式來投遞,那樣會導致每次投遞一個消息都同步阻塞等待幾百毫秒,會導致投遞性能和吞吐量大幅度下降。

針對這兩個問題,相對應的方案其實也呼之欲出了。

首先,用來臨時存放未 ack 消息的存儲需要承載高並發寫入,而且我們不需要什麽復雜的運算操作,這種存儲首選絕對不是 MySQL 之類的數據庫,而建議采用 kv 存儲。kv 存儲承載高並發能力極強,而且 kv 操作性能很高。

其次,投遞消息之後等待 ack 的過程必須是異步的,也就是類似上面那樣的代碼,已經給出了一個初步的異步回調的方式。

消息投遞出去之後,這個投遞的線程其實就可以返回了,至於每個消息的異步回調,是通過在channel註冊一個confirm監聽器實現的。

收到一個消息 ack 之後,就從kv存儲中刪除這條臨時消息;收到一個消息 nack 之後,就從 kv 存儲提取這條消息然後重新投遞一次即可;也可以自己對 kv 存儲裏的消息做監控,如果超過一定時長沒收到 ack,就主動重發消息。

大家看看下面的圖,一起來體會一下:

技術分享圖片

消息中間件全鏈路100%數據不丟失能做到嗎?

到此為止,我們已經把生產端和消費端如何保證消息不丟失的相關技術方案結合RabbitMQ這種中間件都給大家分析過了。

其實,架構思想是通用的, 無論你用的是哪一種MQ中間件,他們提供的功能是不太一樣的,但是你都需要考慮如下幾點:

  1. 生產端如何保證投遞出去的消息不丟失:消息在半路丟失,或者在 MQ 內存中宕機導致丟失,此時你如何基於 MQ 的功能保證消息不要丟失?

  2. MQ 自身如何保證消息不丟失:起碼需要讓 MQ 對消息是有持久化到磁盤這個機制。

  3. 消費端如何保證消費到的消息不丟失:如果你處理到一半消費端宕機,導致消息丟失,此時怎麽辦?

目前來說,我們初步的借著 RabbitMQ 舉例,已經把從前到後一整套技術方案的原理、設計和實現都給大家分析了一遍了。

但是此時真的能做到100%數據不丟失嗎?恐怕未必,大家再考慮一下個特殊的場景。

生產端投遞了消息到 MQ,而且持久化到磁盤並且回傳ack給生產端了。

但是此時 MQ 還沒投遞消息給消費端,結果 MQ 部署的機器突然宕機,而且因為未知的原因磁盤損壞了,直接在物理層面導致 MQ 持久化到磁盤的數據找不回來了。

這個大家千萬別以為是開玩笑的,大家如果留意留意行業新聞,這種磁盤損壞導致數據丟失的是真的有的。

那麽此時即使你把 MQ 重啟了,磁盤上的數據也丟失了,數據是不是還是丟失了?

你說,我可以用 MQ 的集群機制啊,給一個數據做多個副本,比如後面我們就會給大家分析 RabbitMQ 的鏡像集群機制,確實可以做到數據多副本。

但是即使數據多副本,一定可以做到100%數據不丟失?

比如說你的機房突然遇到地震,結果機房裏的機器全部沒了,數據是不是還是全丟了?

說這個,並不是說要擡杠。而是告訴大家,技術這個東西,100%都是理論上的期望。

應該說,我們凡事都朝著100%去做,但是理論上是不可能完全做到100%保證的,可能就是做到99.9999%的可能性數據不丟失,但是還是有千萬分之一的概率會丟失。

當然,從實際的情況來說,能做到這種地步,其實基本上已經基本數據不會丟失了。

互聯網面試必殺:如何保證消息中間件全鏈路數據100%不丟失:第一篇
互聯網面試必殺:如何保證消息中間件全鏈路數據100%不丟失:第二篇
互聯網面試必殺:如何保證消息中間件全鏈路數據100%不丟失:第三篇

來源:【微信公眾號 - 石杉的架構筆記】

互聯網面試必殺:如何保證消息中間件全鏈路數據100%不丟失:第四篇