訊息佇列常見問題分析

九卷發表於2020-09-25

一、簡介

很久以前也寫過一篇關於訊息佇列的文章,這裡的文章,這篇文章是對訊息佇列使用場景,以及一些模型做過一點介紹。

這篇文章將分析訊息佇列常見問題。

訊息佇列:利用高效可靠的訊息傳遞機制進行與平臺無關的資料交流,並基於資料通訊來進行分散式系統整合。

從定義看:它是一種資料交流平臺,也是資料通訊平臺。
然而,資料通訊我們可以用http,RPC來進行通訊,這些與訊息佇列有什麼區別呢?
最大的區別就是同步和非同步。http和RPC一般都是同步,而訊息佇列是非同步。

二、為什麼要用訊息佇列

1.解耦
雙方不在基於對方直接通訊了,而是基於訊息佇列來通訊,通過MQ解耦了客戶端和服務端通訊。處理資料的雙方關注的點不同了,比如說一個事務,我們只關心核心流程,而需要依賴其他系統但不是那麼重要的事情,有通知即可,不需要等待結果。這種訊息模型,關心的是通知,而不在意處理過程。也可以用訊息佇列。
上下游開發人員也可以基於訊息佇列傳送訊息,而不需要同步的處理訊息了。

2.非同步處理
傳統的業務邏輯都是基於同步的方式進行處理的。而有了訊息佇列,就可以把訊息存放在MQ裡,訊息佇列的消費者就可以從訊息佇列中獲取資料並進行處理。它不一定要實時處理,可以隔幾分鐘處理訊息佇列裡的資料。

3.削峰和流控
這裡有點像計算機中的硬體,比如CPU和記憶體,CPU運算速度比記憶體高N個數量級,那怎麼才能緩解兩者之間的差異?中間加一個快取來緩解兩者速度的差異。
同理,MQ也可以起到這種作用。對於上下游軟體不同的處理速度的差異進行調節。

比如,我們常見的秒殺應用,前端瞬間湧入成千上萬的請求,前端可以承受這麼大的請求壓力,但是複雜的後端系統,肯定會被壓垮,從而導致秒殺服務不可以用的情況。為了解決這種前後端處理速度不平衡的差異,導致的服務問題,可以引入訊息佇列來調節,用訊息佇列來快取使用者的請求,等待後端系統來消費。

上面就是訊息佇列的主要功能,當然還有其他一些功能,比如訊息廣播,最終一致性等。

使用MQ後的問題

當然使用了訊息佇列,會增加系統的複雜性,一致性延遲,可用性降低等問題。
可用性降低是指系統可用性降低,如果MQ掛了,那麼肯定會影響到整個系統了。
因為上下游系統可能都會與MQ互動。

三、什麼時候引入MQ?

這個要看業務系統功能需求,一個是系統處理是否到達了瓶頸,需要訊息佇列來緩解;
還有,業務系統一致性要求是不是特別高。通常業務系統不會要求那麼高的一致性要求。當然一些高頻交易系統,一致性要求特別高,就不適合用了。

引入任何一個新的軟體必然會增加原有系統的複雜性,還是要根據業務特性進行合理的選擇。

四、訊息佇列常見問題

1.如何保證訊息不被重複消費(怎麼保證冪等)

為什麼會重複消費

  • 生產者:也就是客戶端,可能會重複推送一條資料到MQ中。有可能是客戶端超時重複推送,也有可能是網路比較慢客戶端重複推送了資料到MQ中。
  • MQ:消費者消費完了一條資料,傳送ACK資訊表示消費成功時,這時候,MQ突然掛了,導致MQ以為消費者還未消費該條訊息,MQ恢復後再次推送了該條訊息,導致重複消費。
  • 消費者:與上面MQ掛掉情況類似,消費者已經消費完了一條訊息,正準備給MQ傳送ACK訊息但還未傳送時,這時候消費者掛了,服務重啟後MQ以為消費者還沒有消費該條訊息,再次推送該條訊息。

怎麼處理重複消費

每個訊息都帶一個唯一的訊息id。消費端保證不重複消費就可以了,即使生產端產生了重複的資料,當然生產端也最好控制下重複資料。

消費端保證不重複消費:
通常方法都是儲存消費了的訊息,然後判斷訊息是否存在。

1.先儲存在查詢
每次儲存資料前,先查詢下,不存在就插入。這種是併發不高的情況下可以使用。

2.資料庫新增唯一約束條件
比如唯一索引

3.增加一個訊息表
已經消費的訊息,把訊息id插入到訊息表裡面。
為了保證高併發,訊息表可以用Redis來存。

2.如何處理訊息丟失的問題

