消息隊列綜述

消息隊列MQ幾乎是每一個大型系統的必備選擇,抽時間搞搞MQ的整體需求、實現的重點難點,然後再具體應用某一個框架,比如RabbitMQ、kafka等,就會有事半功倍的效果。

何時需要消息隊列?

可以使用mq的場景有很多,最常用的幾種,是做業務解耦/廣播/錯峯流控等。反之,如果需要強一致性,關注業務邏輯的處理結果,則RPC顯得更爲合適。

解耦

解耦是消息隊列要解決的最本質問題。所謂解耦,簡單點講就是一個事務,只關心核心的流程。而需要依賴其他系統但不那麼重要的事情,有通知即可,無需等待結果。換句話說,基於消息的模型,關心的是“通知”,而非“處理”。
比如有個訂餐系統,客戶預定成功之後,我們只要發送消息給餐廳,告訴他們有客人訂餐成功就可以了,並不關係後續的準備、接待等工作;或許我們還要發送消息給短信提醒服務,讓他們在飯前幾個小時提醒客戶,同樣只關心通知,不關心處理。

廣播

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

錯峯與流控

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

如何設計一個消息隊列

消息隊列內容
基於消息的系統模型,不一定需要broker(消息隊列服務端)。市面上的的Akka(actor模型)、ZeroMQ等,其實都是基於消息的系統設計範式,但是沒有broker。當然,實現就比較麻煩。
實現隊列基本功能:

RPC通信協議

所謂消息隊列,無外乎兩次RPC加一次轉儲,當然需要消費端最終做消費確認的情況是三次RPC。既然是RPC,就必然牽扯出一系列話題,什麼負載均衡啊、服務發現啊、通信協議啊、序列化協議啊,等等。在這一塊,我的強烈建議是不要重複造輪子,利用公司現有的RPC框架:Thrift也好,Dubbo也好,或者是其他自定義的框架也好。

高可用

其實所有的高可用,是依賴於RPC和存儲的高可用來做的。先來看RPC的高可用,美團的基於MTThrift的RPC框架,阿里的Dubbo等,其本身就具有服務自動發現,負載均衡等功能。存儲系統本身的可用性我們不需要操太多心,放心大膽的交給DBA們吧!

服務端承載消息堆積的能力

消息到達服務端如果不經過任何處理就到接收者了,broker就失去了它的意義。爲了滿足我們錯峯/流控/最終可達等一系列需求,把消息存儲下來,然後選擇時機投遞就顯得是順理成章的了。
只是這個存儲可以做成很多方式。比如存儲在內存裏,存儲在分佈式KV裏,存儲在磁盤裏,存儲在數據庫裏等等。但歸結起來,主要有持久化和非持久化兩種。
持久化的形式能更大程度地保證消息的可靠性(如斷電等不可抗外力),並且理論上能承載更大限度的消息堆積(外存的空間遠大於內存)。
但並不是每種消息都需要持久化存儲。很多消息對於投遞性能的要求大於可靠性的要求,且數量極大(如日誌)。這時候,消息不落地直接暫存內存,嘗試幾次failover,最終投遞出去也未嘗不可。

存儲子系統的選擇

我們來看看如果需要數據落地的情況下各種存儲子系統的選擇。理論上,從速度來看,文件系統>分佈式KV(持久化)>分佈式文件系統>數據庫,而可靠性卻截然相反。還是要從支持的業務場景出發作出最合理的選擇,如果你們的消息隊列是用來支持支付/交易等對可靠性要求非常高,但對性能和量的要求沒有這麼高,而且沒有時間精力專門做文件存儲系統的研究,DB是最好的選擇。

消費關係解析

市面上的消息隊列定義了一堆讓人暈頭轉向的名詞,如JMS 規範中的Topic/Queue,Kafka裏面的Topic/Partition/ConsumerGroup,RabbitMQ裏面的Exchange等等。拋開現象看本質,無外乎是單播與廣播的區別。所謂單播,就是點到點;而廣播,是一點對多點。當然,對於互聯網的大部分應用來說,組間廣播、組內單播是最常見的情形。
隊列高級特性設計

可靠投遞(最終一致性)

這是個激動人心的話題,完全不丟消息,究竟可不可能?答案是,完全可能,前提是消息可能會重複,並且,在異常情況下,要接受消息的延遲。
方案說簡單也簡單,就是每當要發生不可靠的事情(RPC等)之前,先將消息落地存儲,然後發送。當失敗或者不知道成功失敗(比如超時)時,消息狀態是待發送,定時任務不停輪詢所有待發送消息,最終一定可以送達。

消費確認

當broker把消息投遞給消費者後,消費者可以立即響應我收到了這個消息。但收到了這個消息只是第一步,我能不能處理這個消息卻不一定。或許因爲消費能力的問題,系統的負荷已經不能處理這個消息;或者是剛纔狀態機裏面提到的消息不是我想要接收的消息,主動要求重發。

重複消息和順序消息

