恕我直言,90% 的程序員都扛不住這波 MQ 的面試四連炮!

概述

大家平時也有用到一些消息中間件(MQ),但是可能對於 MQ 的理解僅僅停留在會使用 API 能實現生產消息、消費消息就完事了。可能很多人都沒有對 MQ 的一些問題思考過。

比如,你跳槽面試時,如果面試官看到你簡歷上寫了,熟練掌握消息中間件,那麼很可能給你發起如下 4 個面試連環炮!

  • 爲什麼要使用 MQ?
  • 使用了 MQ 之後有什麼優缺點?
  • 怎麼保證 MQ 消息不丟失?
  • 怎麼保證 MQ 的高可用性?

本 Chat 將通過一些場景,配合着通俗易懂的語言和多張手繪彩圖,討論一下這些問題。

爲什麼要使用 MQ?

相信大家也聽過這樣的一句話:好的架構不是設計出來的,是演進出來的。

這句話在引入 MQ 的場景同樣適用,使用 MQ 必定有其道理,是用來解決實際問題的。而不是胡亂看見別人用了,我也用着玩兒一下。

其實使用 MQ 的場景有挺多的,但是比較核心的有 3 個:異步、解耦、削峯填谷。

異步

假設 A 系統接收一個請求,需要在自己本地寫庫執行 SQL,接着需要調用 BCD 三個系統的接口。自己本地寫庫要 3ms,調用 BCD 三個系統分別要 300ms、450ms、200ms。最終請求總延時是 3 + 300 + 450 + 200 = 953ms,接近 1s,可能用戶會感覺太慢了。

此時整個系統大概是這樣的: 但是一旦使用了 MQ 之後,系統 A 只需要發送 3 條消息到 MQ 中的 3 個消息隊列,然後就返回給用戶了,假設發送消息到 MQ 中耗時 20ms,那麼用戶感知到這個接口的耗時僅僅是 20 + 3 = 23ms,用戶幾乎無感知,倍兒爽。

此時整個系統結構大概是這樣的: 可以看到通過 MQ 的異步功能,可以大大提高接口的性能。

解耦

假設 A 系統在用戶發生某個操作的時候,需要把用戶提交的數據同時推送到 B、C 兩個系統的時候。這個時候負責 A 系統的哥們想:沒事啊,B、C 兩個系統給我提供一個 Http 接口或者 RPC 接口,我把數據推送過去不就完事了嗎。負責 A 系統的哥們美滋滋。 一切看起來很美好,但是,隨着業務快速迭代,這個時候系統 D 和系統 E 也想要這個數據,A 系統的開發同學就改咯,在發送數據給 BC 的同時加上一個 E,但是後面麻煩來了。

整個系統好像不止這個數據要發送給 BCD、還有第二、第三個數據要發送給 BCD、甚至有時候又加入了 E、F 等等系統也要這個數據,有時候可能 B 系統突然又不要這個數據了,A 系統改來改去,開發同學頭皮發麻了。

更復雜的場景是數據通過接口傳給其他系統有時候還要考慮重試、超時等一些異常情況,頭髮都白了呀。。。

來感受一下現場: 在這種情況下,使用 MQ 來解耦是在合適不過了,因爲負責 A 系統的哥們只需要把消息扔到 MQ 就行了,其他系統按需來訂閱消息就好了,就算某個系統不需要這個數據了,也不會需要 A 系統改動代碼: 這樣一來,負責 A 系統的哥們神清氣爽了。

削峯填谷

比如我們的訂單系統,在下單的時候就會往數據庫寫數據,但是數據庫只能支撐每秒 1000 左右的併發寫入,併發量再高就容易宕機。低峯期在的時候併發也就 100 多個,但是在高峯期時候,併發量會突然激增到 5000 以上,這個時候數據庫肯定死了。

來感受一下數據庫被打死的絕望: 但是使用了 MQ 之後,消息就被 MQ 保存起來了,然後系統就可以按照自己的消費能力來消費,比如每秒 1000 個數據,這樣慢慢寫入數據庫,這樣就不會打死數據庫了: 至於爲什麼叫做削峯填谷呢?來看看這個圖

如果沒有用 MQ 的情況下併發量高峯期的時候是有一個“頂峯”的,然後高峯期過後又是一個低併發的“谷”。

但是使用了 MQ 之後,限制消費消息的速度爲 1000,但是這樣一來,高峯期產生的數據勢必會被積壓在 MQ 中,但是高峯就被“削”掉了,但是因爲消息積壓,在高峯期過後的一段時間內,消費消息的速度還是會維持在 1000QPS,直到消費完積壓的消息,這就叫做“填谷”。

通過上面的分析,知道了爲什麼要使用 MQ,以及使用了 MQ 有什麼好處。知其所以然,明白了自己的系統爲什麼要使用 MQ。

就不會出現“我們組長要用 MQ 我們就用了”這樣的事情了。

使用了 MQ 之後有什麼優缺點?

