原創聲明
本文首發于微信公衆号【程式員黃小斜】
本文作者:黃小斜
轉載請務必在文章開頭注明出處和作者。
本文思維導圖
什麼是消息隊列
“RabbitMQ?”“Kafka?”“RocketMQ?”...在日常學習與開發過程中,我們常常聽到消息隊列這個關鍵詞,可能你是熟練使用消息隊列的老手,又或者你是不懂消息隊列的新手,不論你了不了解消息隊列,本文都将帶你搞懂消息隊列的一些基本理論。如果你是老手,你可能從本文學到你之前不曾注意的一些關于消息隊列的重要概念,如果你是新手,相信本文将是你打開消息隊列大門的一闆磚。
根據百度百科的說法,“消息隊列”是在消息的傳輸過程中儲存消息的容器。消息隊列管理器在将消息從它的源中繼到它的目标時充當中間人。隊列的主要目的是提供路由并保證消息的傳遞;如果發送消息時接收者不可用,消息隊列會保留消息,直到可以成功地傳遞它。`
為什麼要使用消息隊列
我覺得使用消息隊列主要有兩點好處:
1.通過異步處理提高系統性能(削峰、減少響應所需時間);
2.降低系統耦合性。如果在面試的時候你被面試官問到這個問題的話,一般情況是你在你的履歷上涉及到消息隊列這方面的内容,這個時候推薦你結合你自己的項目來回答。
《大型網站技術架構》第四章和第七章均有提到消息隊列對應用性能及擴充性的提升。
在我平時的日常工作中,用到消息隊列的場景可不少,比如,我有一個定時任務需要在A應用每天7點開始排程,那麼定時任務系統如何告訴這個A應用呢,一種辦法是直接調用A應用的RPC服務,但是,定時任務系統不可能去記錄那麼多應用的RPC服務,是以如果換成消息,就大大降低了複雜度。
還有一種常見使用消息隊列的場景,那就是把一些不需要及時處理的RPC調用改成消息,比如最典型的電商下單,一定是實時性要求很高的,但是,有一些消息會在使用者下單後進行異步的發送,比如使用者對商品的評價,使用者的退款請求,這些請求不需要被實時地進行處理,完全可以異步化處理,這個時候使用消息隊列就是再好不過的選擇了,消息隊列會幫你存儲這些待處理的消息,并且等應用負載較低的時候再分發給應用處理,或者是等待應用主動向消息隊列擷取消息。
常用的消息隊列
我們可以把消息隊列比作是一個存放消息的容器,當我們需要使用消息的時候可以取出消息供自己使用。消息隊列是分布式系統中重要的元件,使用消息隊列主要是為了通過異步處理提高系統性能和削峰、降低系統耦合性。目前使用較多的消息隊列有ActiveMQ,RabbitMQ,Kafka,RocketMQ。
當然,在我們公司内部用的大多都是自研的消息隊列産品,一方面是因為需要适配金融級分布式場景,另一方面自研的中間件有專門的的團隊維護,出了什麼問題才能及時處理和修複。
下面我們就一起來看看這些開源的消息隊列是怎麼設計的,各有什麼優缺點呢。
RabbitMQ
2007年釋出,是一個在
AMQP(進階消息隊列協定)基礎上完成的,可複用的企業消息系統,是目前最主流的消息中間件之一。
主要特性:
- 可靠性: 提供了多種技術可以讓你在性能和可靠性之間進行權衡。這些技術包括持久性機制、投遞确認、釋出者證明和高可用性機制;
- 靈活的路由: 消息在到達隊列前是通過交換機進行路由的。RabbitMQ為典型的路由邏輯提供了多種内置交換機類型。如果你有更複雜的路由需求,可以将這些交換機組合起來使用,你甚至可以實作自己的交換機類型,并且當做RabbitMQ的插件來使用;
- 消息叢集:在相同區域網路中的多個RabbitMQ伺服器可以聚合在一起,作為一個獨立的邏輯代理來使用;
- 隊列高可用:隊列可以在叢集中的機器上進行鏡像,以確定在硬體問題下還保證消息安全;
- 多種協定的支援:支援多種消息隊列協定;
- 伺服器端用Erlang語言編寫,支援隻要是你能想到的所有程式設計語言;
- 管理界面: RabbitMQ有一個易用的使用者界面,使得使用者可以監控和管理消息Broker的許多方面;
- 跟蹤機制:如果消息異常,RabbitMQ提供消息跟蹤機制,使用者可以找出發生了什麼;
- 插件機制:提供了許多插件,來從多方面進行擴充,也可以編寫自己的插件;
優點:
- 由于erlang語言的特性,mq 性能較好,高并發;
- 健壯、穩定、易用、跨平台、支援多種語言、文檔齊全;
- 有消息确認機制和持久化機制,可靠性高;
- 高度可定制的路由;
- 管理界面較豐富,在網際網路公司也有較大規模的應用;
- 社群活躍度高;
缺點:
- 盡管結合erlang語言本身的并發優勢,性能較好,但是不利于做二次開發和維護;
- 實作了代理架構,意味着消息在發送到用戶端之前可以在中央節點上排隊。此特性使得RabbitMQ易于使用和部署,但是使得其運作速度較慢,因為中央節點增加了延遲,消息封裝後也比較大;
- 需要學習比較複雜的接口和協定,學習和維護成本較高;
ActiveMQ
是由Apache出品,ActiveMQ 是一個完全支援JMS1.1和J2EE 1.4規範的 JMS Provider實作。它非常快速,支援多種語言的用戶端和協定,而且可以非常容易的嵌入到企業的應用環境中,并有許多進階功能。
- 服從JMS 規範:JMS 規範提供了良好的标準和保證,包括:同步或異步的消息分發,一次和僅一次的消息分發,消息接收和訂閱等等。遵從 JMS 規範的好處在于,不論使用什麼 JMS 實作提供者,這些基礎特性都是可用的;
- 連接配接性:ActiveMQ 提供了廣泛的連接配接選項,支援的協定有:HTTP/S,IP 多點傳播,SSL,STOMP,TCP,UDP,XMPP等等。對衆多協定的支援讓 ActiveMQ 擁有了很好的靈活性。
- 支援的協定種類多:OpenWire、STOMP、REST、XMPP、AMQP ;
- 持久化插件和安全插件:ActiveMQ 提供了多種持久化選擇。而且,ActiveMQ 的安全性也可以完全依據使用者需求進行自定義鑒權和授權;
- 支援的用戶端語言種類多:除了 Java 之外,還有:C/C++,.NET,Perl,PHP,Python,Ruby;
- 代理叢集:多個 ActiveMQ 代理可以組成一個叢集來提供服務;
- 異常簡單的管理:ActiveMQ 是以開發者思維被設計的。是以,它并不需要專門的管理者,因為它提供了簡單又使用的管理特性。有很多中方法可以監控 ActiveMQ 不同層面的資料,包括使用在 JConsole 或者 ActiveMQ 的Web Console 中使用 JMX,通過處理 JMX 的告警消息,通過使用指令行腳本,甚至可以通過監控各種類型的日志。
- 跨平台(JAVA編寫與平台無關有,ActiveMQ幾乎可以運作在任何的JVM上)
- 可以用JDBC:可以将資料持久化到資料庫。雖然使用JDBC會降低ActiveMQ的性能,但是資料庫一直都是開發人員最熟悉的存儲媒體。将消息存到資料庫,看得見摸得着。而且公司有專門的DBA去對資料庫進行調優,主從分離;
- 支援JMS :支援JMS的統一接口;
- 支援自動重連;
- 有安全機制:支援基于shiro,jaas等多種安全配置機制,可以對Queue/Topic進行認證和授權。
- 監控完善:擁有完善的監控,包括Web Console,JMX,Shell指令行,Jolokia的REST API;
- 界面友善:提供的Web Console可以滿足大部分情況,還有很多第三方的元件可以使用,如hawtio;
- 區活躍度不及RabbitMQ高;
- 根據其他使用者回報,會出莫名其妙的問題,會丢失消息;
- 目前重心放到activemq6.0産品-apollo,對5.x的維護較少;
- 不适合用于上千個隊列的應用場景;
RocketMQ
出自 阿裡公司的開源産品,用 Java 語言實作,在設計時參考了 Kafka,并做出了自己的一些改進,消息可靠性上比 Kafka 更好。RocketMQ在阿裡集團被廣泛應用在訂單,交易,充值,流計算,消息推送,日志流式處理,binglog分發等場景。
- 是一個隊列模型的消息中間件,具有高性能、高可靠、高實時、分布式特點;
- Producer、Consumer、隊列都可以分布式;
- Producer向一些隊列輪流發送消息,隊列集合稱為Topic,Consumer如果做廣播消費,則一個consumer執行個體消費這個Topic對應的所有隊列,如果做叢集消費,則多個Consumer執行個體平均消費這個topic對應的隊列集合;
- 能夠保證嚴格的消息順序;
- 提供豐富的消息拉取模式;
- 高效的訂閱者水準擴充能力;
- 實時的消息訂閱機制;
- 億級消息堆積能力;
- 較少的依賴;
- 單機支援 1 萬以上持久化隊列
- RocketMQ 的所有消息都是持久化的,先寫入系統 PAGECACHE,然後刷盤,可以保證記憶體與磁盤都有一份資料,
通路時,直接從記憶體讀取。
- 模型簡單,接口易用(JMS 的接口很多場合并不太實用);
- 性能非常好,可以大量堆積消息在broker中;
- 支援多種消費,包括叢集消費、廣播消費等。
- 各個環節分布式擴充設計,主從HA;
- 開發度較活躍,版本更新很快。
支援的用戶端語言不多,目前是java及c++,其中c++不成熟;
RocketMQ社群關注度及成熟度也不及前兩者;
沒有web管理界面,提供了一個CLI(指令行界面)管理工具帶來查詢、管理和診斷各種問題;
沒有在 mq 核心中去實作JMS等接口;
Kafka
Apache Kafka是一個分布式消息釋出訂閱系統。它最初由LinkedIn公司基于獨特的設計實作為一個分布式的送出日志系統( a distributed commit log),,之後成為Apache項目的一部分。Kafka系統快速、可擴充并且可持久化。它的分區特性,可複制和可容錯都是其不錯的特性。
- 快速持久化,可以在O(1)的系統開銷下進行消息持久化;
- 高吞吐,在一台普通的伺服器上既可以達到10W/s的吞吐速率;
- .完全的分布式系統,Broker、Producer、Consumer都原生自動支援分布式,自動實作負載均衡;
- 支援同步和異步複制兩種HA;
- 支援資料批量發送和拉取;
- zero-copy:減少IO操作步驟;
- 資料遷移、擴容對使用者透明;
- 無需停機即可擴充機器;
- 其他特性:嚴格的消息順序、豐富的消息拉取模型、高效訂閱者水準擴充、實時的消息訂閱、億級的消息堆積能力、定期删除機制;
- 用戶端語言豐富,支援java、.net、php、ruby、python、go等多種語言;
- 性能卓越,單機寫入TPS約在百萬條/秒,消息大小10個位元組;
- 提供完全分布式架構, 并有replica機制, 擁有較高的可用性和可靠性, 理論上支援消息無限堆積;
- 支援批量操作;
- 消費者采用Pull方式擷取消息, 消息有序, 通過控制能夠保證所有消息被消費且僅被消費一次;
- 有優秀的第三方Kafka Web管理界面Kafka-Manager;
- 在日志領域比較成熟,被多家公司和多個開源項目使用;
- Kafka單機超過64個隊列/分區,Load會發生明顯的飙高現象,隊列越多,load越高,發送消息響應時間變長
- 使用短輪詢方式,實時性取決于輪詢間隔時間;
- 消費失敗不支援重試;
- 支援消息順序,但是一台代理當機後,就會産生消息亂序;
- 社群更新較慢;

消息隊列的pull和push
這麼多的消息隊列,有一個差別很重要,那就是模式,到底是pull好還是push好,下面就讓我們來一探究竟吧
Push
Push即服務端主動發送資料給用戶端。在服務端收到消息之後立即推送給用戶端。
Push模型最大的好處就是實時性。因為服務端可以做到隻要有消息就立即推送,是以消息的消費沒有“額外”的延遲。
但是Push模式在消息中間件的場景中會面臨以下一些問題:
- 在Broker端需要維護Consumer的狀态,不利于Broker去支援大量的Consumer的場景
- Consumer的消費速度是不一緻的,由Broker進行推送難以處理不同的Consumer的狀況
- Broker難以處理Consumer無法消費消息的情況(Broker無法确定Consumer的故障是短暫的還是永久的)
- 大量的推送消息會加重Consumer的負載或者沖垮Consumer
Pull模式可以很好的應對以上的這些場景。
Pull
Pull模式由Consumer主動從Broker擷取消息。
這樣帶來了一些好處:
- Broker不再需要維護Consumer的狀态(每一次pull都包含了其實偏移量等必要的資訊)
- 狀态維護在Consumer,是以Consumer可以很容易的根據自身的負載等狀态來決定從Broker擷取消息的頻率
Pull模式還有一個好處是可以聚合消息。
因為Broker無法預測寫一條消息産生的時間,是以在收到消息之後隻能立即推送給Consumer,是以無法對消息聚合後再推送給Consumer。 而Pull模式由Consumer主動來擷取消息,每一次Pull時都盡可能多的擷取已近在Broker上的消息。
但是,和Push模式正好相反,Pull就面臨了實時性的問題。
因為由Consumer主動來Pull消息,是以實時性和Pull的周期相關,這裡就産生了“額外”延遲。如果為了降低延遲來提升Pull的執行頻率,可能在沒有消息的時候産生大量的Pull請求(消息中間件是完全解耦的,Broker和Consumer無法預測下一條消息在什麼時候産生);如果頻率低了,那延遲自然就大了。
另外,Pull模式狀态維護在Consumer,是以多個Consumer之間需要互相協調,這裡就需要引入ZK或者自己實作NameServer之類的服務來完成Consumer之間的協調。
有沒有一種方式,能結合Push和Pull的優勢,同時變各自的缺陷呢?答案是肯定的。
Long-Polling
使用long-polling模式,Consumer主動發起請求到Broker,正常情況下Broker響應消息給Consumer;在沒有消息或者其他一些特殊場景下,可以将請求阻塞在服務端延遲傳回。
long-polling不是一種Push模式,而是Pull的一個變種。
那麼:
- 在Broker一直有可讀消息的情況下,long-polling就等價于執行間隔為0的pull模式(每次收到Pull結果就發起下一次Pull請求)。
- 在Broker沒有可讀消息的情況下,請求阻塞在了Broker,在産生下一條消息或者請求“逾時之前”響應請求給Consumer。
以上兩點避免了多餘的Pull請求,同時也解決Pull請求的執行頻率導緻的“額外”的延遲。
注意上面有一個概念:“逾時之前”。每一個請求都有逾時時間,Pull請求也是。“逾時之前”的含義是在Consumer的“Pull”請求逾時之前。
基于long-polling的模型,Broker需要保證在請求逾時之前傳回一個結果給Consumer,無論這個結果是讀取到了消息或者沒有可讀消息。
因為Consumer和Broker之間的時間是有偏差的,且請求從Consumer發送到Broker也是需要時間的,是以如果一個請求的逾時時間是5秒,而這個請求在Broker端阻塞了5秒才傳回,那麼Consumer在收到Broker響應之前就會判定請求逾時。是以Broker需要保證在Consumer判定請求逾時之前傳回一個結果。
通常的做法時在Broker端可以阻塞請求的時間總是小于long-polling請求的逾時時間。比如long-polling請求的逾時時間為30秒,那麼Broker在收到請求後最遲在25s之後一定會傳回一個結果。中間5s的內插補點來應對Broker和Consumer的始終存在偏差和網絡存在延遲的情況。 (可見Long-Polling模式的前提是Broker和Consumer之間的時間偏差沒有“很大”)
Long-Polling還存在什麼問題嗎,還能改進嗎?
Dynamic Push/Pull
“在Broker一直有可讀消息的情況下,long-polling就等價于執行間隔為0的pull模式(每次收到Pull結果就發起下一次Pull請求)。”
這是上面long-polling在服務端一直有可消費消息的處理情況。在這個情況下,一條消息如果在long-polling請求傳回時到達服務端,那麼它被Consumer消費到的延遲是:
假設Broker和Consumer之間的一次網絡開銷時間為R毫秒,
那麼這條消息需要經曆3R才能到達Consumer
第一個R:消息已經到達Broker,但是long-polling請求已經讀完資料準備傳回Consumer,從Broker到Consumer消耗了R
第二個R:Consumer收到了Broker的響應,發起下一次long-polling,這個請求到達Broker需要一個R
的時間
第三個R:Broker收到請求讀取了這條資料,那麼傳回到Consumer需要一個R的時間
是以總共需要3R(不考慮讀取的開銷,隻考慮網絡開銷)
另外,在這種情況下Broker和Consumer之間一直在進行請求和響應(long-polling變成了間隔為0的pull)。
考慮這樣一種方式,它有long-polling的優勢,同時能減少在有消息可讀的情況下由Broker主動push消息給Consumer,減少不必要的請求。