RocketMQ面面觀

RocketMQ面面觀

Rocket基本概念


1 消息模型(Message Model)

RocketMQ主要由 Producer、Broker、Consumer 三部分組成,其中Producer 負責生產消息,Consumer 負責消費消息,Broker 負責存儲消息。Broker 在實際部署過程中對應一臺服務器,每個 Broker 可以存儲多個Topic的消息,每個Topic的消息也可以分片存儲於不同的 Broker。Message Queue 用於存儲消息的物理地址,每個Topic中的消息地址存儲於多個 Message Queue 中。ConsumerGroup 由多個Consumer 實例構成。

2 消息生產者(Producer)

負責生產消息,一般由業務系統負責生產消息。一個消息生產者會把業務應用系統裏產生的消息發送到broker服務器。RocketMQ提供多種發送方式,同步發送、異步發送、順序發送、單向發送。同步和異步方式均需要Broker返回確認信息,單向發送不需要。

3 消息消費者(Consumer)

負責消費消息,一般是後臺系統負責異步消費。一個消息消費者會從Broker服務器拉取消息、並將其提供給應用程序。從用戶應用的角度而言提供了兩種消費形式:拉取式消費、推動式消費。

4 主題(Topic)

表示一類消息的集合,每個主題包含若干條消息,每條消息只能屬於一個主題,是RocketMQ進行消息訂閱的基本單位。

5 代理服務器(Broker Server)

消息中轉角色,負責存儲消息、轉發消息。代理服務器在RocketMQ系統中負責接收從生產者發送來的消息並存儲、同時爲消費者的拉取請求作準備。代理服務器也存儲消息相關的元數據,包括消費者組、消費進度偏移和主題和隊列消息等。

6 名字服務(Name Server)

名稱服務充當路由消息的提供者。生產者或消費者能夠通過名字服務查找各主題相應的Broker IP列表。多個Namesrv實例組成集羣,但相互獨立,沒有信息交換。

7 拉取式消費(Pull Consumer)

Consumer消費的一種類型,應用通常主動調用Consumer的拉消息方法從Broker服務器拉消息、主動權由應用控制。一旦獲取了批量消息,應用就會啓動消費過程。

8 推動式消費(Push Consumer)

Consumer消費的一種類型,該模式下Broker收到數據後會主動推送給消費端,該消費模式一般實時性較高。

9 生產者組(Producer Group)

同一類Producer的集合,這類Producer發送同一類消息且發送邏輯一致。如果發送的是事務消息且原始生產者在發送之後崩潰,則Broker服務器會聯繫同一生產者組的其他生產者實例以提交或回溯消費。

10 消費者組(Consumer Group)

同一類Consumer的集合,這類Consumer通常消費同一類消息且消費邏輯一致。消費者組使得在消息消費方面,實現負載均衡和容錯的目標變得非常容易。要注意的是,消費者組的消費者實例必須訂閱完全相同的Topic。RocketMQ 支持兩種消息模式:集羣消費(Clustering)和廣播消費(Broadcasting)。

11 集羣消費(Clustering)

集羣消費模式下,相同Consumer Group的每個Consumer實例平均分攤消息。

12 廣播消費(Broadcasting)

廣播消費模式下,相同Consumer Group的每個Consumer實例都接收全量的消息。

13 普通順序消息(Normal Ordered Message)

普通順序消費模式下,消費者通過同一個消費隊列收到的消息是有順序的,不同消息隊列收到的消息則可能是無順序的。

14 嚴格順序消息(Strictly Ordered Message)

嚴格順序消息模式下,消費者收到的所有消息均是有順序的。

15 消息(Message)

消息系統所傳輸信息的物理載體,生產和消費數據的最小單位,每條消息必須屬於一個主題。RocketMQ中每個消息擁有唯一的Message ID,且可以攜帶具有業務標識的Key。系統提供了通過Message ID和Key查詢消息的功能。

16 標籤(Tag)

爲消息設置的標誌,用於同一主題下區分不同類型的消息。來自同一業務單元的消息,可以根據不同業務目的在同一主題下設置不同標籤。標籤能夠有效地保持代碼的清晰度和連貫性,並優化RocketMQ提供的查詢系統。消費者可以根據Tag實現對不同子主題的不同消費邏輯,實現更好的擴展性。

特性(features)


1 訂閱與發佈

消息的發佈是指某個生產者向某個topic發送消息;消息的訂閱是指某個消費者關注了某個topic中帶有某些tag的消息,進而從該topic消費數據。

2 消息順序

消息有序指的是一類消息消費時,能按照發送的順序來消費。例如:一個訂單產生了三條消息分別是訂單創建、訂單付款、訂單完成。消費時要按照這個順序消費纔能有意義,但是同時訂單之間是可以並行消費的。RocketMQ可以嚴格的保證消息有序。

順序消息分爲全局順序消息與分區順序消息,全局順序是指某個Topic下的所有消息都要保證順序;部分順序消息只要保證每一組消息被順序消費即可。

  • 全局順序
    對於指定的一個 Topic,所有消息按照嚴格的先入先出(FIFO)的順序進行發佈和消費。
    適用場景:性能要求不高,所有的消息嚴格按照 FIFO 原則進行消息發佈和消費的場景
  • 分區順序
    對於指定的一個 Topic,所有消息根據 sharding key 進行區塊分區。 同一個分區內的消息按照嚴格的 FIFO 順序進行發佈和消費。 Sharding key 是順序消息中用來區分不同分區的關鍵字段,和普通消息的 Key 是完全不同的概念。
    適用場景:性能要求高,以 sharding key 作爲分區字段,在同一個區塊中嚴格的按照 FIFO 原則進行消息發佈和消費的場景。

3 消息過濾

RocketMQ的消費者可以根據Tag進行消息過濾,也支持自定義屬性過濾。消息過濾目前是在Broker端實現的,優點是減少了對於Consumer無用消息的網絡傳輸,缺點是增加了Broker的負擔、而且實現相對複雜。

4 消息可靠性

RocketMQ支持消息的高可靠,影響消息可靠性的幾種情況:

  1. Broker非正常關閉
  2. Broker異常Crash
  3. OS Crash
  4. 機器掉電,但是能立即恢復供電情況
  5. 機器無法開機(可能是cpu、主板、內存等關鍵設備損壞)
  6. 磁盤設備損壞

1)、2)、3)、4) 四種情況都屬於硬件資源可立即恢復情況,RocketMQ在這四種情況下能保證消息不丟,或者丟失少量數據(依賴刷盤方式是同步還是異步)。

5)、6)屬於單點故障,且無法恢復,一旦發生,在此單點上的消息全部丟失。RocketMQ在這兩種情況下,通過異步複製,可保證99%的消息不丟,但是仍然會有極少量的消息可能丟失。通過同步雙寫技術可以完全避免單點,同步雙寫勢必會影響性能,適合對消息可靠性要求極高的場合,例如與Money相關的應用。注:RocketMQ從3.0版本開始支持同步雙寫。

5 至少一次

至少一次(At least Once)指每個消息必須投遞一次。Consumer先Pull消息到本地,消費完成後,才向服務器返回ack,如果沒有消費一定不會ack消息,所以RocketMQ可以很好的支持此特性。

6 回溯消費

回溯消費是指Consumer已經消費成功的消息,由於業務上需求需要重新消費,要支持此功能,Broker在向Consumer投遞成功消息後,消息仍然需要保留。並且重新消費一般是按照時間維度,例如由於Consumer系統故障,恢復後需要重新消費1小時前的數據,那麼Broker要提供一種機制,可以按照時間維度來回退消費進度。RocketMQ支持按照時間回溯消費,時間維度精確到毫秒。

7 事務消息

RocketMQ事務消息(Transactional Message)是指應用本地事務和發送消息操作可以被定義到全局事務中,要麼同時成功,要麼同時失敗。RocketMQ的事務消息提供類似 X/Open XA 的分佈事務功能,通過事務消息能達到分佈式事務的最終一致。

8 定時消息

定時消息(延遲隊列)是指消息發送到broker後,不會立即被消費,等待特定時間投遞給真正的topic。
broker有配置項messageDelayLevel,默認值爲“1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h”,18個level。可以配置自定義messageDelayLevel。注意,messageDelayLevel是broker的屬性,不屬於某個topic。發消息時,設置delayLevel等級即可:msg.setDelayLevel(level)。level有以下三種情況:

  • level == 0,消息爲非延遲消息
  • 1<=level<=maxLevel,消息延遲特定時間,例如level==1,延遲1s
  • level > maxLevel,則level== maxLevel,例如level==20,延遲2h

定時消息會暫存在名爲SCHEDULE_TOPIC_XXXX的topic中,並根據delayTimeLevel存入特定的queue,queueId = delayTimeLevel – 1,即一個queue只存相同延遲的消息,保證具有相同發送延遲的消息能夠順序消費。broker會調度地消費SCHEDULE_TOPIC_XXXX,將消息寫入真實的topic。

需要注意的是,定時消息會在第一次寫入和調度寫入真實topic時都會計數,因此發送數量、tps都會變高。

9 消息重試

Consumer消費消息失敗後,要提供一種重試機制,令消息再消費一次。Consumer消費消息失敗通常可以認爲有以下幾種情況:

  • 由於消息本身的原因,例如反序列化失敗,消息數據本身無法處理(例如話費充值,當前消息的手機號被註銷,無法充值)等。這種錯誤通常需要跳過這條消息,再消費其它消息,而這條失敗的消息即使立刻重試消費,99%也不成功,所以最好提供一種定時重試機制,即過10秒後再重試。
  • 由於依賴的下游應用服務不可用,例如db連接不可用,外系統網絡不可達等。遇到這種錯誤,即使跳過當前失敗的消息,消費其他消息同樣也會報錯。這種情況建議應用sleep 30s,再消費下一條消息,這樣可以減輕Broker重試消息的壓力。

RocketMQ會爲每個消費組都設置一個Topic名稱爲“%RETRY%+consumerGroup”的重試隊列(這裏需要注意的是,這個Topic的重試隊列是針對消費組,而不是針對每個Topic設置的),用於暫時保存因爲各種異常而導致Consumer端無法消費的消息。考慮到異常恢復起來需要一些時間,會爲重試隊列設置多個重試級別,每個重試級別都有與之對應的重新投遞延時,重試次數越多投遞延時就越大。RocketMQ對於重試消息的處理是先保存至Topic名稱爲“SCHEDULE_TOPIC_XXXX”的延遲隊列中,後臺定時任務按照對應的時間進行Delay後重新保存至“%RETRY%+consumerGroup”的重試隊列中。

10 消息重投

生產者在發送消息時,同步消息失敗會重投,異步消息有重試,oneway沒有任何保證。消息重投保證消息儘可能發送成功、不丟失,但可能會造成消息重複,消息重複在RocketMQ中是無法避免的問題。消息重複在一般情況下不會發生,當出現消息量大、網絡抖動,消息重複就會是大概率事件。另外,生產者主動重發、consumer負載變化也會導致重複消息。如下方法可以設置消息重試策略:

  • retryTimesWhenSendFailed:同步發送失敗重投次數,默認爲2,因此生產者會最多嘗試發送retryTimesWhenSendFailed + 1次。不會選擇上次失敗的broker,嘗試向其他broker發送,最大程度保證消息不丟。超過重投次數,拋出異常,由客戶端保證消息不丟。當出現RemotingException、MQClientException和部分MQBrokerException時會重投。
  • retryTimesWhenSendAsyncFailed:異步發送失敗重試次數,異步重試不會選擇其他broker,僅在同一個broker上做重試,不保證消息不丟。
  • retryAnotherBrokerWhenNotStoreOK:消息刷盤(主或備)超時或slave不可用(返回狀態非SEND_OK),是否嘗試發送到其他broker,默認false。十分重要消息可以開啓。

11 流量控制

生產者流控,因爲broker處理能力達到瓶頸;消費者流控,因爲消費能力達到瓶頸。

生產者流控:

  • commitLog文件被鎖時間超過osPageCacheBusyTimeOutMills時,參數默認爲1000ms,返回流控。
  • 如果開啓transientStorePoolEnable == true,且broker爲異步刷盤的主機,且transientStorePool中資源不足,拒絕當前send請求,返回流控。
  • broker每隔10ms檢查send請求隊列頭部請求的等待時間,如果超過waitTimeMillsInSendQueue,默認200ms,拒絕當前send請求,返回流控。
  • broker通過拒絕send 請求方式實現流量控制。

注意,生產者流控,不會嘗試消息重投。

消費者流控:

  • 消費者本地緩存消息數超過pullThresholdForQueue時,默認1000。
  • 消費者本地緩存消息大小超過pullThresholdSizeForQueue時,默認100MB。
  • 消費者本地緩存消息跨度超過consumeConcurrentlyMaxSpan時,默認2000。

