MQ系列2:消息中間件的技術選型
1 背景
在高並發、高消息吞吐的互聯網場景中,我們經常會使用消息隊列(Message Queue)作為基礎設施,在服務端架構中擔當消息中轉、消息削峰、事務非同步處理 等職能。
對於那些不需要實時響應的的業務,我們都可以放在消息隊列中進行傳輸。下面是用戶在進行系統註冊的時候場景,充分體現MQ的作用
可以看到用戶註冊的過程步驟1+步驟2,從請求到響應總共耗時 55 ms。消息消費+簡訊發送的時間比較長,從上面看花了5s多,一般讓消息隊列服務去處理,用戶靜靜等待簡訊送達即可。
消息隊列中間件(簡稱消息中間件)是指利用高效可靠的消息傳遞機制進行與平台無關的數據交流,並基於數據通訊來進行分散式系統的集成。通過提供消息傳遞和消息排隊模型,它可以在分散式環境下提供應用解耦、
彈性伸縮、冗餘存儲、流量削峰、非同步通訊、數據同步等等功能,其作為分散式系統架構中的一個重要組件,有著舉足輕重的地位。
2 消息中間件的組成
Broker:消息伺服器,以服務的形式運行在server端,給各個業務系統提供核心消息數據的中轉服務。
Producer:消息生產者,業務的發起方,負責生產消息傳輸給broker。
Consumer:消息消費者,業務的處理方,負責從broker獲取消息並進行業務邏輯處理
Topic:主題模組,發布/訂閱模式下的消息統一彙集地,不同生產者向topic發送消息,由MQ伺服器分發到不同的訂閱者,實現消息的廣播
Queue:隊列,PTP模式下,特定生產者向特定queue發送消息,消費者訂閱特定的queue完成指定消息的接收。
Message:消息體,根據不同通訊協議定義的固定格式進行編碼的數據包,來封裝業務數據,實現消息的傳輸。
這邊以kafka為例子,這是典型的集群模式,Kafka通過Zookeeper管理集群配置,選舉leader,以及在Consumer Group發生變化時進行rebalance。Producer使用push模式將消息發布到broker,Consumer使用pull模式從broker訂閱並消費消息。
- producer 負責生產消息
- consumer 負責消費消息
- broker 消息伺服器,提供消息核心的處理工作
- zookeeper 用於生產者和消費者的註冊與發現
3 消息中間件的模式分類
PTP點對點:使用queue作為通訊載體
消息生產者生產消息發送到queue中,然後消息消費者從queue中取出並且消費消息。
不可重複消費,消息被消費以後,queue中不再存儲,所以消息消費者不可能消費到已經被消費的消息。 Queue支援存在多個消費者,但是對一個消息而言,只會有一個消費者可以消費。
Pub/Sub發布訂閱(廣播):使用topic作為通訊載體
消息生產者(發布)將消息發布到topic中,同時有多個消息消費者(訂閱)消費該消息。和點對點方式不同,發布到topic的消息會被所有訂閱者消費,所以從1到N個訂閱者都能得到這個消息的拷貝。
4 消息中間件的優勢
系統解耦:交互系統之間沒有直接的調用關係,只是通過消息傳輸,故系統侵入性不強,耦合度低。
削峰、提高系統響應時間:例如原來的一套邏輯,可將緊急重要(需要立刻響應)的業務放到該調用方法中,響應要求不高的使用消息隊列,放到MQ隊列中,供消費者處理。
業務的有序性處理:先來先處理,比如一個系統處理某件事需要很長一段時間,但是在處理這件事情時候,有其他人也發出了請求,可以把請求放在消息隊里,一個一個來處理
為大數據處理架構提供服務:通過消息作為整合,大數據的背景下,消息隊列還與實時處理架構整合,為數據處理提供性能支援。
5 消息中間件常用協議
6 豐富強大的消息中間件生態
目前開源的消息中間還是很豐富的,大家用的比較多的比如 ActiveMQ、RabbitMQ、Kafka、RocketMQ、ZeroMQ 等。
但是每個人的業務場景不一樣,受限於系統的規模,業務的取捨(如延遲容忍度,死信、重試的需求,可持久化需求),並不是每一款消息中間件都能滿足你的需求。
除了個別大廠會進行自研(如 阿里的Rocket MQ、滴滴的DD MQ)之外,大部分同學還是要對選型有一些思考的。各自都有各自的側重點,選擇合適自己、揚長避短無疑是最好的方式。
6.1 主流MQ介紹
下面基於受眾程度,對三款主流的MQ做介紹,通過各項指標上的對比,給出我們在實際應用場景中的建議。
RabbitMQ:
採用 Erlang 語言實現的 AMQP 協議的消息中間件,起源於金融系統,廣泛應用在分散式系統中,承擔消息轉發的職責。RabbitMQ 發展歷史比較久遠,影響範圍比較大,被很多開發者認可,在可靠性、可用性、可擴展性、功能性方面有著非凡表現。
RocketMQ:
阿里開源的消息中間件,目前已經捐獻給 Apache 基金會,它是由 Java 語言開發的,具備高吞吐量、高可用性、適合大規模分散式系統應用等特點。並且在阿里的雙11、618等重要活動中經受住了考驗。
Kafka:
起初是由 LinkedIn 公司採用 Scala 語言開發的一個分散式、多分區、多副本且基於 zookeeper 協調的分散式消息系統,現已捐獻給 Apache 基金會。它是一種高吞吐量的分散式發布訂閱消息系統,以可水平擴展和高吞吐率而被廣泛使用。
目前越來越多的開源分散式處理系統如 Cloudera、Apache Storm、Spark、Flink 等都支援與 Kafka 集成。
6.2 主流MQ對比
特性
|
RabbitMQ
|
RocketMQ
|
kafka
|
開發語言
|
erlang
|
java
|
scala
|
支援協議
|
AMQP
|
自定義
|
基於TCP 自定義
|
消息存儲能力
|
記憶體、磁碟。支援少量堆積。
|
磁碟。支援大量堆積。
|
記憶體、磁碟、資料庫。支援大量堆積。
|
消息事務性
|
支援(信道設置事務模式,性能有影響)
|
支援
|
支援
|
單機吞吐量
|
萬級
|
10萬級+
|
10萬級+
|
時效性
|
us級
|
ms級
|
ms級以內
|
消息重複
|
支援at least once、at most once |
支援at least once
|
支援at least once、at most once |
消息回溯
|
不支援
|
支援指定時間點的回溯
|
支援指定分區offset位置的回溯
|
消息重試
|
不支援,但可以設置autoACK=false,未收到確認的會重入隊列
|
支援
|
不支援,但可以通過消息回溯的方式來實現
|
可用性
|
高(主從架構)
|
非常高(分散式架構)
|
非常高(分散式架構)
|
功能特性說明
|
基於erlang開發,所以並發能力很強,性能極其好,延時很低;
管理介面較豐富
|
MQ功能比較完備,擴展性佳
|
只支援主要的MQ功能,像一些消息查詢,消息回溯等功能支援的不是很強,在大數據領域應用廣。
|
6.3 選型建議
6.3.1 系統建設規模角度
中小型系統建議選用RabbitMQ,數據量相對較小,選型應首選功能比較完備的,所以kafka排除。RocketMQ是阿里出品,如果阿里放棄維護,中小型公司一般很難投入人力進行RocketMQ的訂製化開發,因此不推薦。
6.3.2 業務規模角度
6.3.3 功能性層面選型
功能項 | Kafka(1.1.0+) | RabbitMQ(3.6.10+) |
優先順序隊列 | 不支援 | 支援:具有優先被消費的特權,建議優先順序大小設置在10以內,否則價值不大 |
延遲隊列 | 不支援 | 支援 |
死信隊列 | 不支援 | 支援:保存無法被正確投遞的消息,避免消息被無端丟棄。 |
重試模式 | 不支援 |
不支援:RabbitMQ中可以參考延遲隊列實現一個重試隊列,需要再封裝一下,也不是太難。 如果要在kafka中實現重試隊列,首先要實現延遲隊列的功能,相對比較複雜。 |
消費模式 | 拉 模式 | 推+拉 模式 |
廣播消費(pub/sub) | 支援:kafka對廣播消費的支援比較強大 | 支援:能力相比較kafka 弱一些 |
消息回溯 | 支援:kafka可以按照 offset(偏移量)和 timestamp(時間戳) 兩種維度進行消息回溯。 | 不支援:RabbitMQ消息一旦被確認消費便丟棄 |
消息堆積 | 支援 |
支援:記憶體堆積過大會影響性能,如果僅考慮吞吐量因素,kafka的堆積效率比RabbitMQ總體高很多。 |
持久化 | 支援 | 支援 |
消息追蹤 | 不支援:消息追蹤可以通過外部系統來支援,但是支援粒度肯定沒有內置的細膩 |
支援:RabbitMQ中可以採用Firehose 或者 rabbitmq_tracing插件實現。
|
消息過濾 | 客戶端級別的支援 | 不支援,可以訂製化封裝 |
多租戶 | 不支援 | 支援 |
多協議支援 | 只支援自定義協議,目前幾個主流版本中存在兼容性問題。 | RabbitMQ本身就是AMQP協議的實現,同時支援MQTT、STOMP等協議 |
跨語言支援 | 採用Scala和Java編寫,支援多種語言的客戶端 | 採用Erlang編寫,支援多種語言的客戶端 |
流量控制 | 支援client和user級別,可將流控配置在生產者和消費者層面 | RabbitMQ的流控基於 Credit-Based 演算法,是內部被動觸發的保護機制,僅用於生產者層面。 |
消息順序性 | 支援單分區(partition)級別的順序性,在各自的分區中排序 |
順序性消費的條件比較苛刻,需要單執行緒發送、單執行緒消費,這樣吞吐量就下來了。 而且無法使用延遲隊列、優先隊列等一些高級功能,所以一般不使用。 |
安全機制 |
(TLS/SSL、SASL)身份認證和(讀/寫)許可權控制 |
與kafka相似 |
冪等性 | 單個生產者+單partition + 單會話 場景下,支援冪等性 | 不支援 |
事務性消息 | 支援 | 支援 |
- 優先順序隊列:可配置優先順序,優先順序高的消息具備優先被消費的特權,這樣可以為下游服務提供不同消息級別的保證。這種模式只是在生產效率高於消費效率的時候才有效果。如果消費者的消費速度大於生產者的速度,消息中間件伺服器(Broker)中沒有消息堆積,就不存在對待消費數據進行優先順序排序的需求了。
- 延遲隊列:延遲隊列會存儲對應的延遲消息,延遲消息是指消息被生產後,並不馬上消費,而是等待一定時間後,消費者才拿到消息進行消費。延遲隊列的模式分為兩種,基於消息的延遲和基於隊列的延遲。
- 基於消息的延遲是指為每條消息設置不同的延遲時間,那麼每當隊列中有新消息進入的時候就會重新根據延遲時間排序,但是這會對性能造成很大的影響。
- 基於隊列的延遲,設置不同延遲級別的隊列,如 15s、30s、1m、10m 等,每個隊列中消息的延遲時間都是相同的,這樣不需要消耗大量性能去做延遲時間排序,每個消息都有固定的投遞時間。
- 延遲隊列的常用的場景有以下幾種:
- 1、購買火車票提示:30分鐘之內未付款,將自動取消訂單!
- 2、雙11網購時,距離聚划算活動開始時間還有 17小時,到時全場5折優惠。
- 死信隊列:由於某些原因消息無法被正確的投遞,為了確保消息不會被無故的丟棄,一般會存儲到一個特殊的隊列中,我們稱之為死信隊列。與此對應的還有一個「回退隊列」的概念,試想如果消費者在消費時發生了異常,那麼就不會對這一次消費進行確認(Ack), 進而發生回滾消息的操作之後消息始終會放在隊列的頂部,然後不斷被處理和回滾,導致隊列陷入死循環。為了解決這個問題,可以為每個隊列設置一個回退隊列,它和死信隊列都是為異常的處理提供的一種機制保障。實際情況下,回退隊列的角色可以由死信隊列和重試隊列來扮演。
- 重試隊列:重試隊列其實可以看成是一種回退隊列,具體指消費端消費消息失敗時,為防止消息無故丟失而重新將消息回滾到 Broker 中。與回退隊列不同的是重試隊列一般分成多個重試等級,每個重試等級一般也會設置重新投遞延時,重試次數越多投遞延時就越大。比如第一次重試延遲時間為5s,再次消費失敗後延遲重試時間為10s,以此類推,重試越多次重新投遞的時間就越久。為了避免延遲時間被無限放大,需要有個重試次數限制,超過就寫入死信隊列。這邊需要注意:延遲隊列動作由內部觸發,重試隊列動作由外部消費端觸發。
- 消費模式:消費模式分為推(push)模式和拉(pull)模式。推模式是指由 Broker 主動推送消息至消費端,實時性較好,不過需要保證服務端推送的消息不會嚴重超過消費端消化能力。而拉模式是指消費端定時定量主動向 Broker 端請求拉取消息,雖然實時性較差,但是可以根據自身的消費能力來拉取。
- 廣播消費:消息一般有兩種發送模式:點對點(P2P,Point-to-Point)模式和發布/訂閱(Pub/Sub)模式。對於P2P模式而言,消息被消費以後,隊列中不會再存儲,即使有多個消費者,一條消息只會被一個消費者消費。而發布訂閱(Pub/Sub)模式定義了如何向一個內容節點發布和訂閱消息,這個內容節點稱為主題(topic),主題可以認為是消息傳遞的中介,消息發布者將消息發布到某個主題,而消息訂閱者則從主題中訂閱消息。主題使得消息的訂閱者與消息的發布者互相保持獨立,不需要進行接觸即可保證消息的傳遞,發布 / 訂閱模式在消息的一對多廣播時採用。RabbitMQ 是一種典型的點對點模式,而 Kafka 是一種典型的發布訂閱模式。
- 消息回溯:一般消息在消費完成之後就被處理了,之後再也不能消費到該條消息。消息回溯正好相反,是指消息在消費完成之後,還能追溯到之前被消費掉的消息。
- 消息堆積 + 持久化:進行流量的削峰填谷是消息中間件的一個核心功能,實現的能力主要體現在消息堆積能力上。消息堆積分記憶體式堆積和磁碟式堆積。RabbitMQ 是典型的記憶體式堆積,可以通過一些方式持久化到磁碟中,但是會降低一些性能。Kafka 是典型的磁碟式堆積,所有的消息都存儲在磁碟中,存儲容量是有了很大的提升,但是磁碟性能會比記憶體差很多。
- 消息追蹤:在消息中間件中,消息的鏈路追蹤非常重要,它可以對生產和消費過的消息進行trace追蹤。這樣,在出現故障的時候,就可以快速的定位問題。
- 消息過濾:消息過濾是指按照既定的過濾規則為下游用戶提供指定類別的消息。就以 kafka 而言,完全可以將不同類別的消息發送至不同的 topic 中,由此可以實現某種意義的消息過濾,或者 Kafka 還可以根據分區對同一個 topic 中的消息進行分類。不過更加嚴格意義上的消息過濾應該是對既定的消息採取一定的方式按照一定的過濾規則進行過濾。同樣以 Kafka 為例,可以通過客戶端提供的 ConsumerInterceptor 介面或者 Kafka Stream 的 filter 功能進行消息過濾。
- 流量控制:flow control,當生產者和消費者 處理速度不均衡問題,通過對生產者和消費者的限流,來保障兩者的均衡。通常的流控方法有 Stop-and-wait、滑動窗口以及令牌桶等。
- 消息順序性:順序性是指保證消息有序,特別是分散式場景下,有序的執行,是保證一致性 (Consistency)的前提。
- 消息冪等性:對於確保消息在生產者和消費者之間進行傳輸而言一般有三種傳輸保障(delivery guarantee):At most once,至多一次,消息可能丟失,但絕不會重複傳輸;At least once,至少一次,消息絕不會丟,但是可能會重複;Exactly once,精確一次,每條消息肯定會被傳輸一次且僅一次。對於大多數消息中間件而言,一般只提供 At most once 和 At least once 兩種傳輸保障,對於第三種一般很難做到,由此消息冪等性也很難保證。
- 事務性消息:原子性事務中的操作為一個整體,要麼都做,要麼都不做。即一旦出錯,就回滾事務,事務是由事務開始(Begin Transaction)和事務結束(End Transaction)之間執行的全體操作組成。Kafka 和 RabbitMQ 都支援,不過僅僅指的是生產者發送消息是一個事務性操作,要麼發送成功,要麼發送失敗。
6.3.4 性能層面
功能維度是消息中間件選型中的一個重要的參考維度,但性能也是考慮的一個重要環節。
吞吐量角度:Kafka 在開啟冪等、事務功能的時候會使其性能降低,RabbitMQ 在開啟 rabbitmq_tracing 插件的時候也會極大的影響其性能。消息中間件的性能一般是指其吞吐量,雖然從功能維度上來說,RabbitMQ 的優勢要大於 Kafka,但是 Kafka 的吞吐量要比 RabbitMQ 高出 1 至 2 個數量級,一般 RabbitMQ 的單機 QPS 在萬級別之內,而 Kafka 的單機 QPS 可以維持在十萬級別,甚至可以達到百萬級。
時延角度:另外一個是時延,作為性能維度的一個重要指標,卻往往在消息中間件領域所被忽視,因為一般使用消息中間件的場景對時效性的要求並不是很高,如果要求時效性完全可以採用 RPC 的方式實現。消息中間件具備消息堆積的能力。Kafka是ms以內,RabbitMQ是us級別的。
6.3.5 高可用角度
高可用角度是指系統的出錯概率和無故障運行時長。
如消息丟失,是使用消息中間件時所不得不面對的一個同點,其背後消息可靠性也是衡量消息中間件好壞的一個關鍵因素。尤其是在金融支付領域,消息可靠性尤為重要。然而說到可靠性必然要說到可用性,注意這兩者之間的區別,消息中間件的可靠性是指對消息不丟失的保障程度;
而消息中間件的可用性是指無故障運行的時間百分比,通常用幾個 9 來衡量,如 99.99% 就是一個不錯的指標。
對應的 RabbitMQ 是通過鏡像環形隊列實現多副本及強一致性語義的。多副本可以保證在 master 節點宕機異常之後可以提升 slave 作為新的 master 而繼續提供服務來保障可用性。
6.3.6 運維管理層面
消息中間件一個很重要的考慮層面是運維管理,比如:申請、審核、監控、告警、管理、容災、部署等。
對消息中間件的使用 從使用、接入規範、全方位的監控、流量統計和分析等方面,提供有效的基準數據,也可以在檢測到異常的情況配合告警,以便運維、開發人員的迅速介入。除了一般的監控項(比如硬體、GC 等)之外,對於消息中間件還需要關注端到端時延、消息審計、消息堆積等方面。
對於 RabbitMQ 而言,最正統的監控管理工具莫過於 rabbitmq_management 插件了,另外還有 AppDynamics, Collectd, DataDog, Ganglia 等多種優秀的產品。
Kafka 豐富的管理工具,比如:Kafka Manager, Kafka Monitor, Kafka Offset Monitor 等產品,其中 Cruise 還可以提供自動化運維的功能。
6.3.7 社區力度及生態發展
Kafka 和 RabbitMQ 都有一系列開源的監控管理產品,社區活躍,產品生態都很不錯。