【寫在前面】
消息隊列中間件(簡稱消息中間件)是指利用高效可靠的消息傳遞機制進行與平臺無關的數據交流,並基於數據通信來進行分佈式系統的集成。
它具有低耦合、可靠投遞、廣播、流量控制、最終一致性等一系列功能,是異步RPC的主要手段之一。
作爲分佈式系統架構中的一個重要組件,消息中間件的選型,變成了一個炙手可熱的話題。
【消息隊列的種類】
現在,市面上的消息隊列種類可謂是種類繁多,主流的消息隊列差不多有以下幾種:
- Kafka
由LinkedIn公司採用Scala語言開發的一個分佈式、多分區、多副本,基於zookeeper協調的分佈式消息系統「現在最新版本的Kafka已經可以不依賴ZK獨立運行」,現已捐獻給Apache基金會。
它是一種高吞吐量的分佈式發佈訂閱消息系統,以可水平擴展和高吞吐率而被廣泛使用。
目前很多的開源分佈式處理系統如Cloudera、Apache Storm、Spark、Flink等都支持與Kafka集成。
- RabbitMQ
採用Erlang語言實現的AMQP協議的消息中間件,最初起源於金融系統,用於在分佈式系統中存儲轉發消息。
使用場景非常豐富,被廣泛應用於雲相關的架構中,如:OpenStack。
- RocketMQ
阿里出品,已經捐獻個Apache基金會。
Java編寫,具備高吞吐量、高可用性、適合大規模分佈式系統應用等特點。
- ActiveMQ
Apache出品,使用Java編寫,市場份額並不高,最新架構被命名爲Apollo。
- ZeroMQ
號稱史上最快的消息隊列,基於C語言開發。
可在多線程、多內核和主機之間彈性伸縮,雖然大多數時候我們習慣將其歸類於消息隊列,但是其和前面的幾種有着本質的區別,ZeroMQ本身就不是一個消息隊列服務器,更像是一組底層網絡通訊庫,對原有的Socket API上加了一層封裝。
【幾種特殊的消息隊列】
- 優先級隊列
普通的消息隊列,一般是先進先出,但是有些時候,需要將部分重要的消息後進先出,優先級隊列就起作用了。
優先級隊列主要用於消息積壓時,優先級較高的系統的消息需要被更早消費的場景。
- 延時隊列
對於一部分場景,並不希望消費者可以立即拿到消息盡心消費,而是等待一段時間再進行消費,對隊列進行延遲消費,比如:
-
關閉空閒連接。
服務器中,有很多客戶端的連接,空閒一段時間之後需要關閉之。
-
清理過期數據業務。
比如緩存中的對象,超過了空閒時間,需要從緩存中移出。
-
任務超時處理。
在網絡協議滑動窗口請求應答式交互時,處理超時未響應的請求。
下單之後如果三十分鐘之內沒有付款就自動取消訂單。
對於延時任務的實現方式,一般會有兩種:
- 基於消息的延時,對每條消息記錄延時時間,在同一個隊列中記錄,然後排序「性能較差,每次入隊列都需要重新排序」
- 基於隊列的延時,針對不同時長的延時,放入不同的隊列
- 死信隊列
由於某些原因消息無法被正確的投遞,爲了確保消息不會被無故的丟棄,一般將其置於一個特殊角色的隊列,這個隊列一般稱之爲死信隊列。
變成死信隊列一般有以下幾種情況:
- 消息被拒絕(basic.reject / basic.nack),並且requeue = false
- 消息TTL過期
- 隊列達到最大長度
- 回退隊列
- 重試隊列
重試隊列可以看做是一種特殊的回退隊列。
在消費端進行消費的時候,爲防止消費失敗導致消息直接被未執行而丟棄,所以在消費失敗後重新將其加入到隊列中,等待消費者再次消費。
很多重試隊列都會設置間隔時間的策略,可以設置每隔10s重試一次,也可以設置第一次隔5s,第二次隔10s,第三次隔30s,避免系統的短暫故障導致隊列重試次數快速增長。
重試越多次重新投遞的時間就越久,爲此需要設置一個上限,超過投遞次數就入死信隊列。
【消息模式】
- 消費模式
消費模式分爲推(push)模式和拉(pull)模式。
- 推模式------由Broker主動推送消息至消費端,實時性較好,不過需要一直保持長連接「可能會浪費網絡帶寬」,且需要對應的流制機制來確保服務端推送過來的消息不會壓垮消費端。
- 拉模式------消費端主動向Broker端請求拉取(一般是定時或者定量)消息,實時性較差,但是可以根據自身的處理能力而控制拉取的消息量。
- 傳遞模式
消息隊列一般有兩種傳遞模式:
-
點對點模式
一條消息只會被一個消費者消費,消費以後隊列中將不會繼續存儲詞條消息。
-
發佈/訂閱模式
這種模式下,有一個核心的概念叫做Topic,生產者發佈攜帶某個Topic的消息,消費者消費攜帶某個Topic的消息。
Topic的存在,解耦了生產者和消費者,不需要進行相互接觸即可成功進行生產和消費。
RabbitMQ是一種典型的點對點模式,而Kafka是一種典型的發佈訂閱模式。
但是RabbitMQ中可以通過設置交換器類型來實現發佈訂閱模式而達到廣播消費的效果,Kafka中,也可以通過設置消費者組實現點對點的效果。
【消息存儲】
- 消息回溯
一般消息在消費完成後就被註銷了。
消息回溯指的是,在消息被消費之後,依舊能再次消費到這個消息。消息回溯的主要目的是復現舊場景。
此外,很多其他功能也會依賴消息回溯,比如:索引恢復,本地緩存重建,補償機制等。
- 消息堆積 + 持久化
消息堆積功能,直接決定了消息是否支持削峯。
消費者的能力都是有限的,當大量消息被生產的時候,如果沒有消息堆積,消息隊列可能會直接崩潰。
消息堆積也有兩種實現方式:
- 內存堆積------RabbitMQ是一種典型的內存堆積實現堆積的消息隊列,在內存達到一定量級之後,會堆積到磁盤中「換頁存儲,影響吞吐,但是內存堆積本身效率很高」。
- 磁盤堆積------Kafka是一種典型的磁盤堆積的消息隊列,磁盤的容量是很大的,可以認爲是無限大,但是磁盤相對內存來講,讀取速度會慢一些。
- 消息追蹤
在分佈式系統中,消息追蹤的功能非常重要,有了消息追蹤,我們可以做到對消息的全鏈路跟蹤,進而對定位問題有很好的幫助。
【附加功能】
- 流量控制
流量控制(flow control)針對的是發送方和接收方速度不匹配的問題,提供一種速度匹配服務抑制發送速率使接收方應用程序的讀取速率與之相適應。通常的流控方法有Stop-and-wait、滑動窗口以及令牌桶等。
- 順序消費
保證消息有序。這個功能有個很常見的應用場景就是CDC(Change Data Chapture),以MySQL爲例,如果其傳輸的binlog的順序出錯,比如原本是先對一條數據加1,然後再乘以2,發送錯序之後就變成了先乘以2後加1了,造成了數據不一致。
- 冪等
確保消息在生產者和消費者之間進行傳輸而言一般有三種傳輸保障(delivery guarantee):
- At most once,至多一次,消息可能丟失,但絕不會重複傳輸;
- At least once,至少一次,消息絕不會丟,但是可能會重複;
- Exactly once,精確一次,每條消息肯定會被傳輸一次且僅一次。
對於大多數消息中間件而言,一般只提供At most once和At least once兩種傳輸保障,對於第三種一般很難做到,由此消息冪等性也很難保證。
- 事務
事務對於使用過數據庫的同學來講並不陌生,要麼全部成功,要麼全部失敗。
而對於消息中間件來講,事務概念指的是要麼發送成功,要麼發送失敗。
【協同和隔離】
- 安全
在Kafka 0.9版本之後增加了身份認證和權限控制兩種安全機制。
身份認證是指客戶端與服務端連接進行身份認證,包括客戶端與Broker之間、Broker與Broker之間、Broker與ZooKeeper之間的連接認證。
目前支持SSL、SASL等認證機制。
權限控制是指對客戶端的讀寫操作進行權限控制,包括對消息或Kafka集羣操作權限控制。
權限控制是可插拔的,並支持與外部的授權服務進行集成。
對於RabbitMQ而言,其同樣提供身份認證(TLS/SSL、SASL)和權限控制(讀寫操作)的安全機制。
- 多租戶
在軟件架構設計中,很多時候我們需要考慮到多用戶環境下,不同用戶之間數據的相互隔離。
RabbitMQ支持多租戶技術, 每個租戶叫一個vhost。
- vhost本質上是一個mini版的RabbitMQ服務器,擁有自己的隊列、綁定、交換器和權限控制;
- vhost通過在各個實例間提供邏輯上分離,允許你爲不同應用程序安全保密地運行數據;
- 多協議支持
一般消息層面的協議有AMQP、MQTT、STOMP、XMPP等(消息領域中的JMS更多的是一個規範而不是一個協議),支持的協議越多其應用範圍就會越廣,通用性越強。
有的消息協議是基於本身的私有協議運轉的。「Kafka」
- 跨語言支持
支持多種語言,也在一定程度上決定了消息隊列是適用性。
其實,現在很多的中間件,都在向語言無關的方向發展,其實也是一種趨勢。
【比較】
下圖對市面上流行的消息中漸漸做了一個比較:
功能 |
Kafka |
RabbitMQ |
ZeroMQ |
RocketMQ |
ActiveMQ |
支持的協議 |
自己定義的一套(基於TCP的協議) |
AMQP |
TCP/UDP |
自己定義的一套 |
OpenWire、STOMP、REST、XMPP、AMQP |
存儲方式 |
內存、磁盤、數據庫。支持大量堆積。 |
內存、磁盤。支持少量堆積。 |
消息發送端的內存或者磁盤中。不支持持久化。 |
磁盤。支持大量堆積。 |
內存、磁盤、數據庫。支持少量堆積。 |
事務支持 |
支持 |
支持,使用事務會使性能有所下降 |
不支持 |
支持 |
支持 |
負載均衡 |
支持負載均衡 |
對負載均衡的支持不好 |
去中心化,不支持負載均衡,本身只是一個多線程網絡庫 |
支持負載均衡 |
支持負載均衡,需要通過集成zookeeper來實現負載均衡 |
集羣 |
天然的 Leader-Slave 無狀態集羣,每臺服務器既是Master也是Slave。 |
支持簡單集羣,複製模式,對高級集羣模式支持不好。 |
去中心化,不支持集羣。 |
常用多對 Master-Slave 模式,開源版本需手動切換Slave變成Master。集羣的slave會從master拉取數據備份,master分佈在不同的broker上。 |
支持簡單集羣模式,比如'主-備',對高級集羣模式支持不好。 |
高可用 |
非常高(分佈式) |
高(主從) |
高 |
非常高(分佈式) |
高(主從) |
消息重複 |
支持at least once、at most once |
支持at least once、at most once |
既不支持at least once、也不支持at most once、更不支持exactly only once |
支持at least once |
支持at least once |
吞吐量(TPS) |
灰常大,Kafka 按批次發送消息和消費消息 |
比較大 |
非常大 |
大,rocketMQ 接收端可以批量消費消息,可以配置每次消費的消息數,但是發送端不是批量發送。 |
比較大 |
消息訂閱與發佈 |
基於topic以及按照topic進行正則匹配的發佈訂閱模式 |
提供了4種:direct, topic ,Headers和fanout |
點對點(P2P) |
基於topic/messageTag以及按照消息類型、屬性進行正則匹配的發佈訂閱模式 |
點對點(P2P)、廣播(發佈-訂閱) |
消息順序支持 |
支持 |
不支持 |
不支持 |
支持 |
不支持 |
消息確認支持 |
支持 |
支持 |
支持 |
支持 |
支持 |
消息回溯支持 |
支持指定分區offset位置的回溯 |
不支持 |
不支持 |
支持指定時間點的回溯 |
不支持 |
消息重試支持 |
不支持,但是可以實現 |
不支持,但是可以利用消息確認機制實現 |
不支持 |
支持 |
不支持 |
併發度 |
高 |
極高 |
高 |
高 |
高 |
管理界面 |
一般 |
好 |
無 |
無 |
一般 |
【性能】
對於消息隊列來講,性能的重要性,有時候比功能還要高。
從上面那張表中我們可以看出,總體來講,RabbitMQ在功能的健全性上面,是要比Kafka更強的,但是Kafka的吞吐量,是比RabbitMQ高出數十倍的。一般來講,單機RabbitMQ的QPS在萬級別以內,但是Kafka的單機QPS,號稱可以達到百萬級。
注:吞吐量是會受限於硬件設施的,如:網絡帶寬,磁盤讀寫速度等。 |
作爲性能的一大考察點,時延一般會被人們所忽視,原因在於:對時延有要求的系統,一般都不會使用消息隊列去進行系統間通信,而是直接通過RPC調用去實現。消息中間件的消息堆積能力,就註定了消息是可接受時延的。
【可靠性】
對於消息中間件而言,每個消息中間件都存在消息丟失的問題。
對於Kafka而言,是通過ISR協議來保證消息的可靠:
在zk中會保存AR(Assigned Replicas)列表,其中包含了分區所有的副本,其中 AR = ISR+OSR ISR(in sync replica) 是kafka動態維護的一組同步副本。 在ISR中有成員存活時,只有這個組的成員纔可以成爲leader,內部保存的爲每次提交信息時必須同步的副本(acks = all時),每當leader掛掉時,在ISR集合中選舉出一個follower作爲leader提供服務,當ISR中的副本被認爲壞掉的時候,會被踢出ISR,當重新跟上leader的消息數據時,重新進入ISR。 OSR(out sync replica) 保存的副本不必保證必須同步完成才進行確認。 OSR內的副本是否同步了leader的數據,不影響數據的提交,OSR內的follower盡力的去同步leader,可能數據版本會落後。 |
而對於RabbitMQ而言,需要有兩個層面的保障:
- 對於生產者而言:
-
可以開啓事務功能
如果消息沒有成功被 RabbitMQ 接收到,那麼生產者會收到異常報錯,此時就可以回滾事務channel.txRollback,然後重試發送消息。
但是問題在於,事務機制太過於損耗性能,所以一般還可以採用下面這種方式。
-
開啓confirm模式
在生產者開啓confirm模式之後,每次寫的消息都會分配一個唯一的id,如果寫入成功,則會回傳一個ack消息,如果沒能處理,則會回調一個nack接口,然後生產者即會重試。
在這個機制下,可以自己在內存中維護每個消息id的狀態,如果超出一定時間還沒有收到此消息的回調,就可以重發。
-
而對於MQ本身而言:
需要開啓RabbitMQ的持久化,在消息寫入之後會持久化到磁盤,哪怕是RabbitMQ自己掛了,恢復之後,會自動從磁盤讀取之間的數據。
這樣的話,就和Kafka很類似了,Kafka就是把消息直接寫入到磁盤的。
【管理和運維】
在消息中間件的使用過程中,不可避免地會出現各種各樣的問題,比如前文中提到的消息的丟失,性能的監控,橫向擴容,日常告警,灰度發佈,災備處理等。
所以我們在選用一個消息中間件的時候,還需要考慮到這些擴展能力是否夠用,如果不夠用,是否支持定製化開發,擴展插件等等。
這些非核心能力點對於用戶來講也是非常重要的,而對於這些功能點來講,在很大程度上其實取決於社區的活躍度和生態的發展,所以,在做技術選型的時候,最好選用當下比較熱門以及有大公司做背書的技術,這樣後續的版本更新和漏洞修復都有保障。
【自研】
對於很多大公司而言,爲了滿足自己個性化的需求,一般都會採用先自研,後開源的方式,比如LinkedIn的Kafka,阿里的RocketMQ等等,其實當時市面上已經有相對好用的產品如:ActiveMQ,RabbitMQ,但是每個公司的設計理念和產品需求都是不一樣的,所以,對於大廠而言,自研也是一條不錯的路。
但是無論是採用市面上開源的產品去進行定製化開發,還是自己去從0開始平地起高樓,我們都要先問自己幾個問題:
我們是否真的需要自研?無論是消費模式,還是消息存儲,或者是協議的支持,市面上的產品是不是真的不滿足我們的要求?
若要自研,我們的具體場景是什麼?採用什麼語言設計?若採用Java,可不可以,對Java中的ArrayBlockingQueue做一個簡單的封裝,或者基於文件、數據庫、Redis等底層存儲封裝而形成一個消息中間件。亦或者,根據Kafka或者RabbitMQ的可擴展性,來選擇性的擴展。
對於JD而言,其實已經有了衆多的消息隊列產品如:***,***,***,***等等,品類繁多,尚未開源。
每個產品都有每個產品適應的場景,但是對於同一家企業來講,難免有重複造輪子的嫌疑,筆者也曾遍歷了以上所述的這些所有產品,其實在基本功能上,相似度是很高的,所以有一個大膽的想法,我們可以集中開發投入做一個基本的消息隊列系統,然後在擴展性上做一些響應的側重,這樣,每個產品在自己的個性化需求上面可以有自己的設計,但是都使用同一個內核,這樣是否可以在降本增效上面更進一步?都是我們需要考慮的問題。