消費者流控的結果是降低拉取頻率。

12 死信隊列

死信隊列用於處理無法被正常消費的消息。當一條消息初次消費失敗,消息隊列會自動進行消息重試;達到最大重試次數後,若消費依然失敗,則表明消費者在正常情況下無法正確地消費該消息,此時,消息隊列 不會立刻將消息丟棄,而是將其發送到該消費者對應的特殊隊列中。

RocketMQ將這種正常情況下無法被消費的消息稱爲死信消息(Dead-Letter Message),將存儲死信消息的特殊隊列稱爲死信隊列(Dead-Letter Queue)。在RocketMQ中,可以通過使用console控制檯對死信隊列中的消息進行重發來使得消費者實例再次進行消費。

架構設計


1 技術架構

architecture

RocketMQ架構上主要分爲四部分,如上圖所示:

  • Producer:消息發佈的角色,支持分佈式集羣方式部署。Producer通過MQ的負載均衡模塊選擇相應的Broker集羣隊列進行消息投遞,投遞的過程支持快速失敗並且低延遲。

  • Consumer:消息消費的角色,支持分佈式集羣方式部署。支持以push推,pull拉兩種模式對消息進行消費。同時也支持集羣方式和廣播方式的消費,它提供實時消息訂閱機制,可以滿足大多數用戶的需求。

  • NameServer:NameServer是一個非常簡單的Topic路由註冊中心,其角色類似Dubbo中的zookeeper,支持Broker的動態註冊與發現。主要包括兩個功能:Broker管理,NameServer接受Broker集羣的註冊信息並且保存下來作爲路由信息的基本數據。然後提供心跳檢測機制,檢查Broker是否還存活;路由信息管理,每個NameServer將保存關於Broker集羣的整個路由信息和用於客戶端查詢的隊列信息。然後Producer和Conumser通過NameServer就可以知道整個Broker集羣的路由信息,從而進行消息的投遞和消費。NameServer通常也是集羣的方式部署,各實例間相互不進行信息通訊。Broker是向每一臺NameServer註冊自己的路由信息,所以每一個NameServer實例上面都保存一份完整的路由信息。當某個NameServer因某種原因下線了,Broker仍然可以向其它NameServer同步其路由信息,Producer,Consumer仍然可以動態感知Broker的路由的信息。

  • BrokerServer:Broker主要負責消息的存儲、投遞和查詢以及服務高可用保證,爲了實現這些功能,Broker包含了以下幾個重要子模塊。

  1. Remoting Module:整個Broker的實體,負責處理來自clients端的請求。
  2. Client Manager:負責管理客戶端(Producer/Consumer)和維護Consumer的Topic訂閱信息
  3. Store Service:提供方便簡單的API接口處理消息存儲到物理硬盤和查詢功能。
  4. HA Service:高可用服務,提供Master Broker 和 Slave Broker之間的數據同步功能。
  5. Index Service:根據特定的Message key對投遞到Broker的消息進行索引服務,以提供消息的快速查詢。

architecture

2 部署架構

architecture

RocketMQ 網絡部署特點

  • NameServer是一個幾乎無狀態節點,可集羣部署,節點之間無任何信息同步。

  • Broker部署相對複雜,Broker分爲Master與Slave,一個Master可以對應多個Slave,但是一個Slave只能對應一個Master,Master與Slave 的對應關係通過指定相同的BrokerName,不同的BrokerId 來定義,BrokerId爲0表示Master,非0表示Slave。Master也可以部署多個。每個Broker與NameServer集羣中的所有節點建立長連接,定時註冊Topic信息到所有NameServer。 注意:當前RocketMQ版本在部署架構上支持一Master多Slave,但只有BrokerId=1的從服務器纔會參與消息的讀負載。

  • Producer與NameServer集羣中的其中一個節點(隨機選擇)建立長連接,定期從NameServer獲取Topic路由信息,並向提供Topic 服務的Master建立長連接,且定時向Master發送心跳。Producer完全無狀態,可集羣部署。

  • Consumer與NameServer集羣中的其中一個節點(隨機選擇)建立長連接,定期從NameServer獲取Topic路由信息,並向提供Topic服務的Master、Slave建立長連接,且定時向Master、Slave發送心跳。Consumer既可以從Master訂閱消息,也可以從Slave訂閱消息,消費者在向Master拉取消息時,Master服務器會根據拉取偏移量與最大偏移量的距離(判斷是否讀老消息,產生讀I/O),以及從服務器是否可讀等因素建議下一次是從Master還是Slave拉取。

結合部署架構圖,描述集羣工作流程:

  • 啓動NameServer,NameServer起來後監聽端口,等待Broker、Producer、Consumer連上來,相當於一個路由控制中心。
  • Broker啓動,跟所有的NameServer保持長連接,定時發送心跳包。心跳包中包含當前Broker信息(IP+端口等)以及存儲所有Topic信息。註冊成功後,NameServer集羣中就有Topic跟Broker的映射關係。
  • 收發消息前,先創建Topic,創建Topic時需要指定該Topic要存儲在哪些Broker上,也可以在發送消息時自動創建Topic。
  • Producer發送消息,啓動時先跟NameServer集羣中的其中一臺建立長連接,並從NameServer中獲取當前發送的Topic存在哪些Broker上,輪詢從隊列列表中選擇一個隊列,然後與隊列所在的Broker建立長連接從而向Broker發消息。
  • Consumer跟Producer類似,跟其中一臺NameServer建立長連接,獲取當前訂閱Topic存在哪些Broker上,然後直接跟Broker建立連接通道,開始消費消息。

設計(design)


1 消息存儲

design

消息存儲是RocketMQ中最爲複雜和最爲重要的一部分,本節將分別從RocketMQ的消息存儲整體架構、PageCache與Mmap內存映射以及RocketMQ中兩種不同的刷盤方式三方面來分別展開敘述。

1.1 消息存儲整體架構

消息存儲架構圖中主要有下面三個跟消息存儲相關的文件構成。

(1) CommitLog:消息主體以及元數據的存儲主體,存儲Producer端寫入的消息主體內容,消息內容不是定長的。單個文件大小默認1G ,文件名長度爲20位,左邊補零,剩餘爲起始偏移量,比如00000000000000000000代表了第一個文件,起始偏移量爲0,文件大小爲1G=1073741824;當第一個文件寫滿了,第二個文件爲00000000001073741824,起始偏移量爲1073741824,以此類推。消息主要是順序寫入日誌文件,當文件滿了,寫入下一個文件;

(2) ConsumeQueue:消息消費隊列,引入的目的主要是提高消息消費的性能,由於RocketMQ是基於主題topic的訂閱模式,消息消費是針對主題進行的,如果要遍歷commitlog文件中根據topic檢索消息是非常低效的。Consumer即可根據ConsumeQueue來查找待消費的消息。其中,ConsumeQueue(邏輯消費隊列)作爲消費消息的索引,保存了指定Topic下的隊列消息在CommitLog中的起始物理偏移量offset,消息大小size和消息Tag的HashCode值。consumequeue文件可以看成是基於topic的commitlog索引文件,故consumequeue文件夾的組織方式如下:topic/queue/file三層組織結構,具體存儲路徑爲:$HOME/store/consumequeue/{topic}/{queueId}/{fileName}。同樣consumequeue文件採取定長設計,每一個條目共20個字節,分別爲8字節的commitlog物理偏移量、4字節的消息長度、8字節tag hashcode,單個文件由30W個條目組成,可以像數組一樣隨機訪問每一個條目,每個ConsumeQueue文件大小約5.72M;

(3) IndexFile:IndexFile(索引文件)提供了一種可以通過key或時間區間來查詢消息的方法。Index文件的存儲位置是:$HOME \store\index${fileName},文件名fileName是以創建時的時間戳命名的,固定的單個IndexFile文件大小約爲400M,一個IndexFile可以保存 2000W個索引,IndexFile的底層存儲設計爲在文件系統中實現HashMap結構,故rocketmq的索引文件其底層實現爲hash索引。

在上面的RocketMQ的消息存儲整體架構圖中可以看出,RocketMQ採用的是混合型的存儲結構,即爲Broker單個實例下所有的隊列共用一個日誌數據文件(即爲CommitLog)來存儲。RocketMQ的混合型存儲結構(多個Topic的消息實體內容都存儲於一個CommitLog中)針對Producer和Consumer分別採用了數據和索引部分相分離的存儲結構,Producer發送消息至Broker端,然後Broker端使用同步或者異步的方式對消息刷盤持久化,保存至CommitLog中。只要消息被刷盤持久化至磁盤文件CommitLog中,那麼Producer發送的消息就不會丟失。正因爲如此,Consumer也就肯定有機會去消費這條消息。當無法拉取到消息後,可以等下一次消息拉取,同時服務端也支持長輪詢模式,如果一個消息拉取請求未拉取到消息,Broker允許等待30s的時間,只要這段時間內有新消息到達,將直接返回給消費端。這裏,RocketMQ的具體做法是,使用Broker端的後臺服務線程—ReputMessageService不停地分發請求並異步構建ConsumeQueue(邏輯消費隊列)和IndexFile(索引文件)數據。

1.2 頁緩存與內存映射

頁緩存(PageCache)是OS對文件的緩存,用於加速對文件的讀寫。一般來說,程序對文件進行順序讀寫的速度幾乎接近於內存的讀寫速度,主要原因就是由於OS使用PageCache機制對讀寫訪問操作進行了性能優化,將一部分的內存用作PageCache。對於數據的寫入,OS會先寫入至Cache內,隨後通過異步的方式由pdflush內核線程將Cache內的數據刷盤至物理磁盤上。對於數據的讀取,如果一次讀取文件時出現未命中PageCache的情況,OS從物理磁盤上訪問讀取文件的同時,會順序對其他相鄰塊的數據文件進行預讀取。

在RocketMQ中,ConsumeQueue邏輯消費隊列存儲的數據較少,並且是順序讀取,在page cache機制的預讀取作用下,Consume Queue文件的讀性能幾乎接近讀內存,即使在有消息堆積情況下也不會影響性能。而對於CommitLog消息存儲的日誌數據文件來說,讀取消息內容時候會產生較多的隨機訪問讀取,嚴重影響性能。如果選擇合適的系統IO調度算法,比如設置調度算法爲“Deadline”(此時塊存儲採用SSD的話),隨機讀的性能也會有所提升。

另外,RocketMQ主要通過MappedByteBuffer對文件進行讀寫操作。其中,利用了NIO中的FileChannel模型將磁盤上的物理文件直接映射到用戶態的內存地址中(這種Mmap的方式減少了傳統IO將磁盤文件數據在操作系統內核地址空間的緩衝區和用戶應用程序地址空間的緩衝區之間來回進行拷貝的性能開銷),將對文件的操作轉化爲直接對內存地址進行操作,從而極大地提高了文件的讀寫效率(正因爲需要使用內存映射機制,故RocketMQ的文件存儲都使用定長結構來存儲,方便一次將整個文件映射至內存)。

1.3 消息刷盤

design

(1) 同步刷盤:如上圖所示,只有在消息真正持久化至磁盤後RocketMQ的Broker端纔會真正返回給Producer端一個成功的ACK響應。同步刷盤對MQ消息可靠性來說是一種不錯的保障,但是性能上會有較大影響,一般適用於金融業務應用該模式較多。

(2) 異步刷盤:能夠充分利用OS的PageCache的優勢,只要消息寫入PageCache即可將成功的ACK返回給Producer端。消息刷盤採用後臺異步線程提交的方式進行,降低了讀寫延遲,提高了MQ的性能和吞吐量。

2 通信機制

RocketMQ消息隊列集羣主要包括NameServer、Broker(Master/Slave)、Producer、Consumer4個角色,基本通訊流程如下:

(1) Broker啓動後需要完成一次將自己註冊至NameServer的操作;隨後每隔30s時間定時向NameServer上報Topic路由信息。

(2) 消息生產者Producer作爲客戶端發送消息時候,需要根據消息的Topic從本地緩存的TopicPublishInfoTable獲取路由信息。如果沒有則更新路由信息會從NameServer上重新拉取,同時Producer會默認每隔30s向NameServer拉取一次路由信息。

(3) 消息生產者Producer根據2)中獲取的路由信息選擇一個隊列(MessageQueue)進行消息發送;Broker作爲消息的接收者接收消息並落盤存儲。

(4) 消息消費者Consumer根據2)中獲取的路由信息,並再完成客戶端的負載均衡後,選擇其中的某一個或者某幾個消息隊列來拉取消息並進行消費。

