《我想進大廠》之MQ奪命連環11問

科技繆繆發表於2020-09-29

繼之前的mysql奪命連環之後,我發現我這個標題被好多套用的,什麼奪命zookeeper,奪命多執行緒一大堆,這一次,開始面試題系列MQ專題,訊息佇列作為日常常見的使用中介軟體,面試也是必問的點之一,一起來看看MQ的面試題。

你們為什麼使用mq?具體的使用場景是什麼?

mq的作用很簡單,削峰填谷。以電商交易下單的場景來說,正向交易的過程可能涉及到建立訂單、扣減庫存、扣減活動預算、扣減積分等等。每個介面的耗時如果是100ms,那麼理論上整個下單的鏈路就需要耗費400ms,這個時間顯然是太長了。

如果這些操作全部同步處理的話,首先呼叫鏈路太長影響介面效能,其次分散式事務的問題很難處理,這時候像扣減預算和積分這種對實時一致性要求沒有那麼高的請求,完全就可以通過mq非同步的方式去處理了。同時,考慮到非同步帶來的不一致的問題,我們可以通過job去重試保證介面呼叫成功,而且一般公司都會有核對的平臺,比如下單成功但是未扣減積分的這種問題可以通過核對作為兜底的處理方案。

使用mq之後我們的鏈路變簡單了,同時非同步傳送訊息我們的整個系統的抗壓能力也上升了。

那你們使用什麼mq?基於什麼做的選型?

我們主要調研了幾個主流的mq,kafka、rabbitmq、rocketmq、activemq,選型我們主要基於以下幾個點去考慮:

  1. 由於我們系統的qps壓力比較大,所以效能是首要考慮的要素。
  2. 開發語言,由於我們的開發語言是java,主要是為了方便二次開發。
  3. 對於高併發的業務場景是必須的,所以需要支援分散式架構的設計。
  4. 功能全面,由於不同的業務場景,可能會用到順序訊息、事務訊息等。

基於以上幾個考慮,我們最終選擇了RocketMQ。

你上面提到非同步傳送,那訊息可靠性怎麼保證?

訊息丟失可能發生在生產者傳送訊息、MQ本身丟失訊息、消費者丟失訊息3個方面。

生產者丟失

生產者丟失訊息的可能點在於程式傳送失敗拋異常了沒有重試處理,或者傳送的過程成功但是過程中網路閃斷MQ沒收到,訊息就丟失了。

由於同步傳送的一般不會出現這樣使用方式,所以我們就不考慮同步傳送的問題,我們基於非同步傳送的場景來說。

非同步傳送分為兩個方式:非同步有回撥和非同步無回撥,無回撥的方式,生產者傳送完後不管結果可能就會造成訊息丟失,而通過非同步傳送+回撥通知+本地訊息表的形式我們就可以做出一個解決方案。以下單的場景舉例。

  1. 下單後先儲存本地資料和MQ訊息表,這時候訊息的狀態是傳送中,如果本地事務失敗,那麼下單失敗,事務回滾。
  2. 下單成功,直接返回客戶端成功,非同步傳送MQ訊息
  3. MQ回撥通知訊息傳送結果,對應更新資料庫MQ傳送狀態
  4. JOB輪詢超過一定時間(時間根據業務配置)還未傳送成功的訊息去重試
  5. 在監控平臺配置或者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沒有這些)

你說到消費者消費失敗的問題,那麼如果一直消費失敗導致訊息積壓怎麼處理?

因為考慮到時消費者消費一直出錯的問題,那麼我們可以從以下幾個角度來考慮:

  1. 消費者出錯,肯定是程式或者其他問題導致的,如果容易修復,先把問題修復,讓consumer恢復正常消費
  2. 如果時間來不及處理很麻煩,做轉發處理,寫一個臨時的consumer消費方案,先把訊息消費,然後再轉發到一個新的topic和MQ資源,這個新的topic的機器資源單獨申請,要能承載住當前積壓的訊息
  3. 處理完積壓資料後,修復consumer,去消費新的MQ和現有的MQ資料,新MQ消費完成後恢復原狀

那如果訊息積壓達到磁碟上限,訊息被刪除了怎麼辦?

這。。。他媽都刪除了我有啥辦法啊。。。冷靜,再想想。。有了。

最初,我們傳送的訊息記錄是落庫儲存了的,而轉發傳送的資料也儲存了,那麼我們就可以通過這部分資料來找到丟失的那部分資料,再單獨跑個指令碼重發就可以了。如果轉發的程式沒有落庫,那就和消費方的記錄去做對比,只是過程會更艱難一點。

說了這麼多,那你說說RocketMQ實現原理吧?