訊息丟失的原因

  • 生產者:生產者推送訊息到MQ中,但是網路出現了故障,比如網路超時,網路抖動,導致訊息沒有推送到MQ中,在網路中丟失了。又或者推送到MQ中了,但是這時候MQ內部出錯導致訊息丟失。

  • MQ:MQ自己內部發生了錯誤,導致訊息丟失。

  • 消費者:有時處理訊息的消費者處理不當,還沒等訊息處理完,就給MQ傳送確認資訊,但是這時候消費者自身出問題,掛了,確認訊息已經傳送給MQ告訴MQ自己已經消費完了,導致訊息丟失。

如何保證訊息不丟失呢? 下面談談這方面的做法。

3.如何保證訊息可靠性傳輸

整個訊息從生產到消費一般分為三個階段:生產者-生產階段,MQ-儲存階段,消費者-消費階段

3.1 生產者-生產階段
在這個階段,一般通過請求確認機制,來保證訊息可靠性傳輸。 與TCP/IP協議裡ACK機制有點像。
客戶端傳送訊息到訊息佇列,訊息佇列給客戶端一個確認響應,表示訊息已經收到,客戶端收到響應,表示一次正常訊息傳送完畢。

3.2 MQ-儲存階段
訊息佇列給客戶端傳送確認訊息。儲存完成後,才傳送確認訊息。

3.3 消費者-消費階段
跟生產階段相同,消費完了,給訊息佇列傳送確認訊息。

4.如何保證訊息的順序性

我們日常說的順序性是什麼呢?

比如說小孩早上上學過程,他先起床,然後洗漱,吃早餐,最後上學。我們認為他做的事情是有先後順序的,及是時間的先後順序,我們用時間來標記他的順序。
更抽象的理解,這些發生的事件有一個相同的參考系,即他們的時間是對應同一個物理時鐘的時間。

如果沒有絕對的時間作為參考系,那他們之間還能確定順序嗎?
如果事件之間有因果關係,比如A、B兩個事件是因果關係,那麼A一定發生在B之前(前應後果)。相反,在沒有一個絕對的時間的參考的情況下,若A、B之間沒有因果關係,那麼A、B之間就沒有順序關係。跟java裡的happen before很像。

總結一下,我們說順序時,其實說的是

  • 在有絕對時間作為參考系的情況下,事件發生的時間先後關係;
  • 在沒有絕對時間作為參考系的情況下,一種由因果關係推斷出來的happening before的關係;

在分散式系統領域,有一篇關於時間,時鐘和事件的順序的很有名的一篇論文
Time, Clocks, and the Ordering of Events in a Distributed System
,可以看一看,上面舉例情況都是參考這篇論文。

參考上面的結論,在訊息佇列中,我們也是以時間作為參考系,讓訊息有序。

但是,在訊息佇列中,訊息有序會遇到一些問題,下面讓我們來討論這些問題。

訊息的順序性的一些問題

在計算機系統中,有一個比較棘手的問題是,它可以是多執行緒執行的,而且哪個執行緒先執行,哪個執行緒後執行,完全是由作業系統決定的,完全沒有規律,是亂序執行。顯然與訊息佇列中的訊息有序相悖。

還有,在訊息佇列中,涉及到生產者,MQ,消費者,還有網路,這4者之間的關係。然後他們又涉及到訊息的順序性,就有很多種情況需要考慮。可以參考這篇文章
分散式開放訊息系統(RocketMQ)的原理與實踐
(作者:CHUAN.CHEN),各種情況討論的很全面。

最後的結論就是:訊息的順序性,不僅僅是MQ本身儲存訊息要保證順序性,還需要生產者和消費者一同來保證順序性。

順序性保證

在訊息佇列中,訊息的順序性需要3方面來保證:
1、生產者傳送訊息時要保證順序
2、訊息被訊息佇列儲存時要保持和傳送的順序一致
3、訊息被消費時保持和儲存的順序一致

生產者:傳送時要求使用者在同一個執行緒中採用同步的方式傳送。
訊息佇列:儲存保持和傳送的順序一致。一般是在一個分割槽中保持順序性。
消費者:一個分割槽的訊息由一個執行緒來處理消費訊息。

https://www.hicsc.com/post/2020041566 這個連結中,作者分析了RocketMQ順序訊息的程式碼實現。

5.訊息佇列中訊息延遲問題

你說的 訊息的延遲 是延遲訊息佇列嗎? 啊,並不是,是完全2個不同的概念。延遲訊息佇列是MQ提供的一個功能。訊息的延遲,是指消費端消費的速度跟不上生產端產生訊息的速度,可能導致消費端丟失資料,也可能導致訊息積壓在MQ中。所以這裡說的訊息的延遲,指的是消費端消費訊息的延遲。

訊息佇列的消費模型pull和push:

1、push模式