看到這個問題蒙圈了,用了 MQ 就用了 MQ,優點上面已經說了,但是這個缺點是啥啊。好像沒啥缺點啊。

如果你這樣想,就大錯特錯了,在設計系統的過程中,除了要清楚的知道爲什麼要用這個東西,還要思考一下用了之後有什麼壞處。這樣才能心裏有底,防範於未然。

接下來我們就討論一下用 MQ 會有什麼缺點吧?

系統可用性降低

大家想想一下,上面的說解耦的場景,本來 A 系統的哥們要把系統關鍵數據發送給 BC 系統的,現在突然加入了一個 MQ 了,BC 系統接收數據要通過 MQ 來接收,但是大家有沒有考慮過一個問題,萬一 MQ 掛了怎麼辦?這就引出一個問題,加入了 MQ 之後,整體系統的可用性是不是就降低了?因爲多了一個風險因素:MQ 可能會掛掉。只要 MQ 掛了,數據沒了,系統運行就不對了。

系統複雜度提高

本來我的系統通過接口調用一下就能完事的,但是加入一個 MQ 之後,需要考慮消息重複消費、消息丟失、甚至消息順序性的問題,爲了解決這些問題,有需要引入很多複雜的機制,這樣一來是不是系統的複雜度提高了。

數據一致性問題

本來好好的,A 系統調用 BC 系統接口,如果 BC 系統出錯了,會拋出異常,返回給 A 系統,讓 A 系統知道,這樣的話就可以做回滾操作了,但是使用了 MQ 之後,A 系統發送完消息就完事了,就認爲成功了。但是好死不死,剛好 C 系統寫數據庫的時候失敗了,但是 A 認爲 C 已經成功了?這樣一來數據就不一致了。

通過分析引入 MQ 的優缺點之後,就明白了使用 MQ 有很多優點,但是會發現它帶來的缺點又會需要你做各種額外的系統設計來彌補,最後你可能會發現整個系統複雜了好幾倍,所以設計系統的時候要基於這些考慮做出取捨,很多時候你會發現該用的還是要用的。

怎麼保證 MQ 消息不丟失?

使用了 MQ 之後,還要關心消息丟失的問題。這裏我們挑 RabbitMQ 來說明一下吧。

生產者弄丟了數據

RabbitMQ 生產者將數據發送到 rabbitmq 的時候,可能數據在網絡傳輸中搞丟了,這個時候 RabbitMQ 收不到消息,消息就丟了。

RabbitMQ 提供了兩種方式來解決這個問題:

事務方式:

在生產者發送消息之前,通過 channel.txSelect 開啓一個事務,接着發送消息,如果消息沒有成功被 RabbitMQ 接收到,生產者會收到異常,此時就可以進行事務回滾 channel.txRollback 然後重新發送。假如 RabbitMQ 收到了這個消息,就可以提交事務 channel.txCommit。

但是這樣一來,生產者的吞吐量和性能都會降低很多,現在一般不這麼幹。

另外一種方式就是通過 confirm 機制:

這個 confirm 模式是在生產者哪裏設置的,就是每次寫消息的時候會分配一個唯一的 id,然後 RabbitMQ 收到之後會回傳一個 ack,告訴生產者這個消息 ok 了。如果 rabbitmq 沒有處理到這個消息,那麼就回調一個 nack 的接口,這個時候生產者就可以重發。

事務機制和 cnofirm 機制最大的不同在於事務機制是同步的,提交一個事務之後會阻塞在那兒,但是 confirm 機制是異步的,發送一個消息之後就可以發送下一個消息,然後那個消息 rabbitmq 接收了之後會異步回調你一個接口通知你這個消息接收到了。

所以一般在生產者這塊避免數據丟失,都是用 confirm 機制的。

Rabbitmq 弄丟了數據

RabbitMQ 集羣也會弄丟消息,這個問題在官方文檔的教程中也提到過,就是說在消息發送到 RabbitMQ 之後,默認是沒有落地磁盤的,萬一 RabbitMQ 宕機了,這個時候消息就丟失了。

所以爲了解決這個問題,RabbitMQ 提供了一個持久化的機制,消息寫入之後會持久化到磁盤,哪怕是宕機了,恢復之後也會自動恢復之前存儲的數據,這樣的機制可以確保消息不會丟失。

設置持久化有兩個步驟:

  • 第一個是創建 queue 的時候將其設置爲持久化的,這樣就可以保證 rabbitmq 持久化 queue 的元數據,但是不會持久化 queue 裏的數據;
  • 第二個是發送消息的時候將消息的 deliveryMode 設置爲 2,就是將消息設置爲持久化的,此時 rabbitmq 就會將消息持久化到磁盤上去。

但是這樣一來可能會有人說:萬一消息發送到 RabbitMQ 之後,還沒來得及持久化到磁盤就掛掉了,數據也丟失了。