RocketMQ由NameServer註冊中心叢集、Producer生產者叢集、Consumer消費者叢集和若干Broker(RocketMQ程式)組成,它的架構原理是這樣的:

  1. Broker在啟動的時候去向所有的NameServer註冊,並保持長連線,每30s傳送一次心跳
  2. Producer在傳送訊息的時候從NameServer獲取Broker伺服器地址,根據負載均衡演算法選擇一臺伺服器來傳送訊息
  3. Conusmer消費訊息的時候同樣從NameServer獲取Broker地址,然後主動拉取訊息來消費

為什麼RocketMQ不使用Zookeeper作為註冊中心呢?

我認為有以下幾個點是不使用zookeeper的原因:

  1. 根據CAP理論,同時最多隻能滿足兩個點,而zookeeper滿足的是CP,也就是說zookeeper並不能保證服務的可用性,zookeeper在進行選舉的時候,整個選舉的時間太長,期間整個叢集都處於不可用的狀態,而這對於一個註冊中心來說肯定是不能接受的,作為服務發現來說就應該是為可用性而設計。
  2. 基於效能的考慮,NameServer本身的實現非常輕量,而且可以通過增加機器的方式水平擴充套件,增加叢集的抗壓能力,而zookeeper的寫是不可擴充套件的,而zookeeper要解決這個問題只能通過劃分領域,劃分多個zookeeper叢集來解決,首先操作起來太複雜,其次這樣還是又違反了CAP中的A的設計,導致服務之間是不連通的。
  3. 持久化的機制來帶的問題,ZooKeeper 的 ZAB 協議對每一個寫請求,會在每個 ZooKeeper 節點上保持寫一個事務日誌,同時再加上定期的將記憶體資料映象(Snapshot)到磁碟來保證資料的一致性和永續性,而對於一個簡單的服務發現的場景來說,這其實沒有太大的必要,這個實現方案太重了。而且本身儲存的資料應該是高度定製化的。
  4. 訊息傳送應該弱依賴註冊中心,而RocketMQ的設計理念也正是基於此,生產者在第一次傳送訊息的時候從NameServer獲取到Broker地址後快取到本地,如果NameServer整個叢集不可用,短時間內對於生產者和消費者並不會產生太大影響。

那Broker是怎麼儲存資料的呢?

RocketMQ主要的儲存檔案包括commitlog檔案、consumequeue檔案、indexfile檔案。

Broker在收到訊息之後,會把訊息儲存到commitlog的檔案當中,而同時在分散式的儲存當中,每個broker都會儲存一部分topic的資料,同時,每個topic對應的messagequeue下都會生成consumequeue檔案用於儲存commitlog的物理位置偏移量offset,indexfile中會儲存key和offset的對應關係。

CommitLog檔案儲存於${Rocket_Home}/store/commitlog目錄中,從圖中我們可以明顯看出來檔名的偏移量,每個檔案預設1G,寫滿後自動生成一個新的檔案。

由於同一個topic的訊息並不是連續的儲存在commitlog中,消費者如果直接從commitlog獲取訊息效率非常低,所以通過consumequeue儲存commitlog中訊息的偏移量的實體地址,這樣消費者在消費的時候先從consumequeue中根據偏移量定位到具體的commitlog物理檔案,然後根據一定的規則(offset和檔案大小取模)在commitlog中快速定位。

Master和Slave之間是怎麼同步資料的呢?

而訊息在master和slave之間的同步是根據raft協議來進行的:

  1. 在broker收到訊息後,會被標記為uncommitted狀態
  2. 然後會把訊息傳送給所有的slave
  3. slave在收到訊息之後返回ack響應給master
  4. master在收到超過半數的ack之後,把訊息標記為committed
  5. 傳送committed訊息給所有slave,slave也修改狀態為committed

你知道RocketMQ為什麼速度快嗎?

是因為使用了順序儲存、Page Cache和非同步刷盤。

  1. 我們在寫入commitlog的時候是順序寫入的,這樣比隨機寫入的效能就會提高很多
  2. 寫入commitlog的時候並不是直接寫入磁碟,而是先寫入作業系統的PageCache
  3. 最後由作業系統非同步將快取中的資料刷到磁碟

什麼是事務、半事務訊息?怎麼實現的?

事務訊息就是MQ提供的類似XA的分散式事務能力,通過事務訊息可以達到分散式事務的最終一致性。

半事務訊息就是MQ收到了生產者的訊息,但是沒有收到二次確認,不能投遞的訊息。

實現原理如下:

  1. 生產者先傳送一條半事務訊息到MQ
  2. MQ收到訊息後返回ack確認
  3. 生產者開始執行本地事務
  4. 如果事務執行成功傳送commit到MQ,失敗傳送rollback
  5. 如果MQ長時間未收到生產者的二次確認commit或者rollback,MQ對生產者發起訊息回查
  6. 生產者查詢事務執行最終狀態
  7. 根據查詢事務狀態再次提交二次確認

最終,如果MQ收到二次確認commit,就可以把訊息投遞給消費者,反之如果是rollback,訊息會儲存下來並且在3天后被刪除。

- END -

相關文章