一次教會你如何解決RabbitMQ消息丟失問題
一、前情提示
上篇文章:《?一篇全面而且透徹的RabbitMQ性能優(yōu)化指南?!》,我們分析了RabbitMQ開啟手動ack機制保證消費端數(shù)據(jù)不丟失的時候,prefetch機制對消費者的吞吐量以及內存消耗的影響。
通過分析,我們知道了prefetch過大容易導致內存溢出,prefetch過小又會導致消費吞吐量過低,所以在實際項目中需要慎重測試和設置。
這篇文章,我們轉移到消息中間件的生產端,一起來看看如何保證投遞到MQ的數(shù)據(jù)不丟失。
如果投遞出去的消息在網(wǎng)絡傳輸過程中丟失,或者在RabbitMQ的內存中還沒寫入磁盤的時候宕機,都會導致生產端投遞到MQ的數(shù)據(jù)丟失。
而且丟失之后,生產端自己還感知不到,同時還沒辦法來補救。
下面的圖就展示了這個問題。
所以本文呢,我們就來逐步分析一下。
二、保證投遞消息不丟失的confirm機制
其實要解決這個問題,相信大家看過之前的消費端ack機制之后,也都猜到了。
很簡單,就是生產端(比如上圖的訂單服務)首先需要開啟一個confirm模式,接著投遞到MQ的消息,如果MQ一旦將消息持久化到磁盤之后,必須也要回傳一個confirm消息給生產端。
這樣的話,如果生產端的服務接收到了這個confirm消息,就知道是已經持久化到磁盤了。
否則如果沒有接收到confirm消息,那么就說明這條消息半路可能丟失了,此時你就可以重新投遞消息到MQ去,確保消息不要丟失。
而且一旦你開啟了confirm模式之后,每次消息投遞也同樣是有一個delivery tag的,也是起到唯一標識一次消息投遞的作用。
這樣,MQ回傳ack給生產端的時候,會帶上這個delivery tag。你就知道具體對應著哪一次消息投遞了,可以刪除這條消息。
此外,如果RabbitMQ接收到一條消息之后,結果內部出錯發(fā)現(xiàn)無法處理這條消息,那么他會回傳一個nack消息給生產端。此時你就會感知到這條消息可能處理有問題,你可以選擇重新再次投遞這條消息到MQ去。
或者另一種情況,如果某條消息很長時間都沒給你回傳ack/nack,那可能是極端意外情況發(fā)生了,數(shù)據(jù)也丟了,你也可以自己重新投遞消息到MQ去。
通過這套confirm機制,就可以實現(xiàn)生產端投遞消息不會丟失的效果。大家來看看下面的圖,一起來感受一下。
三、confirm機制的代碼實現(xiàn)
下面,我們再來看看confirm機制的代碼實現(xiàn):
四、confirm機制投遞消息的高延遲性
這里有一個很關鍵的點,就是一旦啟用了confirm機制投遞消息到MQ之后,MQ是不保證什么時候會給你一個ack或者nack的。
因為RabbitMQ自己內部將消息持久化到磁盤,本身就是通過異步批量的方式來進行的。
正常情況下,你投遞到RabbitMQ的消息都會先駐留在內存里,然后過了幾百毫秒的延遲時間之后,再一次性批量把多條消息持久化到磁盤里去。
這樣做,是為了兼顧高并發(fā)寫入的吞吐量和性能的,因為要是你來一條消息就寫一次磁盤,那么性能會很差,每次寫磁盤都是一次fsync強制刷入磁盤的操作,是很耗時的。
所以正是因為這個原因,你打開了confirm模式之后,很可能你投遞出去一條消息,要間隔幾百毫秒之后,MQ才會把消息寫入磁盤,接著你才會收到MQ回傳過來的ack消息,這個就是所謂confirm機制投遞消息的高延遲性。
大家看看下面的圖,一起來感受一下。
五、高并發(fā)下如何投遞消息才能不丟失
大家可以考慮一下,在生產端高并發(fā)寫入MQ的場景下,你會面臨兩個問題:
1、你每次寫一條消息到MQ,為了等待這條消息的ack,必須把消息保存到一個存儲里。
并且這個存儲不建議是內存,因為高并發(fā)下消息是很多的,每秒可能都幾千甚至上萬的消息投遞出去,消息的ack要等幾百毫秒的話,放內存可能有內存溢出的風險。
2、絕對不能以同步寫消息 + 等待ack的方式來投遞,那樣會導致每次投遞一個消息都同步阻塞等待幾百毫秒,會導致投遞性能和吞吐量大幅度下降。
針對這兩個問題,相對應的方案其實也呼之欲出了。
首先,用來臨時存放未ack消息的存儲需要承載高并發(fā)寫入,而且我們不需要什么復雜的運算操作,這種存儲首選絕對不是MySQL之類的數(shù)據(jù)庫,而建議采用kv存儲。kv存儲承載高并發(fā)能力極強,而且kv操作性能很高。
其次,投遞消息之后等待ack的過程必須是異步的,也就是類似上面那樣的代碼,已經給出了一個初步的異步回調的方式。
消息投遞出去之后,這個投遞的線程其實就可以返回了,至于每個消息的異步回調,是通過在channel注冊一個confirm監(jiān)聽器實現(xiàn)的。
收到一個消息ack之后,就從kv存儲中刪除這條臨時消息;收到一個消息nack之后,就從kv存儲提取這條消息然后重新投遞一次即可;也可以自己對kv存儲里的消息做監(jiān)控,如果超過一定時長沒收到ack,就主動重發(fā)消息。
大家看看下面的圖,一起來體會一下:
六、消息中間件全鏈路100%數(shù)據(jù)不丟失能做到嗎?
到此為止,我們已經把生產端和消費端如何保證消息不丟失的相關技術方案結合RabbitMQ這種中間件都給大家分析過了。
其實,架構思想是通用的, 無論你用的是哪一種MQ中間件,他們提供的功能是不太一樣的,但是你都需要考慮如下幾點:
?1.生產端如何保證投遞出去的消息不丟失:消息在半路丟失,或者在MQ內存中宕機導致丟失,此時你如何基于MQ的功能保證消息不要丟失?
2.MQ自身如何保證消息不丟失:起碼需要讓MQ對消息是有持久化到磁盤這個機制。
3.消費端如何保證消費到的消息不丟失:如果你處理到一半消費端宕機,導致消息丟失,此時怎么辦??
目前來說,我們初步的借著RabbitMQ舉例,已經把從前到后一整套技術方案的原理、設計和實現(xiàn)都給大家分析了一遍了。
但是此時真的能做到100%數(shù)據(jù)不丟失嗎?恐怕未必,大家再考慮一下個特殊的場景。
生產端投遞了消息到MQ,而且持久化到磁盤并且回傳ack給生產端了。
?但是此時MQ還沒投遞消息給消費端,結果MQ部署的機器突然宕機,而且因為未知的原因磁盤損壞了,直接在物理層面導致MQ持久化到磁盤的數(shù)據(jù)找不回來了。
這個大家千萬別以為是開玩笑的,大家如果留意留意行業(yè)新聞,這種磁盤損壞導致數(shù)據(jù)丟失的是真的有的。
那么此時即使你把MQ重啟了,磁盤上的數(shù)據(jù)也丟失了,數(shù)據(jù)是不是還是丟失了?
你說,我可以用MQ的集群機制啊,給一個數(shù)據(jù)做多個副本,比如后面我們就會給大家分析RabbitMQ的鏡像集群機制,確實可以做到數(shù)據(jù)多副本。
但是即使數(shù)據(jù)多副本,一定可以做到100%數(shù)據(jù)不丟失??
比如說你的機房突然遇到地震,結果機房里的機器全部沒了,數(shù)據(jù)是不是還是全丟了?
說這個,并不是說要抬杠。而是告訴大家,技術這個東西,100%都是理論上的期望。
應該說,我們凡事都朝著100%去做,但是理論上是不可能完全做到100%保證的,可能就是做到99.9999%的可能性數(shù)據(jù)不丟失,但是還是有千萬分之一的概率會丟失。
當然,從實際的情況來說,能做到這種地步,其實基本上已經基本數(shù)據(jù)不會丟失了。