天天看點

五分鐘學後端技術:如何學習後端工程師必學的消息隊列

原創聲明

本文首發于微信公衆号【程式員黃小斜】

本文作者:黃小斜

轉載請務必在文章開頭注明出處和作者。

本文思維導圖

什麼是消息隊列

“RabbitMQ?”“Kafka?”“RocketMQ?”...在日常學習與開發過程中,我們常常聽到消息隊列這個關鍵詞,可能你是熟練使用消息隊列的老手,又或者你是不懂消息隊列的新手,不論你了不了解消息隊列,本文都将帶你搞懂消息隊列的一些基本理論。如果你是老手,你可能從本文學到你之前不曾注意的一些關于消息隊列的重要概念,如果你是新手,相信本文将是你打開消息隊列大門的一闆磚。

根據百度百科的說法,“消息隊列”是在消息的傳輸過程中儲存消息的容器。消息隊列管理器在将消息從它的源中繼到它的目标時充當中間人。隊列的主要目的是提供路由并保證消息的傳遞;如果發送消息時接收者不可用,消息隊列會保留消息,直到可以成功地傳遞它。`

為什麼要使用消息隊列

我覺得使用消息隊列主要有兩點好處:

1.通過異步處理提高系統性能(削峰、減少響應所需時間);

2.降低系統耦合性。如果在面試的時候你被面試官問到這個問題的話,一般情況是你在你的履歷上涉及到消息隊列這方面的内容,這個時候推薦你結合你自己的項目來回答。

《大型網站技術架構》第四章和第七章均有提到消息隊列對應用性能及擴充性的提升。

在我平時的日常工作中,用到消息隊列的場景可不少,比如,我有一個定時任務需要在A應用每天7點開始排程,那麼定時任務系統如何告訴這個A應用呢,一種辦法是直接調用A應用的RPC服務,但是,定時任務系統不可能去記錄那麼多應用的RPC服務,是以如果換成消息,就大大降低了複雜度。

還有一種常見使用消息隊列的場景,那就是把一些不需要及時處理的RPC調用改成消息,比如最典型的電商下單,一定是實時性要求很高的,但是,有一些消息會在使用者下單後進行異步的發送,比如使用者對商品的評價,使用者的退款請求,這些請求不需要被實時地進行處理,完全可以異步化處理,這個時候使用消息隊列就是再好不過的選擇了,消息隊列會幫你存儲這些待處理的消息,并且等應用負載較低的時候再分發給應用處理,或者是等待應用主動向消息隊列擷取消息。

常用的消息隊列

我們可以把消息隊列比作是一個存放消息的容器,當我們需要使用消息的時候可以取出消息供自己使用。消息隊列是分布式系統中重要的元件,使用消息隊列主要是為了通過異步處理提高系統性能和削峰、降低系統耦合性。目前使用較多的消息隊列有ActiveMQ,RabbitMQ,Kafka,RocketMQ。

當然,在我們公司内部用的大多都是自研的消息隊列産品,一方面是因為需要适配金融級分布式場景,另一方面自研的中間件有專門的的團隊維護,出了什麼問題才能及時處理和修複。

下面我們就一起來看看這些開源的消息隊列是怎麼設計的,各有什麼優缺點呢。

RabbitMQ

2007年釋出,是一個在

AMQP

(進階消息隊列協定)基礎上完成的,可複用的企業消息系統,是目前最主流的消息中間件之一。

主要特性:

  1. 可靠性: 提供了多種技術可以讓你在性能和可靠性之間進行權衡。這些技術包括持久性機制、投遞确認、釋出者證明和高可用性機制;
  2. 靈活的路由: 消息在到達隊列前是通過交換機進行路由的。RabbitMQ為典型的路由邏輯提供了多種内置交換機類型。如果你有更複雜的路由需求,可以将這些交換機組合起來使用,你甚至可以實作自己的交換機類型,并且當做RabbitMQ的插件來使用;
  3. 消息叢集:在相同區域網路中的多個RabbitMQ伺服器可以聚合在一起,作為一個獨立的邏輯代理來使用;
  4. 隊列高可用:隊列可以在叢集中的機器上進行鏡像,以確定在硬體問題下還保證消息安全;
  5. 多種協定的支援:支援多種消息隊列協定;
  6. 伺服器端用Erlang語言編寫,支援隻要是你能想到的所有程式設計語言;
  7. 管理界面: RabbitMQ有一個易用的使用者界面,使得使用者可以監控和管理消息Broker的許多方面;
  8. 跟蹤機制:如果消息異常,RabbitMQ提供消息跟蹤機制,使用者可以找出發生了什麼;
  9. 插件機制:提供了許多插件,來從多方面進行擴充,也可以編寫自己的插件;

優點:

  1. 由于erlang語言的特性,mq 性能較好,高并發;
  2. 健壯、穩定、易用、跨平台、支援多種語言、文檔齊全;
  3. 有消息确認機制和持久化機制,可靠性高;
  4. 高度可定制的路由;
  5. 管理界面較豐富,在網際網路公司也有較大規模的應用;
  6. 社群活躍度高;

缺點:

  1. 盡管結合erlang語言本身的并發優勢,性能較好,但是不利于做二次開發和維護;
  2. 實作了代理架構,意味着消息在發送到用戶端之前可以在中央節點上排隊。此特性使得RabbitMQ易于使用和部署,但是使得其運作速度較慢,因為中央節點增加了延遲,消息封裝後也比較大;
  3. 需要學習比較複雜的接口和協定,學習和維護成本較高;

ActiveMQ

是由Apache出品,ActiveMQ 是一個完全支援JMS1.1和J2EE 1.4規範的 JMS Provider實作。它非常快速,支援多種語言的用戶端和協定,而且可以非常容易的嵌入到企業的應用環境中,并有許多進階功能。

  1. 服從JMS 規範:JMS 規範提供了良好的标準和保證,包括:同步或異步的消息分發,一次和僅一次的消息分發,消息接收和訂閱等等。遵從 JMS 規範的好處在于,不論使用什麼 JMS 實作提供者,這些基礎特性都是可用的;
  2. 連接配接性:ActiveMQ 提供了廣泛的連接配接選項,支援的協定有:HTTP/S,IP 多點傳播,SSL,STOMP,TCP,UDP,XMPP等等。對衆多協定的支援讓 ActiveMQ 擁有了很好的靈活性。
  3. 支援的協定種類多:OpenWire、STOMP、REST、XMPP、AMQP ;
  4. 持久化插件和安全插件:ActiveMQ 提供了多種持久化選擇。而且,ActiveMQ 的安全性也可以完全依據使用者需求進行自定義鑒權和授權;
  5. 支援的用戶端語言種類多:除了 Java 之外,還有:C/C++,.NET,Perl,PHP,Python,Ruby;
  6. 代理叢集:多個 ActiveMQ 代理可以組成一個叢集來提供服務;
  7. 異常簡單的管理:ActiveMQ 是以開發者思維被設計的。是以,它并不需要專門的管理者,因為它提供了簡單又使用的管理特性。有很多中方法可以監控 ActiveMQ 不同層面的資料,包括使用在 JConsole 或者 ActiveMQ 的Web Console 中使用 JMX,通過處理 JMX 的告警消息,通過使用指令行腳本,甚至可以通過監控各種類型的日志。
  1. 跨平台(JAVA編寫與平台無關有,ActiveMQ幾乎可以運作在任何的JVM上)
  2. 可以用JDBC:可以将資料持久化到資料庫。雖然使用JDBC會降低ActiveMQ的性能,但是資料庫一直都是開發人員最熟悉的存儲媒體。将消息存到資料庫,看得見摸得着。而且公司有專門的DBA去對資料庫進行調優,主從分離;
  3. 支援JMS :支援JMS的統一接口;
  4. 支援自動重連;
  5. 有安全機制:支援基于shiro,jaas等多種安全配置機制,可以對Queue/Topic進行認證和授權。
  6. 監控完善:擁有完善的監控,包括Web Console,JMX,Shell指令行,Jolokia的REST API;
  7. 界面友善:提供的Web Console可以滿足大部分情況,還有很多第三方的元件可以使用,如hawtio;
  1. 區活躍度不及RabbitMQ高;
  2. 根據其他使用者回報,會出莫名其妙的問題,會丢失消息;
  3. 目前重心放到activemq6.0産品-apollo,對5.x的維護較少;
  4. 不适合用于上千個隊列的應用場景;

RocketMQ

出自 阿裡公司的開源産品,用 Java 語言實作,在設計時參考了 Kafka,并做出了自己的一些改進,消息可靠性上比 Kafka 更好。RocketMQ在阿裡集團被廣泛應用在訂單,交易,充值,流計算,消息推送,日志流式處理,binglog分發等場景。

  1. 是一個隊列模型的消息中間件,具有高性能、高可靠、高實時、分布式特點;
  2. Producer、Consumer、隊列都可以分布式;
  3. Producer向一些隊列輪流發送消息,隊列集合稱為Topic,Consumer如果做廣播消費,則一個consumer執行個體消費這個Topic對應的所有隊列,如果做叢集消費,則多個Consumer執行個體平均消費這個topic對應的隊列集合;
  4. 能夠保證嚴格的消息順序;
  5. 提供豐富的消息拉取模式;
  6. 高效的訂閱者水準擴充能力;
  7. 實時的消息訂閱機制;
  8. 億級消息堆積能力;
  9. 較少的依賴;
  1. 單機支援 1 萬以上持久化隊列
  2. RocketMQ 的所有消息都是持久化的,先寫入系統 PAGECACHE,然後刷盤,可以保證記憶體與磁盤都有一份資料,

通路時,直接從記憶體讀取。

  1. 模型簡單,接口易用(JMS 的接口很多場合并不太實用);
  2. 性能非常好,可以大量堆積消息在broker中;
  3. 支援多種消費,包括叢集消費、廣播消費等。
  4. 各個環節分布式擴充設計,主從HA;
  5. 開發度較活躍,版本更新很快。

支援的用戶端語言不多,目前是java及c++,其中c++不成熟;

RocketMQ社群關注度及成熟度也不及前兩者;

沒有web管理界面,提供了一個CLI(指令行界面)管理工具帶來查詢、管理和診斷各種問題;

沒有在 mq 核心中去實作JMS等接口;

Kafka

Apache Kafka

是一個分布式消息釋出訂閱系統。它最初由LinkedIn公司基于獨特的設計實作為一個分布式的送出日志系統( a distributed commit log),,之後成為Apache項目的一部分。Kafka系統快速、可擴充并且可持久化。它的分區特性,可複制和可容錯都是其不錯的特性。

  1. 快速持久化,可以在O(1)的系統開銷下進行消息持久化;
  2. 高吞吐,在一台普通的伺服器上既可以達到10W/s的吞吐速率;
  3. .完全的分布式系統,Broker、Producer、Consumer都原生自動支援分布式,自動實作負載均衡;
  4. 支援同步和異步複制兩種HA;
  5. 支援資料批量發送和拉取;
  6. zero-copy:減少IO操作步驟;
  7. 資料遷移、擴容對使用者透明;
  8. 無需停機即可擴充機器;
  9. 其他特性:嚴格的消息順序、豐富的消息拉取模型、高效訂閱者水準擴充、實時的消息訂閱、億級的消息堆積能力、定期删除機制;
  1. 用戶端語言豐富,支援java、.net、php、ruby、python、go等多種語言;
  2. 性能卓越,單機寫入TPS約在百萬條/秒,消息大小10個位元組;
  3. 提供完全分布式架構, 并有replica機制, 擁有較高的可用性和可靠性, 理論上支援消息無限堆積;
  4. 支援批量操作;
  5. 消費者采用Pull方式擷取消息, 消息有序, 通過控制能夠保證所有消息被消費且僅被消費一次;
  6. 有優秀的第三方Kafka Web管理界面Kafka-Manager;
  7. 在日志領域比較成熟,被多家公司和多個開源項目使用;
  1. Kafka單機超過64個隊列/分區,Load會發生明顯的飙高現象,隊列越多,load越高,發送消息響應時間變長
  2. 使用短輪詢方式,實時性取決于輪詢間隔時間;
  3. 消費失敗不支援重試;
  4. 支援消息順序,但是一台代理當機後,就會産生消息亂序;
  5. 社群更新較慢;
五分鐘學後端技術:如何學習後端工程師必學的消息隊列

消息隊列的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,減少不必要的請求。

參考文章

http://www.luyixian.cn/news_show_261068.aspx https://blog.51cto.com/caczjz/2141194?source=dra https://www.jianshu.com/p/251b76643d47 https://www.jianshu.com/p/36a7775b04ec