從上面1)~3)中可以看出在消息生產者, Broker和NameServer之間都會發生通信(這裏只說了MQ的部分通信),因此如何設計一個良好的網絡通信模塊在MQ中至關重要,它將決定RocketMQ集羣整體的消息傳輸能力與最終的性能。

rocketmq-remoting 模塊是 RocketMQ消息隊列中負責網絡通信的模塊,它幾乎被其他所有需要網絡通信的模塊(諸如rocketmq-client、rocketmq-broker、rocketmq-namesrv)所依賴和引用。爲了實現客戶端與服務器之間高效的數據請求與接收,RocketMQ消息隊列自定義了通信協議並在Netty的基礎之上擴展了通信模塊。

2.1 Remoting通信類結構

design

2.2 協議設計與編解碼

在Client和Server之間完成一次消息發送時,需要對發送的消息進行一個協議約定,因此就有必要自定義RocketMQ的消息協議。同時,爲了高效地在網絡中傳輸消息和對收到的消息讀取,就需要對消息進行編解碼。在RocketMQ中,RemotingCommand這個類在消息傳輸過程中對所有數據內容的封裝,不但包含了所有的數據結構,還包含了編碼解碼操作。

Header字段 類型 Request說明 Response說明
code int 請求操作碼,應答方根據不同的請求碼進行不同的業務處理 應答響應碼。0表示成功,非0則表示各種錯誤
language LanguageCode 請求方實現的語言 應答方實現的語言
version int 請求方程序的版本 應答方程序的版本
opaque int 相當於requestId,在同一個連接上的不同請求標識碼,與響應消息中的相對應 應答不做修改直接返回
flag int 區分是普通RPC還是onewayRPC得標誌 區分是普通RPC還是onewayRPC得標誌
remark String 傳輸自定義文本信息 傳輸自定義文本信息
extFields HashMap<String, String> 請求自定義擴展信息 響應自定義擴展信息

design

可見傳輸內容主要可以分爲以下4部分:

(1) 消息長度:總長度,四個字節存儲,佔用一個int類型;

(2) 序列化類型&消息頭長度:同樣佔用一個int類型,第一個字節表示序列化類型,後面三個字節表示消息頭長度;

(3) 消息頭數據:經過序列化後的消息頭數據;

(4) 消息主體數據:消息主體的二進制字節數據內容;

2.3 消息的通信方式和流程

在RocketMQ消息隊列中支持通信的方式主要有同步(sync)、異步(async)、單向(oneway)
三種。其中“單向”通信模式相對簡單,一般用在發送心跳包場景下,無需關注其Response。這裏,主要介紹RocketMQ的異步通信流程。

design

2.4 Reactor多線程設計

RocketMQ的RPC通信採用Netty組件作爲底層通信庫,同樣也遵循了Reactor多線程模型,同時又在這之上做了一些擴展和優化。

design

上面的框圖中可以大致瞭解RocketMQ中NettyRemotingServer的Reactor 多線程模型。一個 Reactor 主線程(eventLoopGroupBoss,即爲上面的1)負責監聽 TCP網絡連接請求,建立好連接,創建SocketChannel,並註冊到selector上。RocketMQ的源碼中會自動根據OS的類型選擇NIO和Epoll,也可以通過參數配置),然後監聽真正的網絡數據。拿到網絡數據後,再丟給Worker線程池(eventLoopGroupSelector,即爲上面的“N”,源碼中默認設置爲3),在真正執行業務邏輯之前需要進行SSL驗證、編解碼、空閒檢查、網絡連接管理,這些工作交給defaultEventExecutorGroup(即爲上面的“M1”,源碼中默認設置爲8)去做。而處理業務操作放在業務線程池中執行,根據 RomotingCommand 的業務請求碼code去processorTable這個本地緩存變量中找到對應的 processor,然後封裝成task任務後,提交給對應的業務processor處理線程池來執行(sendMessageExecutor,以發送消息爲例,即爲上面的 “M2”)。從入口到業務邏輯的幾個步驟中線程池一直再增加,這跟每一步邏輯複雜性相關,越複雜,需要的併發通道越寬。

線程數 線程名 線程具體說明
1 NettyBoss_%d Reactor 主線程
N NettyServerEPOLLSelector_%d_%d Reactor 線程池
M1 NettyServerCodecThread_%d Worker線程池
M2 RemotingExecutorThread_%d 業務processor處理線程池

3 消息過濾

RocketMQ分佈式消息隊列的消息過濾方式有別於其它MQ中間件,是在Consumer端訂閱消息時再做消息過濾的。RocketMQ這麼做是在於其Producer端寫入消息和Consumer端訂閱消息採用分離存儲的機制來實現的,Consumer端訂閱消息是需要通過ConsumeQueue這個消息消費的邏輯隊列拿到一個索引,然後再從CommitLog裏面讀取真正的消息實體內容,所以說到底也是還繞不開其存儲結構。其ConsumeQueue的存儲結構如下,可以看到其中有8個字節存儲的Message Tag的哈希值,基於Tag的消息過濾正式基於這個字段值的。

design

主要支持如下2種的過濾方式
(1) Tag過濾方式:Consumer端在訂閱消息時除了指定Topic還可以指定TAG,如果一個消息有多個TAG,可以用||分隔。其中,Consumer端會將這個訂閱請求構建成一個 SubscriptionData,發送一個Pull消息的請求給Broker端。Broker端從RocketMQ的文件存儲層—Store讀取數據之前,會用這些數據先構建一個MessageFilter,然後傳給Store。Store從 ConsumeQueue讀取到一條記錄後,會用它記錄的消息tag hash值去做過濾,由於在服務端只是根據hashcode進行判斷,無法精確對tag原始字符串進行過濾,故在消息消費端拉取到消息後,還需要對消息的原始tag字符串進行比對,如果不同,則丟棄該消息,不進行消息消費。

(2) SQL92的過濾方式:這種方式的大致做法和上面的Tag過濾方式一樣,只是在Store層的具體過濾過程不太一樣,真正的 SQL expression 的構建和執行由rocketmq-filter模塊負責的。每次過濾都去執行SQL表達式會影響效率,所以RocketMQ使用了BloomFilter避免了每次都去執行。SQL92的表達式上下文爲消息的屬性。

4 負載均衡

RocketMQ中的負載均衡都在Client端完成,具體來說的話,主要可以分爲Producer端發送消息時候的負載均衡和Consumer端訂閱消息的負載均衡。

4.1 Producer的負載均衡

Producer端在發送消息的時候,會先根據Topic找到指定的TopicPublishInfo,在獲取了TopicPublishInfo路由信息後,RocketMQ的客戶端在默認方式下selectOneMessageQueue()方法會從TopicPublishInfo中的messageQueueList中選擇一個隊列(MessageQueue)進行發送消息。具體的容錯策略均在MQFaultStrategy這個類中定義。這裏有一個sendLatencyFaultEnable開關變量,如果開啓,在隨機遞增取模的基礎上,再過濾掉not available的Broker代理。所謂的"latencyFaultTolerance",是指對之前失敗的,按一定的時間做退避。例如,如果上次請求的latency超過550Lms,就退避3000Lms;超過1000L,就退避60000L;如果關閉,採用隨機遞增取模的方式選擇一個隊列(MessageQueue)來發送消息,latencyFaultTolerance機制是實現消息發送高可用的核心關鍵所在。

4.2 Consumer的負載均衡

在RocketMQ中,Consumer端的兩種消費模式(Push/Pull)都是基於拉模式來獲取消息的,而在Push模式只是對pull模式的一種封裝,其本質實現爲消息拉取線程在從服務器拉取到一批消息後,然後提交到消息消費線程池後,又“馬不停蹄”的繼續向服務器再次嘗試拉取消息。如果未拉取到消息,則延遲一下又繼續拉取。在兩種基於拉模式的消費方式(Push/Pull)中,均需要Consumer端在知道從Broker端的哪一個消息隊列—隊列中去獲取消息。因此,有必要在Consumer端來做負載均衡,即Broker端中多個MessageQueue分配給同一個ConsumerGroup中的哪些Consumer消費。

1、Consumer端的心跳包發送

在Consumer啓動後,它就會通過定時任務不斷地向RocketMQ集羣中的所有Broker實例發送心跳包(其中包含了,消息消費分組名稱、訂閱關係集合、消息通信模式和客戶端id的值等信息)。Broker端在收到Consumer的心跳消息後,會將它維護在ConsumerManager的本地緩存變量—consumerTable,同時並將封裝後的客戶端網絡通道信息保存在本地緩存變量—channelInfoTable中,爲之後做Consumer端的負載均衡提供可以依據的元數據信息。

2、Consumer端實現負載均衡的核心類—RebalanceImpl

在Consumer實例的啓動流程中的啓動MQClientInstance實例部分,會完成負載均衡服務線程—RebalanceService的啓動(每隔20s執行一次)。通過查看源碼可以發現,RebalanceService線程的run()方法最終調用的是RebalanceImpl類的rebalanceByTopic()方法,該方法是實現Consumer端負載均衡的核心。這裏,rebalanceByTopic()方法會根據消費者通信類型爲“廣播模式”還是“集羣模式”做不同的邏輯處理。這裏主要來看下集羣模式下的主要處理流程:

(1) 從rebalanceImpl實例的本地緩存變量—topicSubscribeInfoTable中,獲取該Topic主題下的消息消費隊列集合(mqSet);

(2) 根據topic和consumerGroup爲參數調用mQClientFactory.findConsumerIdList()方法向Broker端發送獲取該消費組下消費者Id列表的RPC通信請求(Broker端基於前面Consumer端上報的心跳包數據而構建的consumerTable做出響應返回,業務請求碼:GET_CONSUMER_LIST_BY_GROUP);

(3) 先對Topic下的消息消費隊列、消費者Id排序,然後用消息隊列分配策略算法(默認爲:消息隊列的平均分配算法),計算出待拉取的消息隊列。這裏的平均分配算法,類似於分頁的算法,將所有MessageQueue排好序類似於記錄,將所有消費端Consumer排好序類似頁數,並求出每一頁需要包含的平均size和每個頁面記錄的範圍range,最後遍歷整個range而計算出當前Consumer端應該分配到的記錄(這裏即爲:MessageQueue)。

design

(4) 然後,調用updateProcessQueueTableInRebalance()方法,具體的做法是,先將分配到的消息隊列集合(mqSet)與processQueueTable做一個過濾比對。

design

  • 上圖中processQueueTable標註的紅色部分,表示與分配到的消息隊列集合mqSet互不包含。將這些隊列設置Dropped屬性爲true,然後查看這些隊列是否可以移除出processQueueTable緩存變量,這裏具體執行removeUnnecessaryMessageQueue()方法,即每隔1s 查看是否可以獲取當前消費處理隊列的鎖,拿到的話返回true。如果等待1s後,仍然拿不到當前消費處理隊列的鎖則返回false。如果返回true,則從processQueueTable緩存變量中移除對應的Entry;

  • 上圖中processQueueTable的綠色部分,表示與分配到的消息隊列集合mqSet的交集。判斷該ProcessQueue是否已經過期了,在Pull模式的不用管,如果是Push模式的,設置Dropped屬性爲true,並且調用removeUnnecessaryMessageQueue()方法,像上面一樣嘗試移除Entry;

最後,爲過濾後的消息隊列集合(mqSet)中的每個MessageQueue創建一個ProcessQueue對象並存入RebalanceImpl的processQueueTable隊列中(其中調用RebalanceImpl實例的computePullFromWhere(MessageQueue mq)方法獲取該MessageQueue對象的下一個進度消費值offset,隨後填充至接下來要創建的pullRequest對象屬性中),並創建拉取請求對象—pullRequest添加到拉取列表—pullRequestList中,最後執行dispatchPullRequest()方法,將Pull消息的請求對象PullRequest依次放入PullMessageService服務線程的阻塞隊列pullRequestQueue中,待該服務線程取出後向Broker端發起Pull消息的請求。其中,可以重點對比下,RebalancePushImpl和RebalancePullImpl兩個實現類的dispatchPullRequest()方法不同,RebalancePullImpl類裏面的該方法爲空,這樣子也就回答了上一篇中最後的那道思考題了。

消息消費隊列在同一消費組不同消費者之間的負載均衡,其核心設計理念是在一個消息消費隊列在同一時間只允許被同一消費組內的一個消費者消費,一個消息消費者能同時消費多個消息隊列。

5 事務消息

Apache RocketMQ在4.3.0版中已經支持分佈式事務消息,這裏RocketMQ採用了2PC的思想來實現了提交事務消息,同時增加一個補償邏輯來處理二階段超時或者失敗的消息,如下圖所示。

design

5.1 RocketMQ事務消息流程概要

上圖說明了事務消息的大致方案,其中分爲兩個流程:正常事務消息的發送及提交、事務消息的補償流程。