這種模式是訊息佇列主動將訊息推送給消費者。

  • 優點:儘可能實時的將訊息傳送給消費者進行消費。
  • 缺點:如果消費端消費能力弱,消費端的消費速度趕不上生產端,而MQ又不斷的給消費端推送訊息,消費端的快取滿了導致快取溢位,就會產生錯誤或丟失資料的可能。
2、pull模式

這種模式是由消費端主動向訊息佇列拉取訊息。

  • 優點:可以自主可控的拉取訊息。
  • 缺點:拉取訊息的頻率不好控制。

a、如果每次pull時間間隔比較久,會增加訊息延遲,訊息到達消費者時間會加長。這樣時間一長會導致MQ中訊息的堆積,而訊息長時間堆積就會導致一系列的問題:

  • 1、如果積壓了幾個小時的資料,有幾千萬的資料量,消費端處理的壓力會越來越大。
  • 2、如果是帶有過期時間的訊息,可能這些訊息已經到了過期時間,因為積壓時間太長,但還沒被消費端消費掉,消費端來不及消費。
  • 3、如果持續的積壓,達到了MQ能儲存訊息數量的上限,也就是說MQ滿了,存不下了,會導致MQ丟掉資料,導致資料丟失。
    想一下,上面的情形是不是跟TCP/IP協議的流量控制和擁塞控制遇到的一些問題很像,也有很多不同。

b、如果每次pull的時間間隔比較短,在一段時間內MQ中沒有可消費的訊息,會產生很多無效的pull請求,導致一定的網路開銷。

所以解決問題的辦法最主要就是優化消費端的消費效能。1.優化消費邏輯 2.水平擴容,增加消費端併發。

延遲問題處理

如果訊息堆積已經發生了,導致了上面的3個問題,這時怎麼辦?
1、積壓了幾個小時幾千萬的資料
第一:肯定要找到積壓資料的原因,一般都是消費端的問題。
第二:如果可以的,擴大消費端的數量,快速消費掉訊息。
第三:擴容,增加多機器消費。新建一個topic,partition是原來10倍,建立原先10倍的queue。然後寫一個臨時的消費程式,這個消費程式去轉移積壓的資料,把積壓的資料均勻輪詢寫入建立好的10倍數量的queue。然後在徵用10倍機器的消費端來消費這個queue。這種做法相當於臨時將 queue 資源和 consumer 資源擴大 10 倍,以正常的 10 倍速度來消費資料。消費完了,恢復原來的部署。這是大廠做法。

2、積壓時間過長,帶有過期時間的訊息過期失效了
這個沒有好的辦法處理,只能通過程式找出丟失的資料,然後也是通過程式把丟失的資料重新匯入到MQ裡,重新消費。

3、長時間積壓倒是MQ寫滿了
這個也沒啥好辦法處理,只能快速消費掉MQ裡的資料,快速消費指消費一個,丟掉一個,不要這些資料了,然後重新匯入資料。使用者少的時候在補回資料。

6.訊息佇列高可用

6.1 kafka

kafka基本架構:

  • Broker:一個kafka節點就是一個broker,多個broker組成一個kafka叢集。一個broker可以是一個單機器kafka伺服器。
  • Topic:存放訊息的主題,相當於一個佇列。可以理解為存放訊息的分類,比如你可以有前端日誌的Topic,後端日誌的Topic。可以理解為MySQL裡的表。
  • Partition:一個topic可以劃分為多個partition,每個partition都是一個有序佇列。把topic主題中的訊息進行分拆,均攤到kafka叢集中不同機器上。partition是topic的進一步拆分。
  • Replica:副本訊息。kafka可以以partition為單位,儲存多個副本,分散在不同的broker上。副本數是可以設定的。
  • Segment: 一個Partition被切分為多個Segment,每個Segment包含索引檔案和資料檔案。
  • Message:kafka裡最基本訊息單元。

一個kafka叢集可以由多個broker組成,每個broker是一個節點,你建立一個topic,這個topic可以劃分為多個partition,每個partition可以儲存在不同的broker上,每個partition存放一部分資料。

6.2 RocketMQ

在 RocketMQ 4.5 版本之前,RocketMQ 只有 Master/Slave 一種部署方式來實現高可用。
一組 Broker 中有一個 Master,有零到多個 Slave,Slave 通過同步複製或非同步複製方式去同步 Master 的資料。Master/Slave 部署模式,提供了一定的高可用性。

上面主從高可用架構有一個缺點:
主節點掛了後需要人為的進行重啟或者切換。為了解決這個問題,後續引入了raft,用raft協議來完成自動選主。RocketMQ的DLedger 就是一個基於 raft 協議的 commitlog 儲存庫,也是 RocketMQ 實現新的高可用多副本架構的關鍵。

還可以多master多slave部署,防止單點故障。

五、參考

相關文章