1. 程式人生 > >高併發之訊息佇列

高併發之訊息佇列

訊息佇列已經逐漸成為企業IT系統內部通訊的核心手段。它具有低耦合、可靠投遞、廣播、流量控制、最終一致性等一系列功能,成為非同步RPC的主要手段之一。

訊息被處理的過程相當於流程A被處理。我們這裡以一個實際的模型來討論下,比如使用者下單成功時給使用者發簡訊,如果沒有這個訊息佇列,我們會選擇同步呼叫發簡訊的介面,

並等待短息傳送成功,這時候假設簡訊介面實現出現問題了,或者簡訊呼叫端超時了,又或者簡訊傳送達到上限了,我們是選擇重試幾次還是放棄,還是選擇把這個放到資料庫

過一段時間再看看呢,不管怎樣,實現都很複雜。

我們可以將發簡訊這個請求放在訊息佇列裡,訊息佇列按照一定的順序挨個處理佇列裡的訊息,當處理到傳送簡訊的任務時,通知簡訊服務傳送訊息,如果出現之前出現的問題,那麼把這個訊息重新放到訊息佇列中。

訊息佇列的好處:

1.成功完成了一個非同步解耦的過程。簡訊傳送時只要保證放到訊息佇列中就可以了,接著做後面的事情就行。一個事務只關心本質的流程,需要依賴其他事情但是不那麼重要的時候,有通知即可,無需等待結果。每個成員不必受其他成員影響,可以更獨立自主,只通過一個簡單的容器來聯絡。

對於我們的訂單系統,訂單最終支付成功之後可能需要給使用者傳送簡訊積分什麼的,但其實這已經不是我們系統的核心流程了。如果外部系統速度偏慢(比如簡訊閘道器速度不好),那麼主流程的時間會加長很多,使用者肯定不希望點選支付過好幾分鐘才看到結果。那麼我們只需要通知簡訊系統“我們支付成功了”,不一定非要等待它處理完成。

2.保證了最終一致性

,通過在佇列中存放任務保證它最終一定會執行。

最終一致性指的是兩個系統的狀態保持一致,要麼都成功,要麼都失敗。當然有個時間限制,理論上越快越好,但實際上在各種異常的情況下,可能會有一定延遲達到最終一致狀態,但最後兩個系統的狀態是一樣的。
業界有一些為“最終一致性”而生的訊息佇列,如Notify(阿里)、QMQ(去哪兒)等,其設計初衷,就是為了交易系統中的高可靠通知。
以一個銀行的轉賬過程來理解最終一致性,轉賬的需求很簡單,如果A系統扣錢成功,則B系統加錢一定成功。反之則一起回滾,像什麼都沒發生一樣。
然而,這個過程中存在很多可能的意外:

  1. A扣錢成功,呼叫B加錢介面失敗。
  2. A扣錢成功,呼叫B加錢介面雖然成功,但獲取最終結果時網路異常引起超時。
  3. A扣錢成功,B加錢失敗,A想回滾扣的錢,但A機器down機。

可見,想把這件看似簡單的事真正做成,真的不那麼容易。所有跨VM的一致性問題,從技術的角度講通用的解決方案是:

  1. 強一致性,分散式事務,但落地太難且成本太高,後文會具體提到。
  2. 最終一致性,主要是用“記錄”和“補償”的方式。在做所有的不確定的事情之前,先把事情記錄下來,然後去做不確定的事情,結果可能是:成功、失敗或是不確定,“不確定”(例如超時等)可以等價為失敗。成功就可以把記錄的東西清理掉了,對於失敗和不確定,可以依靠定時任務等方式把所有失敗的事情重新搞一遍,直到成功為止。
    回到剛才的例子,系統在A扣錢成功的情況下,把要給B“通知”這件事記錄在庫裡(為了保證最高的可靠性可以把通知B系統加錢和扣錢成功這兩件事維護在一個本地事務裡),通知成功則刪除這條記錄,通知失敗或不確定則依靠定時任務補償性地通知我們,直到我們把狀態更新成正確的為止。

3.廣播