1.事務消息發送及提交:

(1) 發送消息(half消息)。

(2) 服務端響應消息寫入結果。

(3) 根據發送結果執行本地事務(如果寫入失敗,此時half消息對業務不可見,本地邏輯不執行)。

(4) 根據本地事務狀態執行Commit或者Rollback(Commit操作生成消息索引,消息對消費者可見)

2.補償流程:

(1) 對沒有Commit/Rollback的事務消息(pending狀態的消息),從服務端發起一次“回查”

(2) Producer收到回查消息,檢查回查消息對應的本地事務的狀態

(3) 根據本地事務狀態,重新Commit或者Rollback

其中,補償階段用於解決消息Commit或者Rollback發生超時或者失敗的情況。

5.2 RocketMQ事務消息設計

1.事務消息在一階段對用戶不可見

在RocketMQ事務消息的主要流程中,一階段的消息如何對用戶不可見。其中,事務消息相對普通消息最大的特點就是一階段發送的消息對用戶是不可見的。那麼,如何做到寫入消息但是對用戶不可見呢?RocketMQ事務消息的做法是:如果消息是half消息,將備份原消息的主題與消息消費隊列,然後改變主題爲RMQ_SYS_TRANS_HALF_TOPIC。由於消費組未訂閱該主題,故消費端無法消費half類型的消息,然後RocketMQ會開啓一個定時任務,從Topic爲RMQ_SYS_TRANS_HALF_TOPIC中拉取消息進行消費,根據生產者組獲取一個服務提供者發送回查事務狀態請求,根據事務狀態來決定是提交或回滾消息。

在RocketMQ中,消息在服務端的存儲結構如下,每條消息都會有對應的索引信息,Consumer通過ConsumeQueue這個二級索引來讀取消息實體內容,其流程如下:

design

RocketMQ的具體實現策略是:寫入的如果事務消息,對消息的Topic和Queue等屬性進行替換,同時將原來的Topic和Queue信息存儲到消息的屬性中,正因爲消息主題被替換,故消息並不會轉發到該原主題的消息消費隊列,消費者無法感知消息的存在,不會消費。其實改變消息主題是RocketMQ的常用“套路”,回想一下延時消息的實現機制。

2.Commit和Rollback操作以及Op消息的引入

在完成一階段寫入一條對用戶不可見的消息後,二階段如果是Commit操作,則需要讓消息對用戶可見;如果是Rollback則需要撤銷一階段的消息。先說Rollback的情況。對於Rollback,本身一階段的消息對用戶是不可見的,其實不需要真正撤銷消息(實際上RocketMQ也無法去真正的刪除一條消息,因爲是順序寫文件的)。但是區別於這條消息沒有確定狀態(Pending狀態,事務懸而未決),需要一個操作來標識這條消息的最終狀態。RocketMQ事務消息方案中引入了Op消息的概念,用Op消息標識事務消息已經確定的狀態(Commit或者Rollback)。如果一條事務消息沒有對應的Op消息,說明這個事務的狀態還無法確定(可能是二階段失敗了)。引入Op消息後,事務消息無論是Commit或者Rollback都會記錄一個Op操作。Commit相對於Rollback只是在寫入Op消息前創建Half消息的索引。

3.Op消息的存儲和對應關係

RocketMQ將Op消息寫入到全局一個特定的Topic中通過源碼中的方法—TransactionalMessageUtil.buildOpTopic();這個Topic是一個內部的Topic(像Half消息的Topic一樣),不會被用戶消費。Op消息的內容爲對應的Half消息的存儲的Offset,這樣通過Op消息能索引到Half消息進行後續的回查操作。

design

4.Half消息的索引構建

在執行二階段Commit操作時,需要構建出Half消息的索引。一階段的Half消息由於是寫到一個特殊的Topic,所以二階段構建索引時需要讀取出Half消息,並將Topic和Queue替換成真正的目標的Topic和Queue,之後通過一次普通消息的寫入操作來生成一條對用戶可見的消息。所以RocketMQ事務消息二階段其實是利用了一階段存儲的消息的內容,在二階段時恢復出一條完整的普通消息,然後走一遍消息寫入流程。

5.如何處理二階段失敗的消息?

如果在RocketMQ事務消息的二階段過程中失敗了,例如在做Commit操作時,出現網絡問題導致Commit失敗,那麼需要通過一定的策略使這條消息最終被Commit。RocketMQ採用了一種補償機制,稱爲“回查”。Broker端對未確定狀態的消息發起回查,將消息發送到對應的Producer端(同一個Group的Producer),由Producer根據消息來檢查本地事務的狀態,進而執行Commit或者Rollback。Broker端通過對比Half消息和Op消息進行事務消息的回查並且推進CheckPoint(記錄那些事務消息的狀態是確定的)。

值得注意的是,rocketmq並不會無休止的的信息事務狀態回查,默認回查15次,如果15次回查還是無法得知事務狀態,rocketmq默認回滾該消息。

6 消息查詢

RocketMQ支持按照下面兩種維度(“按照Message Id查詢消息”、“按照Message Key查詢消息”)進行消息查詢。

6.1 按照MessageId查詢消息

RocketMQ中的MessageId的長度總共有16字節,其中包含了消息存儲主機地址(IP地址和端口),消息Commit Log offset。“按照MessageId查詢消息”在RocketMQ中具體做法是:Client端從MessageId中解析出Broker的地址(IP地址和端口)和Commit Log的偏移地址後封裝成一個RPC請求後通過Remoting通信層發送(業務請求碼:VIEW_MESSAGE_BY_ID)。Broker端走的是QueryMessageProcessor,讀取消息的過程用其中的 commitLog offset 和 size 去 commitLog 中找到真正的記錄並解析成一個完整的消息返回。

6.2 按照Message Key查詢消息

“按照Message Key查詢消息”,主要是基於RocketMQ的IndexFile索引文件來實現的。RocketMQ的索引文件邏輯結構,類似JDK中HashMap的實現。索引文件的具體結構如下:

design

IndexFile索引文件爲用戶提供通過“按照Message Key查詢消息”的消息索引查詢服務,IndexFile文件的存儲位置是:$HOME\store\index${fileName},文件名fileName是以創建時的時間戳命名的,文件大小是固定的,等於40+500W*4+2000W*20= 420000040個字節大小。如果消息的properties中設置了UNIQ_KEY這個屬性,就用 topic + “#” + UNIQ_KEY的value作爲 key 來做寫入操作。如果消息設置了KEYS屬性(多個KEY以空格分隔),也會用 topic + “#” + KEY 來做索引。

其中的索引數據包含了Key Hash/CommitLog Offset/Timestamp/NextIndex offset 這四個字段,一共20 Byte。NextIndex offset 即前面讀出來的 slotValue,如果有 hash衝突,就可以用這個字段將所有衝突的索引用鏈表的方式串起來了。Timestamp記錄的是消息storeTimestamp之間的差,並不是一個絕對的時間。整個Index File的結構如圖,40 Byte 的Header用於保存一些總的統計信息,4*500W的 Slot Table並不保存真正的索引數據,而是保存每個槽位對應的單向鏈表的頭。20*2000W 是真正的索引數據,即一個 Index File 可以保存 2000W個索引。

“按照Message Key查詢消息”的方式,RocketMQ的具體做法是,主要通過Broker端的QueryMessageProcessor業務處理器來查詢,讀取消息的過程就是用topic和key找到IndexFile索引文件中的一條記錄,根據其中的commitLog offset從CommitLog文件中讀取消息的實體內容。

樣例


1 基本樣例

在基本樣例中我們提供如下的功能場景:

  • 使用RocketMQ發送三種類型的消息:同步消息、異步消息和單向消息。其中前兩種消息是可靠的,因爲會有發送是否成功的應答。
  • 使用RocketMQ來消費接收到的消息。

1.1 加入依賴:

maven:

<dependency>
    <groupId>org.apache.rocketmq</groupId>
    <artifactId>rocketmq-client</artifactId>
    <version>4.3.0</version>
</dependency>

gradle

compile 'org.apache.rocketmq:rocketmq-client:4.3.0'

1.2 消息發送

1、Producer端發送同步消息

這種可靠性同步地發送方式使用的比較廣泛,比如:重要的消息通知,短信通知。

public class SyncProducer {
	public static void main(String[] args) throws Exception {
    	// 實例化消息生產者Producer
        DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name");
    	// 設置NameServer的地址
    	producer.setNamesrvAddr("localhost:9876");
    	// 啓動Producer實例
        producer.start();
    	for (int i = 0; i < 100; i++) {
    	    // 創建消息,並指定Topic,Tag和消息體
    	    Message msg = new Message("TopicTest" /* Topic */,
        	"TagA" /* Tag */,
        	("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET) /* Message body */
        	);
        	// 發送消息到一個Broker
            SendResult sendResult = producer.send(msg);
            // 通過sendResult返回消息是否成功送達
            System.out.printf("%s%n", sendResult);
    	}
    	// 如果不再發送消息,關閉Producer實例。
    	producer.shutdown();
    }
}
2、發送異步消息

異步消息通常用在對響應時間敏感的業務場景,即發送端不能容忍長時間地等待Broker的響應。

public class AsyncProducer {
	public static void main(String[] args) throws Exception {
    	// 實例化消息生產者Producer
        DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name");
    	// 設置NameServer的地址
        producer.setNamesrvAddr("localhost:9876");
    	// 啓動Producer實例
        producer.start();
        producer.setRetryTimesWhenSendAsyncFailed(0);
	
	int messageCount = 100;
        // 根據消息數量實例化倒計時計算器
	final CountDownLatch2 countDownLatch = new CountDownLatch2(messageCount);
    	for (int i = 0; i < messageCount; i++) {
                final int index = i;
            	// 創建消息,並指定Topic,Tag和消息體
                Message msg = new Message("TopicTest",
                    "TagA",
                    "OrderID188",
                    "Hello world".getBytes(RemotingHelper.DEFAULT_CHARSET));
                // SendCallback接收異步返回結果的回調
                producer.send(msg, new SendCallback() {
                    @Override
                    public void onSuccess(SendResult sendResult) {
                        System.out.printf("%-10d OK %s %n", index,
                            sendResult.getMsgId());
                    }
                    @Override
                    public void onException(Throwable e) {
      	              System.out.printf("%-10d Exception %s %n", index, e);
      	              e.printStackTrace();
                    }
            	});
    	}
	// 等待5s
	countDownLatch.await(5, TimeUnit.SECONDS);
    	// 如果不再發送消息,關閉Producer實例。
    	producer.shutdown();
    }
}
3、單向發送消息

這種方式主要用在不特別關心發送結果的場景,例如日誌發送。

public class OnewayProducer {
	public static void main(String[] args) throws Exception{
    	// 實例化消息生產者Producer
        DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name");
    	// 設置NameServer的地址
        producer.setNamesrvAddr("localhost:9876");
    	// 啓動Producer實例
        producer.start();
    	for (int i = 0; i < 100; i++) {
        	// 創建消息,並指定Topic,Tag和消息體
        	Message msg = new Message("TopicTest" /* Topic */,
                "TagA" /* Tag */,
                ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET) /* Message body */
        	);
        	// 發送單向消息,沒有任何返回結果
        	producer.sendOneway(msg);

    	}
    	// 如果不再發送消息,關閉Producer實例。
    	producer.shutdown();
    }
}

1.3 消費消息

public class Consumer {

	public static void main(String[] args) throws InterruptedException, MQClientException {

    	// 實例化消費者
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name");

    	// 設置NameServer的地址
        consumer.setNamesrvAddr("localhost:9876");

    	// 訂閱一個或者多個Topic,以及Tag來過濾需要消費的消息
        consumer.subscribe("TopicTest", "*");
    	// 註冊回調實現類來處理從broker拉取回來的消息
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
                System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msgs);
                // 標記該消息已經被成功消費
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
        // 啓動消費者實例
        consumer.start();
        System.out.printf("Consumer Started.%n");
	}
}

2 順序消息樣例

消息有序指的是可以按照消息的發送順序來消費(FIFO)。RocketMQ可以嚴格的保證消息有序,可以分爲分區有序或者全局有序。

順序消費的原理解析,在默認的情況下消息發送會採取Round Robin輪詢方式把消息發送到不同的queue(分區隊列);而消費消息的時候從多個queue上拉取消息,這種情況發送和消費是不能保證順序。但是如果控制發送的順序消息只依次發送到同一個queue中,消費的時候只從這個queue上依次拉取,則就保證了順序。當發送和消費參與的queue只有一個,則是全局有序;如果多個queue參與,則爲分區有序,即相對每個queue,消息都是有序的。