對於這個問題,其實是配合上面的 confirm 機制一起來保證的,就是在消息持久化到磁盤之後纔會給生產者發送 ack 消息。萬一真的遇到了那種極端的情況,生產者是可以感知到的,此時生產者可以通過重試發送消息給別的 RabbitMQ 節點

消費端弄丟了數據

RabbitMQ 消費端弄丟了數據的情況是這樣的:在消費消息的時候,剛拿到消息,結果進程掛了,這個時候 RabbitMQ 就會認爲你已經消費成功了,這條數據就丟了。

對於這個問題,要先說明一下 RabbitMQ 消費消息的機制:在消費者收到消息的時候,會發送一個 ack 給 RabbitMQ,告訴 RabbitMQ 這條消息被消費到了,這樣 RabbitMQ 就會把消息刪除。

但是默認情況下這個發送 ack 的操作是自動提交的,也就是說消費者一收到這個消息就會自動返回 ack 給 RabbitMQ,所以會出現丟消息的問題。

所以針對這個問題的解決方案就是:關閉 RabbitMQ 消費者的自動提交 ack,在消費者處理完這條消息之後再手動提交 ack。

這樣即使遇到了上面的情況,RabbitMQ 也不會把這條消息刪除,會在你程序重啓之後,重新下發這條消息過來。

怎麼保證 MQ 的高可用性性?

使用了 MQ 之後,我們肯定是希望 MQ 有可高用特性,因爲不可能接受機器宕機了,就無法收發消息的情況。

這一塊我們也是基於 RabbitMQ 這種經典的 MQ 來說明一下:

RabbitMQ 是比較有代表性的,因爲是基於主從做高可用性的,我們就以他爲例子講解第一種 MQ 的高可用性怎麼實現。

rabbitmq 有三種模式:單機模式,普通集羣模式,鏡像集羣模式

單機模式

單機模式就是 demo 級別的,就是說只有一臺機器部署了一個 RabbitMQ 程序。這個會存在單點問題,宕機就玩完了,沒什麼高可用性可言。一般就是你本地啓動了玩玩兒的,沒人生產用單機模式。

普通集羣模式

這個模式的意思就是在多臺機器上啓動多個 rabbitmq 實例。類似的 master-slave 模式一樣。但是創建的 queue,只會放在一個 master rabbtimq 實例上,其他實例都同步那個接收消息的 RabbitMQ 元數據。

在消費消息的時候,如果你連接到的 RabbitMQ 實例不是存放 Queue 數據的實例,這個時候 RabbitMQ 就會從存放 Queue 數據的實例上拉去數據,然後返回給客戶端。

總的來說,這種方式有點麻煩,沒有做到真正的分佈式,每次消費者連接一個實例後拉取數據,如果連接到不是存放 queue 數據的實例,這個時候會造成額外的性能開銷。如果從放 Queue 的實例拉取,會導致單實例性能瓶頸。

如果放 queue 的實例宕機了,會導致其他實例無法拉取數據,這個集羣都無法消費消息了,沒有做到真正的高可用。

所以這個事兒就比較尷尬了,這就沒有什麼所謂的高可用性可言了,這方案主要是提高吞吐量的,就是說讓集羣中多個節點來服務某個 queue 的讀寫操作。

鏡像集羣模式

鏡像集羣模式纔是真正的 rabbitmq 的高可用模式,跟普通集羣模式不一樣的是:創建的 queue 無論元數據還是 queue 裏的消息都會存在於多個實例上,每次寫消息到 queue 的時候,都會自動把消息到多個實例的 queue 裏進行消息同步。

這樣的話任何一個機器宕機了別的實例都可以用提供服務,這樣就做到了真正的高可用了。

但是也存在着不好之處:

  • 性能開銷過高,消息需要同步所有機器,會導致網絡帶寬壓力和消耗很重
  • 擴展性低:無法解決某個 queue 數據量特別大的情況,導致 queue 無法線性拓展。就算加了機器,那個機器也會包含 queue 的所有數據,queue 的數據沒有做到分佈式存儲。

對於 RabbitMQ 的高可用一般的做法都是開啓鏡像集羣模式,這樣起碼來說做到了高可用,一個節點宕機了,其他節點可以繼續提供服務。

總結

通過本篇文章,分析了對於 MQ 的一些常規問題:

  • 爲什麼使用 MQ?
  • 使用 MQ 有什麼優缺點
  • 如何保證消息不丟失
  • 如何保證 MQ 高可用性

但是,這些問題僅僅是使用 MQ 的其中一部分需要考慮的問題,事實上,還有其他更加複雜的問題需要我們去解決,

比如說:如何保證消息的順序性?消息隊列如何選型?消息積壓問題如何解決?

本文僅僅是針對 RabbitMQ 的場景舉例子。還有其他比較的消息隊列,比如 RocketMQ、Kafka,不同的 MQ 在面臨上述問題的時候,要根據他們的原理機制來做對應的處理,這些都是本文沒有顧及的內容,將在後面的文章中討論。敬請關注。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章