消息隊列已經逐漸成為企業IT系統內部通信的核心手段。它具有低耦合、可靠投遞、廣播、流量控制、最終一致性等一系列功能,成為非同步RPC的主要手段之一。 消息被處理的過程相當於流程A被處理。我們這裡以一個實際的模型來討論下,比如用戶下單成功時給用戶發簡訊,如果沒有這個消息隊列,我們會選擇同步調用發簡訊的接 ...
消息隊列已經逐漸成為企業IT系統內部通信的核心手段。它具有低耦合、可靠投遞、廣播、流量控制、最終一致性等一系列功能,成為非同步RPC的主要手段之一。
消息被處理的過程相當於流程A被處理。我們這裡以一個實際的模型來討論下,比如用戶下單成功時給用戶發簡訊,如果沒有這個消息隊列,我們會選擇同步調用發簡訊的介面,
並等待短息發送成功,這時候假設簡訊介面實現出現問題了,或者簡訊調用端超時了,又或者簡訊發送達到上限了,我們是選擇重試幾次還是放棄,還是選擇把這個放到資料庫
過一段時間再看看呢,不管怎樣,實現都很複雜。
我們可以將發簡訊這個請求放在消息隊列里,消息隊列按照一定的順序挨個處理隊列里的消息,當處理到發送簡訊的任務時,通知簡訊服務發送消息,如果出現之前出現的問題,那麼把這個消息重新放到消息隊列中。
消息隊列的好處:
1.成功完成了一個非同步解耦的過程。簡訊發送時只要保證放到消息隊列中就可以了,接著做後面的事情就行。一個事務只關心本質的流程,需要依賴其他事情但是不那麼重要的時候,有通知即可,無需等待結果。每個成員不必受其他成員影響,可以更獨立自主,只通過一個簡單的容器來聯繫。
對於我們的訂單系統,訂單最終支付成功之後可能需要給用戶發送簡訊積分什麼的,但其實這已經不是我們系統的核心流程了。如果外部系統速度偏慢(比如簡訊網關速度不好),那麼主流程的時間會加長很多,用戶肯定不希望點擊支付過好幾分鐘才看到結果。那麼我們只需要通知簡訊系統“我們支付成功了”,不一定非要等待它處理完成。
2.保證了最終一致性,通過在隊列中存放任務保證它最終一定會執行。
最終一致性指的是兩個系統的狀態保持一致,要麼都成功,要麼都失敗。當然有個時間限制,理論上越快越好,但實際上在各種異常的情況下,可能會有一定延遲達到最終一致狀態,但最後兩個系統的狀態是一樣的。
業界有一些為“最終一致性”而生的消息隊列,如Notify(阿裡)、QMQ(去哪兒)等,其設計初衷,就是為了交易系統中的高可靠通知。
以一個銀行的轉賬過程來理解最終一致性,轉賬的需求很簡單,如果A系統扣錢成功,則B系統加錢一定成功。反之則一起回滾,像什麼都沒發生一樣。
然而,這個過程中存在很多可能的意外:
- A扣錢成功,調用B加錢介面失敗。
- A扣錢成功,調用B加錢介面雖然成功,但獲取最終結果時網路異常引起超時。
- A扣錢成功,B加錢失敗,A想回滾扣的錢,但A機器down機。
可見,想把這件看似簡單的事真正做成,真的不那麼容易。所有跨VM的一致性問題,從技術的角度講通用的解決方案是:
- 強一致性,分散式事務,但落地太難且成本太高,後文會具體提到。
- 最終一致性,主要是用“記錄”和“補償”的方式。在做所有的不確定的事情之前,先把事情記錄下來,然後去做不確定的事情,結果可能是:成功、失敗或是不確定,“不確定”(例如超時等)可以等價為失敗。成功就可以把記錄的東西清理掉了,對於失敗和不確定,可以依靠定時任務等方式把所有失敗的事情重新搞一遍,直到成功為止。
回到剛纔的例子,系統在A扣錢成功的情況下,把要給B“通知”這件事記錄在庫里(為了保證最高的可靠性可以把通知B系統加錢和扣錢成功這兩件事維護在一個本地事務里),通知成功則刪除這條記錄,通知失敗或不確定則依靠定時任務補償性地通知我們,直到我們把狀態更新成正確的為止。
3.廣播
消息隊列的基本功能之一是進行廣播。如果沒有消息隊列,每當一個新的業務方接入,我們都要聯調一次新介面。有了消息隊列,我們只需要關心消息是否送達了隊列,至於誰希望訂閱,是下游的事情,無疑極大地減少了開發和聯調的工作量。
3.提速。假設我們還需要發送郵件,有了消息隊列就不需要同步等待,我們可以直接並行處理,而下單核心任務可以更快完成。增強業務系統的非同步處理能力。甚至幾乎不可能出現併發現象。
4.削峰和流控。不對於不需要實時處理的請求來說,當併發量特別大的時候,可以先在消息隊列中作緩存,然後陸續發送給對應的服務去處理
試想上下游對於事情的處理能力是不同的。比如,Web前端每秒承受上千萬的請求,並不是什麼神奇的事情,只需要加多一點機器,再搭建一些LVS負載均衡設備和Nginx等即可。但資料庫的處理能力卻十分有限,即使使用SSD加分庫分表,單機的處理能力仍然在萬級。由於成本的考慮,我們不能奢求資料庫的機器數量追上前端。
這種問題同樣存在於系統和系統之間,如簡訊系統可能由於短板效應,速度卡在網關上(每秒幾百次請求),跟前端的併發量不是一個數量級。但用戶晚上個半分鐘左右收到簡訊,一般是不會有太大問題的。如果沒有消息隊列,兩個系統之間通過協商、滑動視窗等複雜的方案也不是說不能實現。但系統複雜性指數級增長,勢必在上游或者下游做存儲,並且要處理定時、擁塞等一系列問題。而且每當有處理能力有差距的時候,都需要單獨開發一套邏輯來維護這套邏輯。所以,利用中間系統轉儲兩個系統的通信內容,併在下游系統有能力處理這些消息的時候,再處理這些消息,是一套相對較通用的方式。
總而言之,消息隊列不是萬能的。對於需要強事務保證而且延遲敏感的,RPC是優於消息隊列的。
對於一些無關痛癢,或者對於別人非常重要但是對於自己不是那麼關心的事情,可以利用消息隊列去做。
支持最終一致性的消息隊列,能夠用來處理延遲不那麼敏感的“分散式事務”場景,而且相對於笨重的分散式事務,可能是更優的處理方式。
當上下游系統處理能力存在差距的時候,利用消息隊列做一個通用的“漏斗”。在下游有能力處理的時候,再進行分發。
如果下游有很多系統關心你的系統發出的通知的時候,果斷地使用消息隊列吧。
消息隊列的使用場景:
主要特點是非同步處理,主要目的是減少請求響應時間和解耦。所以主要的使用場景就是將比較耗時而且不需要即時(同步)返回結果的操作作為消息放入消息隊列。
使用場景的話,舉個例子:假設用戶在你的軟體中註冊,服務端收到用戶的註冊請求後,它會做這些操作:
- 校驗用戶名等信息,如果沒問題會在資料庫中添加一個用戶記錄
- 如果是用郵箱註冊會給你發送一封註冊成功的郵件,手機註冊則會發送一條簡訊
- 分析用戶的個人信息,以便將來向他推薦一些志同道合的人,或向那些人推薦他
- 發送給用戶一個包含操作指南的系統通知
- 等等……
但是對於用戶來說,註冊功能實際只需要第一步,只要服務端將他的賬戶信息存到資料庫中他便可以登錄上去做他想做的事情了。至於其他的事情,非要在這一次請求中全部完成麽?值得用戶浪費時間等你處理這些對他來說無關緊要的事情麽?所以實際當第一步做完後,服務端就可以把其他的操作放入對應的消息隊列中然後馬上返回用戶結果,由消息隊列非同步的進行這些操作。
或者還有一種情況,同時有大量用戶註冊你的軟體,再高併發情況下註冊請求開始出現一些問題,例如郵件介面承受不住,或是分析信息時的大量計算使cpu滿載,這將會出現雖然用戶數據記錄很快的添加到資料庫中了,但是卻卡在發郵件或分析信息時的情況,導致請求的響應時間大幅增長,甚至出現超時,這就有點不划算了。面對這種情況一般也是將這些操作放入消息隊列(生產者消費者模型),消息隊列慢慢的進行處理,同時可以很快的完成註冊請求,不會影響用戶使用其他功能。
為什麼需要消息隊列?
生產和消費的速度或者穩定性不一致。
當今市面上有很多主流的消息中間件,如老牌的ActiveMQ、RabbitMQ,炙手可熱的Kafka,阿裡巴巴自主開發的Notify、MetaQ、RocketMQ等。
Kafka的介紹
Kafka是一種高吞吐量的分散式發佈訂閱消息系統,它可以處理消費者規模的網站中的所有動作流數據。
Kafka 有如下特性:
- 以時間複雜度為O(1)的方式提供消息持久化能力,即使對TB級以上數據也能保證常數時間複雜度的訪問性能。
- 高吞吐率。即使在非常廉價的商用機器上也能做到單機支持每秒100K條以上消息的傳輸。
- 支持Kafka Server間的消息分區,及分散式消費,同時保證每個Partition內的消息順序傳輸。
- 同時支持離線數據處理和實時數據處理。
- Scale out:支持線上水平擴展。
kafka的術語
- Broker:Kafka集群包含一個或多個伺服器,這種伺服器被稱為broker。
- Topic:每條發佈到Kafka集群的消息都有一個類別,這個類別被稱為Topic。(物理上不同Topic的消息分開存儲,邏輯上一個Topic的消息雖然保存於一個或多個broker上但用戶只需指定消息的Topic即可生產或消費數據而不必關心數據存於何處)
- Partition:Partition是物理上的概念,每個Topic包含一個或多個Partition。
- Producer:負責發佈消息到Kafka broker。
- Consumer:消息消費者,向Kafka broker讀取消息的客戶端。
- Consumer Group:每個Consumer屬於一個特定的Consumer Group(可為每個Consumer指定group name,若不指定group name則屬於預設的group)。
下麵來介紹RabbitMQ里的一些基本定義,主要如下:
RabbitMQ Server:提供消息一條從Producer到Consumer的處理。
Exchange:一邊從發佈者方接收消息,一邊把消息推送到隊列。
producer只能將消息發送給exchange。而exchange負責將消息發送到queues。Procuder Publish的Message進入了exchange,exchange會根據routingKey處理接收到的消息,判斷消息是應該推送到指定的隊列還是是多個隊列,或者是直接忽略消息。這些規則是通過交換機類型(exchange type)來定義的主要的type有direct,topic,headers,fanout。具體針對不同的場景使用不同的type。
queue也是通過這個routing keys來做的綁定。交換機將會對綁定鍵(binding key)和路由鍵(routing key)進行精確匹配,從而確定消息該分發到哪個隊列。
Queue:消息隊列。接收來自exchange的消息,然後再由consumer取出。exchange和queue可以一對一,也可以一對多,它們的關係通過routingKey來綁定。
Producer:Client A & B,生產者,消息的來源,消息必鬚髮送給exchange。而不是直接給queue
Consumer:Client 1,2,3消費者,直接從queue中獲取消息進行消費,而不是從exchange中獲取消息進行消費。