下面用訂單進行分區有序的示例。一個訂單的順序流程是:創建、付款、推送、完成。訂單號相同的消息會被先後發送到同一個隊列中,消費時,同一個OrderId獲取到的肯定是同一個隊列。

2.1 順序消息生產

package org.apache.rocketmq.example.order2;

import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.MessageQueueSelector;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.common.message.MessageQueue;

import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

/**
* Producer,發送順序消息
*/
public class Producer {

   public static void main(String[] args) throws Exception {
       DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name");

       producer.setNamesrvAddr("127.0.0.1:9876");

       producer.start();

       String[] tags = new String[]{"TagA", "TagC", "TagD"};

       // 訂單列表
       List<OrderStep> orderList = new Producer().buildOrders();

       Date date = new Date();
       SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
       String dateStr = sdf.format(date);
       for (int i = 0; i < 10; i++) {
           // 加個時間前綴
           String body = dateStr + " Hello RocketMQ " + orderList.get(i);
           Message msg = new Message("TopicTest", tags[i % tags.length], "KEY" + i, body.getBytes());

           SendResult sendResult = producer.send(msg, new MessageQueueSelector() {
               @Override
               public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
                   Long id = (Long) arg;  //根據訂單id選擇發送queue
                   long index = id % mqs.size();
                   return mqs.get((int) index);
               }
           }, orderList.get(i).getOrderId());//訂單id

           System.out.println(String.format("SendResult status:%s, queueId:%d, body:%s",
               sendResult.getSendStatus(),
               sendResult.getMessageQueue().getQueueId(),
               body));
       }

       producer.shutdown();
   }

   /**
    * 訂單的步驟
    */
   private static class OrderStep {
       private long orderId;
       private String desc;

       public long getOrderId() {
           return orderId;
       }

       public void setOrderId(long orderId) {
           this.orderId = orderId;
       }

       public String getDesc() {
           return desc;
       }

       public void setDesc(String desc) {
           this.desc = desc;
       }

       @Override
       public String toString() {
           return "OrderStep{" +
               "orderId=" + orderId +
               ", desc='" + desc + '\'' +
               '}';
       }
   }

   /**
    * 生成模擬訂單數據
    */
   private List<OrderStep> buildOrders() {
       List<OrderStep> orderList = new ArrayList<OrderStep>();

       OrderStep orderDemo = new OrderStep();
       orderDemo.setOrderId(15103111039L);
       orderDemo.setDesc("創建");
       orderList.add(orderDemo);

       orderDemo = new OrderStep();
       orderDemo.setOrderId(15103111065L);
       orderDemo.setDesc("創建");
       orderList.add(orderDemo);

       orderDemo = new OrderStep();
       orderDemo.setOrderId(15103111039L);
       orderDemo.setDesc("付款");
       orderList.add(orderDemo);

       orderDemo = new OrderStep();
       orderDemo.setOrderId(15103117235L);
       orderDemo.setDesc("創建");
       orderList.add(orderDemo);

       orderDemo = new OrderStep();
       orderDemo.setOrderId(15103111065L);
       orderDemo.setDesc("付款");
       orderList.add(orderDemo);

       orderDemo = new OrderStep();
       orderDemo.setOrderId(15103117235L);
       orderDemo.setDesc("付款");
       orderList.add(orderDemo);

       orderDemo = new OrderStep();
       orderDemo.setOrderId(15103111065L);
       orderDemo.setDesc("完成");
       orderList.add(orderDemo);

       orderDemo = new OrderStep();
       orderDemo.setOrderId(15103111039L);
       orderDemo.setDesc("推送");
       orderList.add(orderDemo);

       orderDemo = new OrderStep();
       orderDemo.setOrderId(15103117235L);
       orderDemo.setDesc("完成");
       orderList.add(orderDemo);

       orderDemo = new OrderStep();
       orderDemo.setOrderId(15103111039L);
       orderDemo.setDesc("完成");
       orderList.add(orderDemo);

       return orderList;
   }
}

2.2 順序消費消息

import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;
import org.apache.rocketmq.common.message.MessageExt;
import java.util.List;

package org.apache.rocketmq.example.order2;

import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.ConsumeOrderlyContext;
import org.apache.rocketmq.client.consumer.listener.ConsumeOrderlyStatus;
import org.apache.rocketmq.client.consumer.listener.MessageListenerOrderly;
import org.apache.rocketmq.common.consumer.ConsumeFromWhere;
import org.apache.rocketmq.common.message.MessageExt;

import java.util.List;
import java.util.Random;
import java.util.concurrent.TimeUnit;

/**
* 順序消息消費,帶事務方式(應用可控制Offset什麼時候提交)
*/
public class ConsumerInOrder {

   public static void main(String[] args) throws Exception {
       DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name_3");
       consumer.setNamesrvAddr("127.0.0.1:9876");
       /**
        * 設置Consumer第一次啓動是從隊列頭部開始消費還是隊列尾部開始消費<br>
        * 如果非第一次啓動,那麼按照上次消費的位置繼續消費
        */
       consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);

       consumer.subscribe("TopicTest", "TagA || TagC || TagD");

       consumer.registerMessageListener(new MessageListenerOrderly() {

           Random random = new Random();

           @Override
           public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
               context.setAutoCommit(true);
               for (MessageExt msg : msgs) {
                   // 可以看到每個queue有唯一的consume線程來消費, 訂單對每個queue(分區)有序
                   System.out.println("consumeThread=" + Thread.currentThread().getName() + "queueId=" + msg.getQueueId() + ", content:" + new String(msg.getBody()));
               }

               try {
                   //模擬業務邏輯處理中...
                   TimeUnit.SECONDS.sleep(random.nextInt(10));
               } catch (Exception e) {
                   e.printStackTrace();
               }
               return ConsumeOrderlyStatus.SUCCESS;
           }
       });

       consumer.start();

       System.out.println("Consumer Started.");
   }
}

3 延時消息樣例

3.1 啓動消費者等待傳入訂閱消息


import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;
import org.apache.rocketmq.common.message.MessageExt;
import java.util.List;

public class ScheduledMessageConsumer {
   public static void main(String[] args) throws Exception {
      // 實例化消費者
      DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("ExampleConsumer");
      // 訂閱Topics
      consumer.subscribe("TestTopic", "*");
      // 註冊消息監聽者
      consumer.registerMessageListener(new MessageListenerConcurrently() {
          @Override
          public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> messages, ConsumeConcurrentlyContext context) {
              for (MessageExt message : messages) {
                  // Print approximate delay time period
                  System.out.println("Receive message[msgId=" + message.getMsgId() + "] " + (System.currentTimeMillis() - message.getStoreTimestamp()) + "ms later");
              }
              return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
          }
      });
      // 啓動消費者
      consumer.start();
  }
}

3.2 發送延時消息


import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.common.message.Message;

public class ScheduledMessageProducer {
   public static void main(String[] args) throws Exception {
      // 實例化一個生產者來產生延時消息
      DefaultMQProducer producer = new DefaultMQProducer("ExampleProducerGroup");
      // 啓動生產者
      producer.start();
      int totalMessagesToSend = 100;
      for (int i = 0; i < totalMessagesToSend; i++) {
          Message message = new Message("TestTopic", ("Hello scheduled message " + i).getBytes());
          // 設置延時等級3,這個消息將在10s之後發送(現在只支持固定的幾個時間,詳看delayTimeLevel)
          message.setDelayTimeLevel(3);
          // 發送消息
          producer.send(message);
      }
       // 關閉生產者
      producer.shutdown();
  }
}

3.3 驗證

您將會看到消息的消費比存儲時間晚10秒。

3.4 延時消息的使用場景

比如電商裏,提交了一個訂單就可以發送一個延時消息,1h後去檢查這個訂單的狀態,如果還是未付款就取消訂單釋放庫存。

3.5 延時消息的使用限制

// org/apache/rocketmq/store/config/MessageStoreConfig.java

private String messageDelayLevel = "1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h";

現在RocketMq並不支持任意時間的延時,需要設置幾個固定的延時等級,從1s到2h分別對應着等級1到18
消息消費失敗會進入延時消息隊列,消息發送時間與設置的延時等級和重試次數有關,詳見代碼SendMessageProcessor.java

4 批量消息樣例

批量發送消息能顯著提高傳遞小消息的性能。限制是這些批量消息應該有相同的topic,相同的waitStoreMsgOK,而且不能是延時消息。此外,這一批消息的總大小不應超過4MB。

4.1 發送批量消息

如果您每次只發送不超過4MB的消息,則很容易使用批處理,樣例如下:

String topic = "BatchTest";
List<Message> messages = new ArrayList<>();
messages.add(new Message(topic, "TagA", "OrderID001", "Hello world 0".getBytes()));
messages.add(new Message(topic, "TagA", "OrderID002", "Hello world 1".getBytes()));
messages.add(new Message(topic, "TagA", "OrderID003", "Hello world 2".getBytes()));
try {
   producer.send(messages);
} catch (Exception e) {
   e.printStackTrace();
   //處理error
}

4.2 消息列表分割

複雜度只有當你發送大批量時纔會增長,你可能不確定它是否超過了大小限制(4MB)。這時候你最好把你的消息列表分割一下:


public class ListSplitter implements Iterator<List<Message>> {
   private final int SIZE_LIMIT = 1024 * 1024 * 4;
   private final List<Message> messages;
   private int currIndex;
   public ListSplitter(List<Message> messages) {
           this.messages = messages;
   }
   @Override public boolean hasNext() {
       return currIndex < messages.size();
   }
   @Override public List<Message> next() {
       int nextIndex = currIndex;
       int totalSize = 0;
       for (; nextIndex < messages.size(); nextIndex++) {
           Message message = messages.get(nextIndex);
           int tmpSize = message.getTopic().length() + message.getBody().length;
           Map<String, String> properties = message.getProperties();
           for (Map.Entry<String, String> entry : properties.entrySet()) {
               tmpSize += entry.getKey().length() + entry.getValue().length();
           }
           tmpSize = tmpSize + 20; // 增加日誌的開銷20字節
           if (tmpSize > SIZE_LIMIT) {
               //單個消息超過了最大的限制
               //忽略,否則會阻塞分裂的進程
               if (nextIndex - currIndex == 0) {
                  //假如下一個子列表沒有元素,則添加這個子列表然後退出循環,否則只是退出循環
                  nextIndex++;
               }
               break;
           }
           if (tmpSize + totalSize > SIZE_LIMIT) {
               break;
           } else {
               totalSize += tmpSize;
           }

       }
       List<Message> subList = messages.subList(currIndex, nextIndex);
       currIndex = nextIndex;
       return subList;
   }
}
//把大的消息分裂成若干個小的消息
ListSplitter splitter = new ListSplitter(messages);
while (splitter.hasNext()) {
  try {
      List<Message>  listItem = splitter.next();
      producer.send(listItem);
  } catch (Exception e) {
      e.printStackTrace();
      //處理error
  }
}

5 過濾消息樣例

在大多數情況下,TAG是一個簡單而有用的設計,其可以來選擇您想要的消息。例如:

DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("CID_EXAMPLE");
consumer.subscribe("TOPIC", "TAGA || TAGB || TAGC");

消費者將接收包含TAGA或TAGB或TAGC的消息。但是限制是一個消息只能有一個標籤,這對於複雜的場景可能不起作用。在這種情況下,可以使用SQL表達式篩選消息。SQL特性可以通過發送消息時的屬性來進行計算。在RocketMQ定義的語法下,可以實現一些簡單的邏輯。下面是一個例子:

------------
| message  |
|----------|  a > 5 AND b = 'abc'
| a = 10   |  --------------------> Gotten
| b = 'abc'|
| c = true |
------------
------------
| message  |
|----------|   a > 5 AND b = 'abc'
| a = 1    |  --------------------> Missed
| b = 'abc'|
| c = true |
------------

5.1 基本語法

RocketMQ只定義了一些基本語法來支持這個特性。你也可以很容易地擴展它。

  • 數值比較,比如:>,>=,<,<=,BETWEEN,=;
  • 字符比較,比如:=,<>,IN;
  • IS NULL 或者 IS NOT NULL;
  • 邏輯符號 AND,OR,NOT;

常量支持類型爲:

  • 數值,比如:123,3.1415;
  • 字符,比如:‘abc’,必須用單引號包裹起來;
  • NULL,特殊的常量
  • 布爾值,TRUEFALSE

只有使用push模式的消費者才能用使用SQL92標準的sql語句,接口如下:

public void subscribe(finalString topic, final MessageSelector messageSelector)

5.2 使用樣例

1、生產者樣例

發送消息時,你能通過putUserProperty來設置消息的屬性

DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name");
producer.start();
Message msg = new Message("TopicTest",
   tag,
   ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET)
);
// 設置一些屬性
msg.putUserProperty("a", String.valueOf(i));
SendResult sendResult = producer.send(msg);