如何鑑別消息重複,並冪等的處理重複消息。
每一個消息應該有它的唯一身份。不管是業務方自定義的,還是根據IP/PID/時間戳生成的MessageId,如果有地方記錄這個MessageId,消息到來是能夠進行比對就能完成重複的鑑定。
冪等的處理消息是一門藝術,因爲種種原因重複消息或者錯亂的消息還是來到了,說兩種通用的解決方案:版本號,狀態機。版本號參考了部分TCP協議。狀態機,業務方只需要自己維護一個狀態機,定義各種狀態的流轉關係。例如,”下線”狀態只允許接收”上線”消息,“上線”狀態只能接收“下線消息”,如果上線收到上線消息,或者下線收到下線消息,在消息不丟失和上游業務正確的前提下。要麼是消息發重了,要麼是順序到達反了。這時消費者只需要把“我不能處理這個消息”告訴投遞者,要求投遞者過一段時間重發即可。
一個消息隊列如何儘量減少重複消息的投遞。
broker記錄MessageId,直到投遞成功後清除,重複的ID到來不做處理,這樣只要發送者在清除週期內能夠感知到消息投遞成功,就基本不會在server端產生重複消息。
對於server投遞到consumer的消息,由於不確定對端是在處理過程中還是消息發送丟失的情況下,有必要記錄下投遞的IP地址。決定重發之前詢問這個IP,消息處理成功了嗎?如果詢問無果,再重發。

事務

解決方案從大方向上有兩種:兩階段提交,分佈式事務。本地事務,本地落地,補償發送。
分佈式事務存在的最大問題是成本太高,兩階段提交協議,對於仲裁down機或者單點故障,幾乎是一個無解的黑洞。
那如何使用本地事務解決分佈式事務的問題呢?以本地和業務在一個數據庫實例中建表爲例子,與扣錢的業務操作同一個事務裏,將消息插入本地數據庫。如果消息入庫失敗,則業務回滾;如果消息入庫成功,事務提交。消息只要成功落地,很大程度上就沒有丟失的風險(磁盤物理損壞除外)。而消息只要投遞到服務端確認後本地才做刪除,就完成了producer->broker的可靠投遞,並且當消息存儲異常時,業務也是可以回滾的。

批量

談到批量就不得不提生產者消費者模型。但生產者消費者模型中最大的痛點是:消費者到底應該何時進行消費。大處着眼來看,消費動作都是事件驅動的。主要事件包括:
攢夠了一定數量 / 到達了一定時間 / 隊列裏有新的數據到來
對於及時性要求高的數據,可用採用方式3來完成,比如客戶端向服務端投遞數據。

push 還是 pull

消息隊列,大多是針對push模型的設計。現在市面上有很多經典的也比較成熟的pull模型的消息隊列,如Kafka、MetaQ等。
慢消費無疑是push模型最大的致命傷,穿成流水線來看,如果消費者的速度比發送者的速度慢很多,勢必造成消息在broker的堆積。假設這些消息都是有用的無法丟棄的,消息就要一直在broker端保存。當然這還不是最致命的,最致命的是broker給consumer推送一堆consumer無法處理的消息,consumer不是reject就是error,然後來回踢皮球。
消息延遲與忙等是pull模式最大的短板。由於主動權在消費方,消費方無法準確地決定何時去拉取最新的消息。

MQ與RPC的區別和適用場景:

相同點
RPC和MQ都是用於分佈式系統的兩個關鍵技術,並且裏面都有服務提供者和消費者的概念,可在一定程度上對系統進行解耦。

初步理解的區別

  • 消息隊列主要適用於異步場景,而rpc主要是遠程同步調用
  • rpc讓你遠程調用象本地調用,一般是同步的,例如,你讀一個文件,象調用本地的函數,就是時間久點。消息代理框架一般是異步的,一個線程send,另外一個線程recv
  • 消息隊列是系統級、模塊級的通信。RPC是對象級、函數級通信。
  • RPC系統結構:
    | Consumer | <=> | Provider |
    Consumer調用的Provider提供的服務。
    Message Queue系統結構:
    | Sender | <=> | Queue | <=> | Receiver |
    Sender發送消息給Queue;Receiver從Queue拿到消息來處理。

使用場景:

  • 希望同步得到結果的場合,RPC合適。
  • 希望使用簡單,則RPC;RPC操作基於接口,使用簡單,使用方式模擬本地調用。異步的方式編程比較複雜。
  • 不希望發送端(RPC Consumer、Message Sender)受限於處理端(RPC Provider、Message Receiver)的速度時,使用Message Queue。
  • 隨着業務增長,有的處理端處理量會成爲瓶頸,會進行同步調用到異步消息的改造。通常伴隨着業務流程的改造

JAVA幾種消息隊列應用簡介

RabbitMQ

RabbitMQ是一個AMQP實現,傳統的messaging queue系統實現,基於Erlang。老牌MQ產品了。AMQP協議更多用在企業系統內,對數據一致性、穩定性和可靠性要求很高的場景,對性能和吞吐量還在其次。

RabbitMQ,工作消息隊列

Kafka

Kafka是linkedin開源的MQ系統,主要特點是基於Pull的模式來處理消息消費,追求高吞吐量,一開始的目的就是用於日誌收集和傳輸,0.8開始支持複製,不支持事務,適合產生大量數據的互聯網服務的數據收集業務。

Kafka, 日誌訂閱,着重數據流處理

ZeroMQ

ZeroMQ只是一個網絡編程的Pattern庫,將常見的網絡請求形式(分組管理,鏈接管理,發佈訂閱等)模式化、組件化,簡而言之socket之上、MQ之下。對於MQ來說,網絡傳輸只是它的一部分,更多需要處理的是消息存儲、路由、Broker服務發現和查找、事務、消費模式(ack、重投等)、集羣服務等。

ZeroMQ,本地進程之間的coordination,方便地做socket

參考文章

http://oldratlee.com/post/2013-02-01/synchronous-rpc-vs-asynchronous-message
http://www.tuicool.com/articles/Y7Zriae
http://blog.csdn.net/heyutao007/article/details/50131089

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