MQ 面試題
為什么要使用MQ
(1)解耦:A 系統發送數據到 BCD 三個系統,通過接口調用發送。如果 E 系統也要這個數據呢?那如果 C 系統現在不需要了呢?A 系統負責人幾乎崩潰......A 系統跟其它各種亂七八糟的系統嚴重耦合,A 系統產生一條比較關鍵的數據,很多系統都需要 A 系統將這個數據發送過來。如果使用 MQ,A 系統產生一條數據,發送到 MQ 里面去,哪個系統需要數據自己去 MQ 里面消費。如果新系統需要數據,直接從 MQ 里消費即可;如果某個系統不需要這條數據了,就取消對 MQ 消息的消費即可。這樣下來,A 系統壓根兒不需要去考慮要給誰發送數據,不需要維護這個代碼,也不需要考慮人家是否調用成功、失敗超時等情況。
就是一個系統或者一個模塊,調用了多個系統或者模塊,互相之間的調用很復雜,維護起來很麻煩。但是其實這個調用是不需要直接同步調用接口的,如果用 MQ 給它異步化解耦。
(2)異步:A 系統接收一個請求,需要在自己本地寫庫,還需要在 BCD 三個系統寫庫,自己本地寫庫要 3ms,BCD 三個系統分別寫庫要 300ms、450ms、200ms。最終請求總延時是 3 + 300 + 450 + 200 = 953ms,接近 1s,用戶感覺搞個什么東西,慢死了慢死了。用戶通過瀏覽器發起請求。如果使用 MQ,那么 A 系統連續發送 3 條消息到 MQ 隊列中,假如耗時 5ms,A 系統從接受一個請求到返回響應給用戶,總時長是 3 + 5 = 8ms。
(3)削峰:減少高峰時期對服務器壓力。
那你們使用什么MQ?基於什么做的選型?
我們主要調研了幾個主流的mq,kafka、rabbitmq、rocketmq、activemq,選型我們主要基於以下幾個點去考慮:
- 由於我們系統的qps壓力比較大,所以性能是首要考慮的要素。
- 開發語言,由於我們的開發語言是java,主要是為了方便二次開發。
- 對於高並發的業務場景是必須的,所以需要支持分布式架構的設計。
- 功能全面,由於不同的業務場景,可能會用到順序消息、事務消息等。
基於以上幾個考慮,我們最終選擇了RocketMQ。
你上面提到異步發送,那消息可靠性怎么保證?
消息丟失可能發生在生產者發送消息、MQ本身丟失消息、消費者丟失消息3個方面。
生產者丟失
生產者丟失消息的可能點在於程序發送失敗拋異常了沒有重試處理,或者發送的過程成功但是過程中網絡閃斷MQ沒收到,消息就丟失了。
由於同步發送的一般不會出現這樣使用方式,所以我們就不考慮同步發送的問題,我們基於異步發送的場景來說。
異步發送分為兩個方式:異步有回調和異步無回調,無回調的方式,生產者發送完后不管結果可能就會造成消息丟失,而通過異步發送+回調通知+本地消息表的形式我們就可以做出一個解決方案。以下單的場景舉例。
下單后先保存本地數據和MQ消息表,這時候消息的狀態是發送中,如果本地事務失敗,那么下單失敗,事務回滾。
下單成功,直接返回客戶端成功,異步發送MQ消息
MQ回調通知消息發送結果,對應更新數據庫MQ發送狀態
JOB輪詢超過一定時間(時間根據業務配置)還未發送成功的消息去重試
在監控平台配置或者JOB程序處理超過一定次數一直發送不成功的消息,告警,人工介入。
一般而言,對於大部分場景來說異步回調的形式就可以了,只有那種需要完全保證不能丟失消息的場景我們做一套完整的解決方案。
MQ 丟失
如果生產者保證消息發送到MQ,而MQ收到消息后還在內存中,這時候宕機了又沒來得及同步給從節點,就有可能導致消息丟失。
比如RocketMQ:
RocketMQ分為同步刷盤和異步刷盤兩種方式,默認的是異步刷盤,就有可能導致消息還未刷到硬盤上就丟失了,可以通過設置為同步刷盤的方式來保證消息可靠性,這樣即使MQ掛了,恢復的時候也可以從磁盤中去恢復消息。
比如Kafka也可以通過配置做到:
acks=all 只有參與復制的所有節點全部收到消息,才返回生產者成功。這樣的話除非所有的節點都掛了,消息才會丟失。
replication.factor=N,設置大於1的數,這會要求每個partion至少有2個副本
min.insync.replicas=N,設置大於1的數,這會要求leader至少感知到一個follower還保持着連接
retries=N,設置一個非常大的值,讓生產者發送失敗一直重試
雖然我們可以通過配置的方式來達到MQ本身高可用的目的,但是都對性能有損耗,怎樣配置需要根據業務做出權衡。
消費者丟失
消費者丟失消息的場景:消費者剛收到消息,此時服務器宕機,MQ認為消費者已經消費,不會重復發送消息,消息丟失。
RocketMQ默認是需要消費者回復ack確認,而kafka需要手動開啟配置關閉自動offset。
消費方不返回ack確認,重發的機制根據MQ類型的不同發送時間間隔、次數都不盡相同,如果重試超過次數之后會進入死信隊列,需要手工來處理了。(Kafka沒有這些)
消費失敗導致消息積壓怎么處理?
因為考慮到時消費者消費一直出錯的問題,那么我們可以從以下幾個角度來考慮:
- 消費者出錯,肯定是程序或者其他問題導致的,如果容易修復,先把問題修復,讓consumer恢復正常消費
- 如果時間來不及處理很麻煩,做轉發處理,寫一個臨時的consumer消費方案,先把消息消費,然后再轉發到一個新的topic和MQ資源,這個新的topic的機器資源單獨申請,要能承載住當前積壓的消息
- 處理完積壓數據后,修復consumer,去消費新的MQ和現有的MQ數據,新MQ消費完成后恢復原狀
RocketMQ 實現原理
RocketMQ由NameServer注冊中心集群、Producer生產者集群、Consumer消費者集群和若干Broker(RocketMQ進程)組成,它的架構原理是這樣的:
- Broker在啟動的時候去向所有的NameServer注冊,並保持長連接,每30s發送一次心跳
- Producer在發送消息的時候從NameServer獲取Broker服務器地址,根據負載均衡算法選擇一台服務器來發送消息
- Conusmer消費消息的時候同樣從NameServer獲取Broker地址,然后主動拉取消息來消費
如何保證消息不會重復消費?
設置唯一主鍵,拿到id之后與數據庫進行比對,如果已存在則丟棄消息。
RocketMQ的消費模式
集群模式與廣播模式
RocketMQ 是基於發布訂閱模型的消息中間件。所謂的發布訂閱就是說,consumer 訂閱了 broker 上的某個 topic,當 producer 發布消息到 broker 上的該 topic 時,consumer 就能收到該條消息。
之前我們講過 consumer group 的概念,即消費同一類消息的多個 consumer 實例組成一個消費者組,也可以稱為一個 consumer 集群,這些 consumer 實例使用同一個 group name。需要注意一點,除了使用同一個 group name,訂閱的 tag 也必須是一樣的,只有符合這兩個條件的 consumer 實例才能組成 consumer 集群。
集群消費
當 consumer 使用集群消費時,每條消息只會被 consumer 集群內的任意一個 consumer 實例消費一次。舉個例子,當一個 consumer 集群內有 3 個consumer 實例(假設為consumer 1、consumer 2、consumer 3)時,一條消息投遞過來,只會被consumer 1、consumer 2、consumer 3中的一個消費。
同時記住一點,使用集群消費的時候,consumer 的消費進度是存儲在 broker 上,consumer 自身是不存儲消費進度的。消息進度存儲在 broker 上的好處在於,當你 consumer 集群是擴大或者縮小時,由於消費進度統一在broker上,消息重復的概率會被大大降低了。
注意:在集群消費模式下,並不能保證每一次消息失敗重投都投遞到同一個 consumer 實例。
廣播消費
當 consumer 使用廣播消費時,每條消息都會被 consumer 集群內所有的 consumer 實例消費一次,也就是說每條消息至少被每一個 consumer 實例消費一次。舉個例子,當一個 consumer 集群內有 3 個 consumer 實例(假設為 consumer 1、consumer 2、consumer 3)時,一條消息投遞過來,會被 consumer 1、consumer 2、consumer 3都消費一次。
與集群消費不同的是,consumer 的消費進度是存儲在各個 consumer 實例上,這就容易造成消息重復。還有很重要的一點,對於廣播消費來說,是不會進行消費失敗重投的,所以在 consumer 端消費邏輯處理時,需要額外關注消費失敗的情況。
雖然廣播消費能保證集群內每個 consumer 實例都能消費消息,但是消費進度的維護、不具備消息重投的機制大大影響了實際的使用。因此,在實際使用中,更推薦使用集群消費,因為集群消費不僅擁有消費進度存儲的可靠性,還具有消息重投的機制。而且,我們通過集群消費也可以達到廣播消費的效果。
使用集群消費模擬廣播消費
如果業務上確實需要使用廣播消費,那么我們可以通過創建多個 consumer 實例,每個 consumer 實例屬於不同的 consumer group,但是它們都訂閱同一個 topic。
舉個例子,我們創建 3 個 consumer 實例,consumer 1(屬於consumer group 1)、consumer 2(屬於 consumer group 2)、consumer 3(屬於consumer group 3),它們都訂閱了 topic A ,那么當 producer 發送一條消息到 topic A 上時,由於 3 個consumer 屬於不同的 consumer group,所以 3 個consumer都能收到消息,也就達到了廣播消費的效果了。
除此之外,每個 consumer 實例的消費邏輯可以一樣也可以不一樣,每個consumer group還可以根據需要增加 consumer 實例,比起廣播消費來說更加靈活。