producer.shutdown();
2、消費者樣例

用MessageSelector.bySql來使用sql篩選消息

DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name_4");
// 只有訂閱的消息有這個屬性a, a >=0 and a <= 3
consumer.subscribe("TopicTest", MessageSelector.bySql("a between 0 and 3");
consumer.registerMessageListener(new MessageListenerConcurrently() {
   @Override
   public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
       return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
   }
});
consumer.start();

6 消息事務樣例

事務消息共有三種狀態,提交狀態、回滾狀態、中間狀態:

  • TransactionStatus.CommitTransaction: 提交事務,它允許消費者消費此消息。
  • TransactionStatus.RollbackTransaction: 回滾事務,它代表該消息將被刪除,不允許被消費。
  • TransactionStatus.Unknown: 中間狀態,它代表需要檢查消息隊列來確定狀態。

6.1 發送事務消息樣例

1、創建事務性生產者

使用 TransactionMQProducer類創建生產者,並指定唯一的 ProducerGroup,就可以設置自定義線程池來處理這些檢查請求。執行本地事務後、需要根據執行結果對消息隊列進行回覆。回傳的事務狀態在請參考前一節。

import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;
import org.apache.rocketmq.common.message.MessageExt;
import java.util.List;
public class TransactionProducer {
   public static void main(String[] args) throws MQClientException, InterruptedException {
       TransactionListener transactionListener = new TransactionListenerImpl();
       TransactionMQProducer producer = new TransactionMQProducer("please_rename_unique_group_name");
       ExecutorService executorService = new ThreadPoolExecutor(2, 5, 100, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(2000), new ThreadFactory() {
           @Override
           public Thread newThread(Runnable r) {
               Thread thread = new Thread(r);
               thread.setName("client-transaction-msg-check-thread");
               return thread;
           }
       });
       producer.setExecutorService(executorService);
       producer.setTransactionListener(transactionListener);
       producer.start();
       String[] tags = new String[] {"TagA", "TagB", "TagC", "TagD", "TagE"};
       for (int i = 0; i < 10; i++) {
           try {
               Message msg =
                   new Message("TopicTest1234", tags[i % tags.length], "KEY" + i,
                       ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET));
               SendResult sendResult = producer.sendMessageInTransaction(msg, null);
               System.out.printf("%s%n", sendResult);
               Thread.sleep(10);
           } catch (MQClientException | UnsupportedEncodingException e) {
               e.printStackTrace();
           }
       }
       for (int i = 0; i < 100000; i++) {
           Thread.sleep(1000);
       }
       producer.shutdown();
   }
}

2、實現事務的監聽接口

當發送半消息成功時,我們使用 executeLocalTransaction 方法來執行本地事務。它返回前一節中提到的三個事務狀態之一。checkLocalTranscation 方法用於檢查本地事務狀態,並回應消息隊列的檢查請求。它也是返回前一節中提到的三個事務狀態之一。

public class TransactionListenerImpl implements TransactionListener {
  private AtomicInteger transactionIndex = new AtomicInteger(0);
  private ConcurrentHashMap<String, Integer> localTrans = new ConcurrentHashMap<>();
  @Override
  public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
      int value = transactionIndex.getAndIncrement();
      int status = value % 3;
      localTrans.put(msg.getTransactionId(), status);
      return LocalTransactionState.UNKNOW;
  }
  @Override
  public LocalTransactionState checkLocalTransaction(MessageExt msg) {
      Integer status = localTrans.get(msg.getTransactionId());
      if (null != status) {
          switch (status) {
              case 0:
                  return LocalTransactionState.UNKNOW;
              case 1:
                  return LocalTransactionState.COMMIT_MESSAGE;
              case 2:
                  return LocalTransactionState.ROLLBACK_MESSAGE;
          }
      }
      return LocalTransactionState.COMMIT_MESSAGE;
  }
}

6.2 事務消息使用上的限制

  1. 事務消息不支持延時消息和批量消息。
  2. 爲了避免單個消息被檢查太多次而導致半隊列消息累積,我們默認將單個消息的檢查次數限制爲 15 次,但是用戶可以通過 Broker 配置文件的 transactionCheckMax參數來修改此限制。如果已經檢查某條消息超過 N 次的話( N = transactionCheckMax ) 則 Broker 將丟棄此消息,並在默認情況下同時打印錯誤日誌。用戶可以通過重寫 AbstractTransactionCheckListener 類來修改這個行爲。
  3. 事務消息將在 Broker 配置文件中的參數 transactionMsgTimeout 這樣的特定時間長度之後被檢查。當發送事務消息時,用戶還可以通過設置用戶屬性 CHECK_IMMUNITY_TIME_IN_SECONDS 來改變這個限制,該參數優先於 transactionMsgTimeout 參數。
  4. 事務性消息可能不止一次被檢查或消費。
  5. 提交給用戶的目標主題消息可能會失敗,目前這依日誌的記錄而定。它的高可用性通過 RocketMQ 本身的高可用性機制來保證,如果希望確保事務消息不丟失、並且事務完整性得到保證,建議使用同步的雙重寫入機制。
  6. 事務消息的生產者 ID 不能與其他類型消息的生產者 ID 共享。與其他類型的消息不同,事務消息允許反向查詢、MQ服務器能通過它們的生產者 ID 查詢到消費者。

7 Logappender樣例

RocketMQ日誌提供log4j、log4j2和logback日誌框架作爲業務應用,下面是配置樣例

7.1 log4j樣例

按下面樣例使用log4j屬性配置

log4j.appender.mq=org.apache.rocketmq.logappender.log4j.RocketmqLog4jAppender
log4j.appender.mq.Tag=yourTag
log4j.appender.mq.Topic=yourLogTopic
log4j.appender.mq.ProducerGroup=yourLogGroup
log4j.appender.mq.NameServerAddress=yourRocketmqNameserverAddress
log4j.appender.mq.layout=org.apache.log4j.PatternLayout
log4j.appender.mq.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %-4r [%t] (%F:%L) %-5p - %m%n

按下面樣例使用log4j xml配置來使用異步添加日誌

<appender name="mqAppender1"class="org.apache.rocketmq.logappender.log4j.RocketmqLog4jAppender">
  <param name="Tag" value="yourTag" />
  <param name="Topic" value="yourLogTopic" />
  <param name="ProducerGroup" value="yourLogGroup" />
  <param name="NameServerAddress" value="yourRocketmqNameserverAddress"/>
  <layout class="org.apache.log4j.PatternLayout">
      <param name="ConversionPattern" value="%d{yyyy-MM-dd HH:mm:ss}-%p %t %c - %m%n" />
  </layout>
</appender>
<appender name="mqAsyncAppender1"class="org.apache.log4j.AsyncAppender">
  <param name="BufferSize" value="1024" />
  <param name="Blocking" value="false" />
  <appender-ref ref="mqAppender1"/>
</appender>

7.2 log4j2樣例

用log4j2時,配置如下,如果想要非阻塞,只需要使用異步添加引用即可

<RocketMQ name="rocketmqAppender" producerGroup="yourLogGroup" nameServerAddress="yourRocketmqNameserverAddress"
   topic="yourLogTopic" tag="yourTag">
  <PatternLayout pattern="%d [%p] hahahah %c %m%n"/>
</RocketMQ>

7.3 logback樣例

<appender name="mqAppender1"class="org.apache.rocketmq.logappender.logback.RocketmqLogbackAppender">
  <tag>yourTag</tag>
  <topic>yourLogTopic</topic>
  <producerGroup>yourLogGroup</producerGroup>
  <nameServerAddress>yourRocketmqNameserverAddress</nameServerAddress>
  <layout>
      <pattern>%date %p %t - %m%n</pattern>
  </layout>
</appender>
<appender name="mqAsyncAppender1"class="ch.qos.logback.classic.AsyncAppender">
  <queueSize>1024</queueSize>
  <discardingThreshold>80</discardingThreshold>
  <maxFlushTime>2000</maxFlushTime>
  <neverBlock>true</neverBlock>
  <appender-ref ref="mqAppender1"/>
</appender>

8 OpenMessaging樣例

OpenMessaging旨在建立消息和流處理規範,以爲金融、電子商務、物聯網和大數據領域提供通用框架及工業級指導方案。在分佈式異構環境中,設計原則是面向雲、簡單、靈活和獨立於語言。符合這些規範將幫助企業方便的開發跨平臺和操作系統的異構消息傳遞應用程序。提供了openmessaging-api 0.3.0-alpha的部分實現,下面的示例演示如何基於OpenMessaging訪問RocketMQ。

8.1 OMSProducer樣例

下面的示例演示如何在同步、異步或單向傳輸中向RocketMQ代理髮送消息。

import io.openmessaging.Future;
import io.openmessaging.FutureListener;
import io.openmessaging.Message;
import io.openmessaging.MessagingAccessPoint;
import io.openmessaging.OMS;
import io.openmessaging.producer.Producer;
import io.openmessaging.producer.SendResult;
import java.nio.charset.Charset;
import java.util.concurrent.CountDownLatch;

public class SimpleProducer {
    public static void main(String[] args) {
       final MessagingAccessPoint messagingAccessPoint =
           OMS.getMessagingAccessPoint("oms:rocketmq://localhost:9876/default:default");
       final Producer producer = messagingAccessPoint.createProducer();
       messagingAccessPoint.startup();
       System.out.printf("MessagingAccessPoint startup OK%n");
       producer.startup();
       System.out.printf("Producer startup OK%n");
       {
           Message message = producer.createBytesMessage("OMS_HELLO_TOPIC", "OMS_HELLO_BODY".getBytes(Charset.forName("UTF-8")));
           SendResult sendResult = producer.send(message);
           //final Void aVoid = result.get(3000L);
           System.out.printf("Send async message OK, msgId: %s%n", sendResult.messageId());
       }
       final CountDownLatch countDownLatch = new CountDownLatch(1);
       {
           final Future<SendResult> result = producer.sendAsync(producer.createBytesMessage("OMS_HELLO_TOPIC", "OMS_HELLO_BODY".getBytes(Charset.forName("UTF-8"))));
           result.addListener(new FutureListener<SendResult>() {
               @Override
               public void operationComplete(Future<SendResult> future) {
                   if (future.getThrowable() != null) {
                       System.out.printf("Send async message Failed, error: %s%n", future.getThrowable().getMessage());
                   } else {
                       System.out.printf("Send async message OK, msgId: %s%n", future.get().messageId());
                   }
                   countDownLatch.countDown();
               }
           });
       }
       {
           producer.sendOneway(producer.createBytesMessage("OMS_HELLO_TOPIC", "OMS_HELLO_BODY".getBytes(Charset.forName("UTF-8"))));
           System.out.printf("Send oneway message OK%n");
       }
       try {
           countDownLatch.await();
           Thread.sleep(500); // 等一些時間來發送消息
       } catch (InterruptedException ignore) {
       }
       producer.shutdown();
   }
}

8.2 OMSPullConsumer

用OMS PullConsumer 來從指定的隊列中拉取消息

import io.openmessaging.Message;
import io.openmessaging.MessagingAccessPoint;
import io.openmessaging.OMS;
import io.openmessaging.OMSBuiltinKeys;
import io.openmessaging.consumer.PullConsumer;
import io.openmessaging.producer.Producer;
import io.openmessaging.producer.SendResult;

public class SimplePullConsumer {
    public static void main(String[] args) {
       final MessagingAccessPoint messagingAccessPoint =
           OMS.getMessagingAccessPoint("oms:rocketmq://localhost:9876/default:default");
       messagingAccessPoint.startup();
       final Producer producer = messagingAccessPoint.createProducer();
       final PullConsumer consumer = messagingAccessPoint.createPullConsumer(
           OMS.newKeyValue().put(OMSBuiltinKeys.CONSUMER_ID, "OMS_CONSUMER"));
       messagingAccessPoint.startup();
       System.out.printf("MessagingAccessPoint startup OK%n");
       final String queueName = "TopicTest";
       producer.startup();
       Message msg = producer.createBytesMessage(queueName, "Hello Open Messaging".getBytes());
       SendResult sendResult = producer.send(msg);
       System.out.printf("Send Message OK. MsgId: %s%n", sendResult.messageId());
       producer.shutdown();
       consumer.attachQueue(queueName);
       consumer.startup();
       System.out.printf("Consumer startup OK%n");
       // 運行直到發現一個消息被髮送了
       boolean stop = false;
       while (!stop) {
           Message message = consumer.receive();
           if (message != null) {
               String msgId = message.sysHeaders().getString(Message.BuiltinKeys.MESSAGE_ID);
               System.out.printf("Received one message: %s%n", msgId);
               consumer.ack(msgId);
               if (!stop) {
                   stop = msgId.equalsIgnoreCase(sendResult.messageId());
               }
           } else {
               System.out.printf("Return without any message%n");
           }
       }
       consumer.shutdown();
       messagingAccessPoint.shutdown();
   }
}