訊息佇列的基本功能之一是進行廣播。如果沒有訊息佇列,每當一個新的業務方接入,我們都要聯調一次新介面。有了訊息佇列,我們只需要關心訊息是否送達了佇列,至於誰希望訂閱,是下游的事情,無疑極大地減少了開發和聯調的工作量。

3.提速。假設我們還需要傳送郵件,有了訊息佇列就不需要同步等待,我們可以直接並行處理,而下單核心任務可以更快完成。增強業務系統的非同步處理能力。甚至幾乎不可能出現並發現象。

4.削峰和流控。不對於不需要實時處理的請求來說,當併發量特別大的時候,可以先在訊息佇列中作快取,然後陸續傳送給對應的服務去處理

試想上下游對於事情的處理能力是不同的。比如,Web前端每秒承受上千萬的請求,並不是什麼神奇的事情,只需要加多一點機器,再搭建一些LVS負載均衡裝置和Nginx等即可。但資料庫的處理能力卻十分有限,即使使用SSD加分庫分表,單機的處理能力仍然在萬級。由於成本的考慮,我們不能奢求資料庫的機器數量追上前端。
這種問題同樣存在於系統和系統之間,如簡訊系統可能由於短板效應,速度卡在閘道器上(每秒幾百次請求),跟前端的併發量不是一個數量級。但使用者晚上個半分鐘左右收到簡訊,一般是不會有太大問題的。如果沒有訊息佇列,兩個系統之間通過協商、滑動視窗等複雜的方案也不是說不能實現。但系統複雜性指數級增長,勢必在上游或者下游做儲存,並且要處理定時、擁塞等一系列問題。而且每當有處理能力有差距的時候,都需要單獨開發一套邏輯來維護這套邏輯。所以,利用中間系統轉儲兩個系統的通訊內容,並在下游系統有能力處理這些訊息的時候,再處理這些訊息,是一套相對較通用的方式。

總而言之,訊息佇列不是萬能的。對於需要強事務保證而且延遲敏感的,RPC是優於訊息佇列的。
對於一些無關痛癢,或者對於別人非常重要但是對於自己不是那麼關心的事情,可以利用訊息佇列去做。
支援最終一致性的訊息佇列,能夠用來處理延遲不那麼敏感的“分散式事務”場景,而且相對於笨重的分散式事務,可能是更優的處理方式。
當上下游系統處理能力存在差距的時候,利用訊息佇列做一個通用的“漏斗”。在下游有能力處理的時候,再進行分發。
如果下游有很多系統關心你的系統發出的通知的時候,果斷地使用訊息佇列吧。

訊息佇列的使用場景:

主要特點是非同步處理,主要目的是減少請求響應時間和解耦。所以主要的使用場景就是將比較耗時而且不需要即時(同步)返回結果的操作作為訊息放入訊息佇列。

使用場景的話,舉個例子:
假設使用者在你的軟體中註冊,服務端收到使用者的註冊請求後,它會做這些操作:
  1. 校驗使用者名稱等資訊,如果沒問題會在資料庫中新增一個使用者記錄
  2. 如果是用郵箱註冊會給你傳送一封註冊成功的郵件,手機註冊則會發送一條簡訊
  3. 分析使用者的個人資訊,以便將來向他推薦一些志同道合的人,或向那些人推薦他
  4. 傳送給使用者一個包含操作指南的系統通知
  5. 等等……

但是對於使用者來說,註冊功能實際只需要第一步,只要服務端將他的賬戶資訊存到資料庫中他便可以登入上去做他想做的事情了。至於其他的事情,非要在這一次請求中全部完成麼?值得使用者浪費時間等你處理這些對他來說無關緊要的事情麼?所以實際當第一步做完後,服務端就可以把其他的操作放入對應的訊息佇列中然後馬上返回使用者結果,由訊息佇列非同步的進行這些操作。

或者還有一種情況,同時有大量使用者註冊你的軟體,再高併發情況下注冊請求開始出現一些問題,例如郵件介面承受不住,或是分析資訊時的大量計算使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中獲取訊息進行消費。

 轉:原文連結

原文連結