8.3 OMSPushConsumer

以下示範如何將 OMS PushConsumer 添加到指定的隊列,並通過 MessageListener 消費這些消息。

import io.openmessaging.Message;
import io.openmessaging.MessagingAccessPoint;
import io.openmessaging.OMS;
import io.openmessaging.OMSBuiltinKeys;
import io.openmessaging.consumer.MessageListener;
import io.openmessaging.consumer.PushConsumer;

public class SimplePushConsumer {
    public static void main(String[] args) {
       final MessagingAccessPoint messagingAccessPoint = OMS
           .getMessagingAccessPoint("oms:rocketmq://localhost:9876/default:default");
       final PushConsumer consumer = messagingAccessPoint.
           createPushConsumer(OMS.newKeyValue().put(OMSBuiltinKeys.CONSUMER_ID, "OMS_CONSUMER"));
       messagingAccessPoint.startup();
       System.out.printf("MessagingAccessPoint startup OK%n");
       Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
           @Override
           public void run() {
               consumer.shutdown();
               messagingAccessPoint.shutdown();
           }
       }));
       consumer.attachQueue("OMS_HELLO_TOPIC", new MessageListener() {
           @Override
           public void onReceived(Message message, Context context) {
               System.out.printf("Received one message: %s%n", message.sysHeaders().getString(Message.BuiltinKeys.MESSAGE_ID));
               context.ack();
           }
       });
       consumer.startup();
       System.out.printf("Consumer startup OK%n");
   }
}

最佳實踐

1 生產者

1.1 發送消息注意事項

1 Tags的使用

一個應用儘可能用一個Topic,而消息子類型則可以用tags來標識。tags可以由應用自由設置,只有生產者在發送消息設置了tags,消費方在訂閱消息時纔可以利用tags通過broker做消息過濾:message.setTags(“TagA”)。

2 Keys的使用

每個消息在業務層面的唯一標識碼要設置到keys字段,方便將來定位消息丟失問題。服務器會爲每個消息創建索引(哈希索引),應用可以通過topic、key來查詢這條消息內容,以及消息被誰消費。由於是哈希索引,請務必保證key儘可能唯一,這樣可以避免潛在的哈希衝突。

   // 訂單Id   
   String orderId = "20034568923546";   
   message.setKeys(orderId);   
3 日誌的打印

​消息發送成功或者失敗要打印消息日誌,務必要打印SendResult和key字段。send消息方法只要不拋異常,就代表發送成功。發送成功會有多個狀態,在sendResult裏定義。以下對每個狀態進行說明:

  • SEND_OK

消息發送成功。要注意的是消息發送成功也不意味着它是可靠的。要確保不會丟失任何消息,還應啓用同步Master服務器或同步刷盤,即SYNC_MASTER或SYNC_FLUSH。

  • FLUSH_DISK_TIMEOUT

消息發送成功但是服務器刷盤超時。此時消息已經進入服務器隊列(內存),只有服務器宕機,消息纔會丟失。消息存儲配置參數中可以設置刷盤方式和同步刷盤時間長度,如果Broker服務器設置了刷盤方式爲同步刷盤,即FlushDiskType=SYNC_FLUSH(默認爲異步刷盤方式),當Broker服務器未在同步刷盤時間內(默認爲5s)完成刷盤,則將返回該狀態——刷盤超時。

  • FLUSH_SLAVE_TIMEOUT

消息發送成功,但是服務器同步到Slave時超時。此時消息已經進入服務器隊列,只有服務器宕機,消息纔會丟失。如果Broker服務器的角色是同步Master,即SYNC_MASTER(默認是異步Master即ASYNC_MASTER),並且從Broker服務器未在同步刷盤時間(默認爲5秒)內完成與主服務器的同步,則將返回該狀態——數據同步到Slave服務器超時。

  • SLAVE_NOT_AVAILABLE

消息發送成功,但是此時Slave不可用。如果Broker服務器的角色是同步Master,即SYNC_MASTER(默認是異步Master服務器即ASYNC_MASTER),但沒有配置slave Broker服務器,則將返回該狀態——無Slave服務器可用。

1.2 消息發送失敗處理方式

Producer的send方法本身支持內部重試,重試邏輯如下:

  • 至多重試2次(同步發送爲2次,異步發送爲0次)。
  • 如果發送失敗,則輪轉到下一個Broker。這個方法的總耗時時間不超過sendMsgTimeout設置的值,默認10s。
  • 如果本身向broker發送消息產生超時異常,就不會再重試。

以上策略也是在一定程度上保證了消息可以發送成功。如果業務對消息可靠性要求比較高,建議應用增加相應的重試邏輯:比如調用send同步方法發送失敗時,則嘗試將消息存儲到db,然後由後臺線程定時重試,確保消息一定到達Broker。

上述db重試方式爲什麼沒有集成到MQ客戶端內部做,而是要求應用自己去完成,主要基於以下幾點考慮:首先,MQ的客戶端設計爲無狀態模式,方便任意的水平擴展,且對機器資源的消耗僅僅是cpu、內存、網絡。其次,如果MQ客戶端內部集成一個KV存儲模塊,那麼數據只有同步落盤才能較可靠,而同步落盤本身性能開銷較大,所以通常會採用異步落盤,又由於應用關閉過程不受MQ運維人員控制,可能經常會發生 kill -9 這樣暴力方式關閉,造成數據沒有及時落盤而丟失。第三,Producer所在機器的可靠性較低,一般爲虛擬機,不適合存儲重要數據。綜上,建議重試過程交由應用來控制。

1.3選擇oneway形式發送

通常消息的發送是這樣一個過程:

  • 客戶端發送請求到服務器
  • 服務器處理請求
  • 服務器向客戶端返回應答

所以,一次消息發送的耗時時間是上述三個步驟的總和,而某些場景要求耗時非常短,但是對可靠性要求並不高,例如日誌收集類應用,此類應用可以採用oneway形式調用,oneway形式只發送請求不等待應答,而發送請求在客戶端實現層面僅僅是一個操作系統系統調用的開銷,即將數據寫入客戶端的socket緩衝區,此過程耗時通常在微秒級。

2 消費者

2.1 消費過程冪等

RocketMQ無法避免消息重複(Exactly-Once),所以如果業務對消費重複非常敏感,務必要在業務層面進行去重處理。可以藉助關係數據庫進行去重。首先需要確定消息的唯一鍵,可以是msgId,也可以是消息內容中的唯一標識字段,例如訂單Id等。在消費之前判斷唯一鍵是否在關係數據庫中存在。如果不存在則插入,並消費,否則跳過。(實際過程要考慮原子性問題,判斷是否存在可以嘗試插入,如果報主鍵衝突,則插入失敗,直接跳過)

msgId一定是全局唯一標識符,但是實際使用中,可能會存在相同的消息有兩個不同msgId的情況(消費者主動重發、因客戶端重投機制導致的重複等),這種情況就需要使業務字段進行重複消費。

2.2 消費速度慢的處理方式

1 提高消費並行度

絕大部分消息消費行爲都屬於 IO 密集型,即可能是操作數據庫,或者調用 RPC,這類消費行爲的消費速度在於後端數據庫或者外系統的吞吐量,通過增加消費並行度,可以提高總的消費吞吐量,但是並行度增加到一定程度,反而會下降。所以,應用必須要設置合理的並行度。 如下有幾種修改消費並行度的方法:

  • 同一個 ConsumerGroup 下,通過增加 Consumer 實例數量來提高並行度(需要注意的是超過訂閱隊列數的 Consumer 實例無效)。可以通過加機器,或者在已有機器啓動多個進程的方式。
  • 提高單個 Consumer 的消費並行線程,通過修改參數 consumeThreadMin、consumeThreadMax實現。
2 批量方式消費

某些業務流程如果支持批量方式消費,則可以很大程度上提高消費吞吐量,例如訂單扣款類應用,一次處理一個訂單耗時 1 s,一次處理 10 個訂單可能也只耗時 2 s,這樣即可大幅度提高消費的吞吐量,通過設置 consumer的 consumeMessageBatchMaxSize 返個參數,默認是 1,即一次只消費一條消息,例如設置爲 N,那麼每次消費的消息數小於等於 N。

3 跳過非重要消息

發生消息堆積時,如果消費速度一直追不上發送速度,如果業務對數據要求不高的話,可以選擇丟棄不重要的消息。例如,當某個隊列的消息數堆積到100000條以上,則嘗試丟棄部分或全部消息,這樣就可以快速追上發送消息的速度。示例代碼如下:

    public ConsumeConcurrentlyStatus consumeMessage(
            List<MessageExt> msgs,
            ConsumeConcurrentlyContext context) {
        long offset = msgs.get(0).getQueueOffset();
        String maxOffset =
                msgs.get(0).getProperty(Message.PROPERTY_MAX_OFFSET);
        long diff = Long.parseLong(maxOffset) - offset;
        if (diff > 100000) {
            // TODO 消息堆積情況的特殊處理
            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
        }
        // TODO 正常消費過程
        return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
    }    
4 優化每條消息消費過程

舉例如下,某條消息的消費過程如下:

  • 根據消息從 DB 查詢【數據 1】
  • 根據消息從 DB 查詢【數據 2】
  • 複雜的業務計算
  • 向 DB 插入【數據 3】
  • 向 DB 插入【數據 4】

這條消息的消費過程中有4次與 DB的 交互,如果按照每次 5ms 計算,那麼總共耗時 20ms,假設業務計算耗時 5ms,那麼總過耗時 25ms,所以如果能把 4 次 DB 交互優化爲 2 次,那麼總耗時就可以優化到 15ms,即總體性能提高了 40%。所以應用如果對時延敏感的話,可以把DB部署在SSD硬盤,相比於SCSI磁盤,前者的RT會小很多。

2.3 消費打印日誌

如果消息量較少,建議在消費入口方法打印消息,消費耗時等,方便後續排查問題。

   public ConsumeConcurrentlyStatus consumeMessage(
            List<MessageExt> msgs,
            ConsumeConcurrentlyContext context) {
        log.info("RECEIVE_MSG_BEGIN: " + msgs.toString());
        // TODO 正常消費過程
        return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
    }   

如果能打印每條消息消費耗時,那麼在排查消費慢等線上問題時,會更方便。

2.4 其他消費建議

1 關於消費者和訂閱

​第一件需要注意的事情是,不同的消費者組可以獨立的消費一些 topic,並且每個消費者組都有自己的消費偏移量,請確保同一組內的每個消費者訂閱信息保持一致。

2 關於有序消息

消費者將鎖定每個消息隊列,以確保他們被逐個消費,雖然這將會導致性能下降,但是當你關心消息順序的時候會很有用。我們不建議拋出異常,你可以返回 ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT 作爲替代。

3 關於併發消費

顧名思義,消費者將併發消費這些消息,建議你使用它來獲得良好性能,我們不建議拋出異常,你可以返回 ConsumeConcurrentlyStatus.RECONSUME_LATER 作爲替代。

4 關於消費狀態Consume Status

對於併發的消費監聽器,你可以返回 RECONSUME_LATER 來通知消費者現在不能消費這條消息,並且希望可以稍後重新消費它。然後,你可以繼續消費其他消息。對於有序的消息監聽器,因爲你關心它的順序,所以不能跳過消息,但是你可以返回SUSPEND_CURRENT_QUEUE_A_MOMENT 告訴消費者等待片刻。

5 關於Blocking

不建議阻塞監聽器,因爲它會阻塞線程池,並最終可能會終止消費進程

6 關於線程數設置

消費者使用 ThreadPoolExecutor 在內部對消息進行消費,所以你可以通過設置 setConsumeThreadMin 或 setConsumeThreadMax 來改變它。

7 關於消費位點

當建立一個新的消費者組時,需要決定是否需要消費已經存在於 Broker 中的歷史消息CONSUME_FROM_LAST_OFFSET 將會忽略歷史消息,並消費之後生成的任何消息。CONSUME_FROM_FIRST_OFFSET 將會消費每個存在於 Broker 中的信息。你也可以使用 CONSUME_FROM_TIMESTAMP 來消費在指定時間戳後產生的消息。

3 Broker

3.1 Broker 角色

​ Broker 角色分爲 ASYNC_MASTER(異步主機)、SYNC_MASTER(同步主機)以及SLAVE(從機)。如果對消息的可靠性要求比較嚴格,可以採用 SYNC_MASTER加SLAVE的部署方式。如果對消息可靠性要求不高,可以採用ASYNC_MASTER加SLAVE的部署方式。如果只是測試方便,則可以選擇僅ASYNC_MASTER或僅SYNC_MASTER的部署方式。

3.2 FlushDiskType

​ SYNC_FLUSH(同步刷新)相比於ASYNC_FLUSH(異步處理)會損失很多性能,但是也更可靠,所以需要根據實際的業務場景做好權衡。

3.3 Broker 配置

參數名 默認值 說明
listenPort 10911 接受客戶端連接的監聽端口
namesrvAddr null nameServer 地址
brokerIP1 網卡的 InetAddress 當前 broker 監聽的 IP
brokerIP2 跟 brokerIP1 一樣 存在主從 broker 時,如果在 broker 主節點上配置了 brokerIP2 屬性,broker 從節點會連接主節點配置的 brokerIP2 進行同步
brokerName null broker 的名稱
brokerClusterName DefaultCluster 本 broker 所屬的 Cluser 名稱
brokerId 0 broker id, 0 表示 master, 其他的正整數表示 slave
storePathCommitLog $HOME/store/commitlog/ 存儲 commit log 的路徑
storePathConsumerQueue $HOME/store/consumequeue/ 存儲 consume queue 的路徑
mappedFileSizeCommitLog 1024 * 1024 * 1024(1G) commit log 的映射文件大小
deleteWhen 04 在每天的什麼時間刪除已經超過文件保留時間的 commit log
fileReservedTime 72 以小時計算的文件保留時間
brokerRole ASYNC_MASTER SYNC_MASTER/ASYNC_MASTER/SLAVE
flushDiskType ASYNC_FLUSH SYNC_FLUSH/ASYNC_FLUSH SYNC_FLUSH 模式下的 broker 保證在收到確認生產者之前將消息刷盤。ASYNC_FLUSH 模式下的 broker 則利用刷盤一組消息的模式,可以取得更好的性能。

4 NameServer

​RocketMQ 中,Name Servers 被設計用來做簡單的路由管理。其職責包括:

  • Brokers 定期向每個名稱服務器註冊路由數據。
  • 名稱服務器爲客戶端,包括生產者,消費者和命令行客戶端提供最新的路由信息。

5 客戶端配置

​ 相對於RocketMQ的Broker集羣,生產者和消費者都是客戶端。本小節主要描述生產者和消費者公共的行爲配置。

5.1 客戶端尋址方式

RocketMQ可以令客戶端找到Name Server, 然後通過Name Server再找到Broker。如下所示有多種配置方式,優先級由高到低,高優先級會覆蓋低優先級。

  • 代碼中指定Name Server地址,多個namesrv地址之間用分號分割
producer.setNamesrvAddr("192.168.0.1:9876;192.168.0.2:9876");  

consumer.setNamesrvAddr("192.168.0.1:9876;192.168.0.2:9876");
  • Java啓動參數中指定Name Server地址
-Drocketmq.namesrv.addr=192.168.0.1:9876;192.168.0.2:9876  
  • 環境變量指定Name Server地址
export   NAMESRV_ADDR=192.168.0.1:9876;192.168.0.2:9876   
  • HTTP靜態服務器尋址(默認)

客戶端啓動後,會定時訪問一個靜態HTTP服務器,地址如下:http://jmenv.tbsite.net:8080/rocketmq/nsaddr,這個URL的返回內容如下:

192.168.0.1:9876;192.168.0.2:9876   

客戶端默認每隔2分鐘訪問一次這個HTTP服務器,並更新本地的Name Server地址。URL已經在代碼中硬編碼,可通過修改/etc/hosts文件來改變要訪問的服務器,例如在/etc/hosts增加如下配置:

10.232.22.67    jmenv.taobao.net   

推薦使用HTTP靜態服務器尋址方式,好處是客戶端部署簡單,且Name Server集羣可以熱升級。

5.2 客戶端配置

DefaultMQProducer、TransactionMQProducer、DefaultMQPushConsumer、DefaultMQPullConsumer都繼承於ClientConfig類,ClientConfig爲客戶端的公共配置類。客戶端的配置都是get、set形式,每個參數都可以用spring來配置,也可以在代碼中配置,例如namesrvAddr這個參數可以這樣配置,producer.setNamesrvAddr(“192.168.0.1:9876”),其他參數同理。

1 客戶端的公共配置
參數名 默認值 說明
namesrvAddr Name Server地址列表,多個NameServer地址用分號隔開
clientIP 本機IP 客戶端本機IP地址,某些機器會發生無法識別客戶端IP地址情況,需要應用在代碼中強制指定
instanceName DEFAULT 客戶端實例名稱,客戶端創建的多個Producer、Consumer實際是共用一個內部實例(這個實例包含網絡連接、線程資源等)
clientCallbackExecutorThreads 4 通信層異步回調線程數
pollNameServerInteval 30000 輪詢Name Server間隔時間,單位毫秒
heartbeatBrokerInterval 30000 向Broker發送心跳間隔時間,單位毫秒
persistConsumerOffsetInterval 5000 持久化Consumer消費進度間隔時間,單位毫秒
2 Producer配置
參數名 默認值 說明
producerGroup DEFAULT_PRODUCER Producer組名,多個Producer如果屬於一個應用,發送同樣的消息,則應該將它們歸爲同一組
createTopicKey TBW102 在發送消息時,自動創建服務器不存在的topic,需要指定Key,該Key可用於配置發送消息所在topic的默認路由。
defaultTopicQueueNums 4 在發送消息,自動創建服務器不存在的topic時,默認創建的隊列數
sendMsgTimeout 10000 發送消息超時時間,單位毫秒
compressMsgBodyOverHowmuch 4096 消息Body超過多大開始壓縮(Consumer收到消息會自動解壓縮),單位字節
retryAnotherBrokerWhenNotStoreOK FALSE 如果發送消息返回sendResult,但是sendStatus!=SEND_OK,是否重試發送
retryTimesWhenSendFailed 2 如果消息發送失敗,最大重試次數,該參數只對同步發送模式起作用
maxMessageSize 4MB 客戶端限制的消息大小,超過報錯,同時服務端也會限制,所以需要跟服務端配合使用。
transactionCheckListener 事務消息回查監聽器,如果發送事務消息,必須設置
checkThreadPoolMinSize 1 Broker回查Producer事務狀態時,線程池最小線程數
checkThreadPoolMaxSize 1 Broker回查Producer事務狀態時,線程池最大線程數
checkRequestHoldMax 2000 Broker回查Producer事務狀態時,Producer本地緩衝請求隊列大小
RPCHook null 該參數是在Producer創建時傳入的,包含消息發送前的預處理和消息響應後的處理兩個接口,用戶可以在第一個接口中做一些安全控制或者其他操作。
3 PushConsumer配置
參數名 默認值 說明
consumerGroup DEFAULT_CONSUMER Consumer組名,多個Consumer如果屬於一個應用,訂閱同樣的消息,且消費邏輯一致,則應該將它們歸爲同一組
messageModel CLUSTERING 消費模型支持集羣消費和廣播消費兩種
consumeFromWhere CONSUME_FROM_LAST_OFFSET Consumer啓動後,默認從上次消費的位置開始消費,這包含兩種情況:一種是上次消費的位置未過期,則消費從上次中止的位置進行;一種是上次消費位置已經過期,則從當前隊列第一條消息開始消費
consumeTimestamp 半個小時前 只有當consumeFromWhere值爲CONSUME_FROM_TIMESTAMP時才起作用。
allocateMessageQueueStrategy AllocateMessageQueueAveragely Rebalance算法實現策略
subscription 訂閱關係
messageListener 消息監聽器
offsetStore 消費進度存儲
consumeThreadMin 10 消費線程池最小線程數
consumeThreadMax 20 消費線程池最大線程數
consumeConcurrentlyMaxSpan 2000 單隊列並行消費允許的最大跨度
pullThresholdForQueue 1000 拉消息本地隊列緩存消息最大數
pullInterval 0 拉消息間隔,由於是長輪詢,所以爲0,但是如果應用爲了流控,也可以設置大於0的值,單位毫秒
consumeMessageBatchMaxSize 1 批量消費,一次消費多少條消息
pullBatchSize 32 批量拉消息,一次最多拉多少條
4 PullConsumer配置
參數名 默認值 說明
consumerGroup DEFAULT_CONSUMER Consumer組名,多個Consumer如果屬於一個應用,訂閱同樣的消息,且消費邏輯一致,則應該將它們歸爲同一組
brokerSuspendMaxTimeMillis 20000 長輪詢,Consumer拉消息請求在Broker掛起最長時間,單位毫秒
consumerTimeoutMillisWhenSuspend 30000 長輪詢,Consumer拉消息請求在Broker掛起超過指定時間,客戶端認爲超時,單位毫秒
consumerPullTimeoutMillis 10000 非長輪詢,拉消息超時時間,單位毫秒
messageModel BROADCASTING 消息支持兩種模式:集羣消費和廣播消費
messageQueueListener 監聽隊列變化
offsetStore 消費進度存儲
registerTopics 註冊的topic集合
allocateMessageQueueStrategy AllocateMessageQueueAveragely Rebalance算法實現策略
5 Message數據結構
字段名 默認值 說明
Topic null 必填,消息所屬topic的名稱
Body null 必填,消息體
Tags null 選填,消息標籤,方便服務器過濾使用。目前只支持每個消息設置一個tag
Keys null 選填,代表這條消息的業務關鍵詞,服務器會根據keys創建哈希索引,設置後,可以在Console系統根據Topic、Keys來查詢消息,由於是哈希索引,請儘可能保證key唯一,例如訂單號,商品Id等。
Flag 0 選填,完全由應用來設置,RocketMQ不做干預
DelayTimeLevel 0 選填,消息延時級別,0表示不延時,大於0會延時特定的時間纔會被消費
WaitStoreMsgOK TRUE 選填,表示消息是否在服務器落盤後才返回應答。

6 系統配置

本小節主要介紹系統(JVM/OS)相關的配置。

6.1 JVM選項

​ 推薦使用最新發布的JDK 1.8版本。通過設置相同的Xms和Xmx值來防止JVM調整堆大小以獲得更好的性能。簡單的JVM配置如下所示:

​ ​-server -Xms8g -Xmx8g -Xmn4g ​


如果您不關心RocketMQ Broker的啓動時間,還有一種更好的選擇,就是通過“預觸摸”Java堆以確保在JVM初始化期間每個頁面都將被分配。那些不關心啓動時間的人可以啓用它:
​ -XX:+AlwaysPreTouch
禁用偏置鎖定可能會減少JVM暫停,
​ -XX:-UseBiasedLocking
至於垃圾回收,建議使用帶JDK 1.8的G1收集器。

-XX:+UseG1GC -XX:G1HeapRegionSize=16m   
-XX:G1ReservePercent=25 
-XX:InitiatingHeapOccupancyPercent=30

​ 這些GC選項看起來有點激進,但事實證明它在我們的生產環境中具有良好的性能。另外不要把-XX:MaxGCPauseMillis的值設置太小,否則JVM將使用一個小的年輕代來實現這個目標,這將導致非常頻繁的minor GC,所以建議使用rolling GC日誌文件:

-XX:+UseGCLogFileRotation   
-XX:NumberOfGCLogFiles=5 
-XX:GCLogFileSize=30m

如果寫入GC文件會增加代理的延遲,可以考慮將GC日誌文件重定向到內存文件系統:

-Xloggc:/dev/shm/mq_gc_%p.log123   

6.2 Linux內核參數

​ os.sh腳本在bin文件夾中列出了許多內核參數,可以進行微小的更改然後用於生產用途。下面的參數需要注意,更多細節請參考/proc/sys/vm/*的文檔

  • vm.extra_free_kbytes,告訴VM在後臺回收(kswapd)啓動的閾值與直接回收(通過分配進程)的閾值之間保留額外的可用內存。RocketMQ使用此參數來避免內存分配中的長延遲。(與具體內核版本相關)
  • vm.min_free_kbytes,如果將其設置爲低於1024KB,將會巧妙的將系統破壞,並且系統在高負載下容易出現死鎖。
  • vm.max_map_count,限制一個進程可能具有的最大內存映射區域數。RocketMQ將使用mmap加載CommitLog和ConsumeQueue,因此建議將爲此參數設置較大的值。(agressiveness --> aggressiveness)
  • vm.swappiness,定義內核交換內存頁面的積極程度。較高的值會增加攻擊性,較低的值會減少交換量。建議將值設置爲10來避免交換延遲。
  • File descriptor limits,RocketMQ需要爲文件(CommitLog和ConsumeQueue)和網絡連接打開文件描述符。我們建議設置文件描述符的值爲655350。
  • Disk scheduler,RocketMQ建議使用I/O截止時間調度器,它試圖爲請求提供有保證的延遲。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章